2017-03-17 2 views
2

Prenez la hiérarchie de classe C++ simplifiée suivante à titre d'exemple. Ce que je veux accomplir est que Service fournit une méthode virtuelle pour enregistrer des objets arbitraires Model. Mais chaque sous-classe de Service, par ex. BoxService devrait et ne peut enregistrer que Box objets.Paramètres de covariance C++ - Motif de conception

En raison du fait que C++ ne supporte pas covariance dans les paramètres de la méthode, je ne peux pas simplement déclarer la méthode de sauvegarde dans BoxService.h comme:

void save(Box box); 

Ma question est, est-il modèle de conception préféré ou les meilleures pratiques pour ce problème? Ou devrais-je vérifier dans l'implémentation de la fonction de sauvegarde dans BoxService.cpp si l'objet Model arrivant est de type Box et lancer une exception sinon?

Model.h

class Model { 
private: 
    int id; 
}; 

Box.h

class Box : public Model { 
private: 
    int size; 
}; 

Service.h

class Service { 
public: 
    virtual void save(Model model); 
}; 

BoxSe rvice.h

class BoxService : public Service { 
public: 
    void save(Model box); 
}; 

BoxService.cpp

void BoxService::save(Model box) { 
    // TODO: save box and make sure that box is of type 'Box' and not any other subtype of 'Model' 
} 
+5

Ceci casse le polymorphisme. Si on me donne un 'Service' et que j'appelle' save (model) ', cela ne devrait pas échouer si on me donne un' BoxService'. Les paramètres contravariants vont dans l'autre sens. Si 'Service' peut enregistrer une' Box', alors 'BoxService' pourrait prendre Model comme paramètre, puisque les appelants' Service' passeront 'Box's, qui sont' Model', mais les appelants directement 'BoxService 'pourrait passer n'importe quel type' Model'. C'est ce que C++ ne supporte pas. – chris

+0

Pourquoi 'BoxService' hérite' Service'? Un sac de pommes n'est pas un sac de fruits. – aschepler

+0

Je suis d'accord! Est-il même possible d'agencer un dessin similaire à celui décrit ci-dessus sans casser le polymorphisme? –

Répondre

1

Alors vous le son comme vous voulez les implémentations de fonctionnement du groupe par type de modèle. Je vais expliquer une approche plus POO.

séparée Service des mises en œuvre, mais nous allons se débarrasser du paramètre satanés:

class Service { ... }; 
class ServiceImpl { 
    virtual ~ServiceImpl() {} 
    void save() const = 0; 
    void remove() const = 0; 
}; 

Chaque mise en œuvre sera légère et soutenir les opérations, mais prendra le paramètre dans le constructeur:

class BoxService : public ServiceImpl { 
    Box _b; 

public: 
    BoxService(Box b) : _b(b) {} 

    void save() const { ... } 
    void remove() const { ... } 
}; 

maintenant, nous avons une usine abstraite pour créer des implémentations que nous avons besoin:

class ServiceImplFactory { 
public: 
    std::unique_ptr<ServiceImpl> create(Model m) const { 
     if (auto b = dynamic_cast<Box*>(m)) { 
      return std::make_unique<BoxService>(*b); 
     } else if (auto x = dynamic_cast<X*>(m)) { 
      return std::make_unique<XService>(*x); 
     } else { 
      assert(!"Not all Models covered by Service implementations"); 
     } 
    } 
}; 

Maintenant Service:

class Service { 
    ServiceImplFactory _implFactory; 

public: 
    void save(Model m) const { 
     return _implFactory.create(m)->save(); 
    } 

    void remove(Model m) const { 
     return _implFactory.create(m)->remove(); 
    } 
}; 

D'autres étapes:

  • Ingénieur une solution qui donne une erreur de compilation au lieu d'affirmer.
  • Permet à la fois d'ajouter d'autres types de modèles et d'ajouter d'autres opérations sans modifier (beaucoup) le code existant. Cela devrait être équivalent au problème d'expression. Notez que cette approche de regroupement des opérations par type de modèle nécessite un changement beaucoup plus répandu pour ajouter une nouvelle opération que pour ajouter un nouveau type de modèle. L'inverse serait vrai pour l'utilisation d'un visiteur et le regroupement des types de modèles par opération.Il existe des solutions au problème d'expression, telles que object algebras, mais elles peuvent être un peu plus obscures.
1

est ici peut-être une approche plus fonctionnelle:

Paire chaque type de modèle avec sa mise en œuvre:

template<typename T, typename ExtraType> 
struct WithType { 
    T value; 
    using extra_type = ExtraType; 

    WithType(T value) : value(value) {} 
}; 

Définir un Model comme une variante au lieu d'une hiérarchie d'héritage:

using Model = std::variant<WithType<Box, BoxService>, WithType<X, XService>>; 

Maintenant, visitez la variante:

class Service { 
public: 
    void save(Model m) const { 
     visit([](auto withService) { 
      typename decltype(withService)::extra_type service; 
      service.save(withService.value); 
     }, m); 
    } 

    void remove(Model m) const { 
     visit([](auto withService) { 
      typename decltype(withService)::extra_type service; 
      service.remove(withService.value); 
     }, m); 
    } 
}; 
+0

Presque, mais vous n'avez pas de garantie de sécurité de type pour BoxService, qui dans votre modèle pourrait contenir BagService. – lorro

+0

@lorro, je ne comprends pas. En définissant 'Model' de cette façon, vous attachez le type de service correct à l'objet modèle. Si vous appelez 'save (Box {})', il utilisera 'BoxService'. Si vous appelez 'save (Bag {})', il utilisera 'BagService', en supposant que la définition de' Model' est correcte. Si vous vous trompez et attachez 'BagService' à' Box', je crois que toute invocation 'visit' sera une erreur du compilateur. – chris

+0

vous avez raison, j'ai mal interprété le def de Model. – lorro