J'ai «découvert» les interfaces et j'ai commencé à les aimer. La beauté d'une interface est qu'elle est un contrat, et tout objet qui remplit ce contrat peut être utilisé partout où cette interface est requise.
Le problème avec une interface est qu'elle ne peut pas avoir d'implémentation par défaut, ce qui nuit aux propriétés banales et bat DRY. C'est également bon, car cela maintient la mise en œuvre et le système découplés. L'hérédité, en revanche, maintient un couplage plus étroit et a le potentiel de briser l'encapsulation.
Cas 1 (héritage avec des membres privés, bonne encapsulation, étroitement couplé)
class Employee
{
int money_earned;
string name;
public:
void do_work(){money_earned++;};
string get_name(return name;);
};
class Nurse : public Employee:
{
public:
void do_work(/*do work. Oops, can't update money_earned. Unaware I have to call superclass' do_work()*/);
};
void HireNurse(Nurse *n)
{
nurse->do_work();
)
Cas 2 (juste une interface)
class IEmployee
{
virtual void do_work()=0;
virtual string get_name()=0;
};
//class Nurse implements IEmployee.
//But now, for each employee, must repeat the get_name() implementation,
//and add a name member string, which breaks DRY.
Cas 3: (le meilleur des deux mondes?)
Similaire au cas 1 . Cependant, imaginez que (hypothétiquement) C ++ n'autorise pas les méthodes de substitution, sauf les méthodes qui sont purement virtuelles .
Ainsi, dans le cas 1 , la substitution de do_work () entraînerait une erreur de compilation. Pour résoudre ce problème, nous définissons do_work () comme purement virtuel et ajoutons une méthode distincte increment_money_earned (). Par exemple:
class Employee
{
int money_earned;
string name;
public:
virtual void do_work()=0;
void increment_money_earned(money_earned++;);
string get_name(return name;);
};
class Nurse : public Employee:
{
public:
void do_work(/*do work*/ increment_money_earned(); ); .
};
Mais même cela a des problèmes. Et si dans 3 mois, Joe Coder crée un médecin employé, mais il oublie d'appeler increment_money_earned () dans do_work ()?
La question:
Le cas 3 est-il supérieur au cas 1 ? Est-ce parce qu'il s'agit d'une «meilleure encapsulation» ou d'un «couplage plus lâche», ou pour une autre raison?
Le cas 3 est-il supérieur au cas 2 car il est conforme à SEC?
la source
Réponses:
Une façon de résoudre le problème de l'oubli d'appeler la superclasse est de redonner le contrôle à la superclasse! J'ai re-jigger votre premier exemple pour montrer comment (et le faire compiler;)). Oh, je suppose aussi que
do_work()
dansEmployee
était censé êtrevirtual
dans votre premier exemple.Maintenant,
do_work()
ne peut pas être annulé. Si vous voulez l'étendre, vous devez le faire viaon_do_work()
quido_work()
a le contrôle.Ceci, bien sûr, peut également être utilisé avec l'interface de votre deuxième exemple s'il l'
Employee
étend. Donc, si je vous comprends bien, je pense que cela rend ce cas 3 mais sans avoir à utiliser d'hypothétique C ++! C'est SEC et il a une forte encapsulation.la source
À mon avis, les interfaces ne devraient avoir que des méthodes pures - sans implémentation par défaut. Cela ne rompt en rien le principe DRY, car les interfaces montrent comment accéder à une entité. Juste pour les références, je regarde l'explication DRY ici :
"Chaque élément de connaissance doit avoir une représentation unique, sans ambiguïté et faisant autorité au sein d'un système."
D'un autre côté, le SOLID vous indique que chaque classe doit avoir une interface.
Non, le cas 3 n'est pas supérieur au cas 1. Vous devez vous décider. Si vous souhaitez avoir une implémentation par défaut, faites-le. Si vous voulez une méthode pure, allez-y.
Ensuite, Joe Coder devrait obtenir ce qu'il mérite pour ignorer les tests unitaires ayant échoué. Il a testé cette classe, non? :)
Une taille unique ne convient pas à tous. Il est impossible de dire lequel est le meilleur. Il y a des cas où l'un irait mieux que l'autre.
Vous devriez peut-être apprendre certains modèles de conception au lieu d'essayer d'en inventer quelques-uns.
Je viens de réaliser que vous recherchez un modèle de conception d' interface non virtuelle , car c'est à cela que ressemble votre classe de cas 3.
la source
Les interfaces peuvent avoir des implémentations par défaut en C ++. Rien ne dit qu'une implémentation par défaut d'une fonction ne dépend pas uniquement d'autres membres virtuels (et arguments), donc n'augmente aucun type de couplage.
Pour le cas 2, DRY remplace ici. L'encapsulation existe pour protéger votre programme du changement, des différentes implémentations, mais dans ce cas, vous n'avez pas d'implémentations différentes. Donc encapsulation YAGNI.
En fait, les interfaces d'exécution sont généralement considérées comme inférieures à leurs équivalents au moment de la compilation. Dans le cas de la compilation, vous pouvez avoir à la fois le cas 1 et le cas 2 dans le même paquet, sans parler de ses nombreux autres avantages. Ou même au moment de l'exécution, vous pouvez simplement faire
Employee : public IEmployee
pour effectivement le même avantage. Il existe de nombreuses façons de gérer de telles choses.J'ai arrêté de lire. YAGNI. Le C ++ est ce qu'est le C ++, et le comité des normes ne mettra jamais en œuvre un tel changement, pour d'excellentes raisons.
la source
get_name
. Toutes vos implémentations proposées partageraient la même implémentation deget_name
. De plus, comme je l'ai dit, il n'y a aucune raison de choisir, vous pouvez avoir les deux. De plus, le cas 3 ne vaut absolument rien. Vous pouvez remplacer les virtuels non purs, alors oubliez une conception où vous ne pouvez pas.D'après ce que je vois dans votre implémentation, votre implémentation de cas 3 nécessite une classe abstraite qui peut implémenter des méthodes virtuelles pures qui peuvent ensuite être modifiées dans la classe dérivée. Le cas 3 serait préférable car la classe dérivée peut modifier l'implémentation de do_work au fur et à mesure des besoins et toutes les instances dérivées appartiendraient essentiellement au type abstrait de base.
Je dirais que cela dépend uniquement de la conception de votre implémentation et de l'objectif que vous souhaitez atteindre. La classe abstraite et les interfaces sont implémentées en fonction du problème à résoudre.
Modifier sur la question
Des tests unitaires peuvent être effectués pour vérifier si chaque classe confirme le comportement attendu. Donc, si des tests unitaires appropriés sont appliqués, les bogues peuvent être évités lorsque Joe Coder implémente la nouvelle classe.
la source
L'utilisation d'interfaces n'interrompt DRY que si chaque implémentation est une copie de toutes les autres. Vous pouvez résoudre ce dilemme en appliquant à la fois l'interface et l' héritage, mais dans certains cas, vous souhaiterez peut-être implémenter la même interface sur un certain nombre de classes, mais varier le comportement dans chacune des classes, et cela restera conforme au principe de SEC. Que vous choisissiez d'utiliser l'une des 3 approches que vous avez décrites se résume aux choix que vous devez faire pour appliquer la meilleure technique pour correspondre à une situation donnée. D'un autre côté, vous constaterez probablement qu'avec le temps, vous utilisez davantage les interfaces et n'appliquez l'héritage que là où vous souhaitez supprimer la répétition. Cela ne veut pas dire que c'est le seul raison de l'héritage, mais qu'il est préférable de minimiser l'utilisation de l'héritage pour vous permettre de garder vos options ouvertes si vous trouvez que votre conception doit changer plus tard, et si vous souhaitez minimiser l'impact sur les classes descendantes des effets qu'un changement introduirait dans une classe parent.
la source