web-dev-qa-db-fra.com

La «composition sur l'héritage» viole-t-elle le «principe sec»?

Par exemple, considérez que j'ai une classe pour d'autres classes à étendre:

public class LoginPage {
    public String userId;
    public String session;
    public boolean checkSessionValid() {
    }
}

et quelques sous-classes:

public class HomePage extends LoginPage {

}

public class EditInfoPage extends LoginPage {

}

En fait, la sous-classe n'a aucune méthode à remplacer, je n'accéderais pas non plus à la page d'accueil de manière générique, c'est-à-dire: je ne ferais pas quelque chose comme:

for (int i = 0; i < loginPages.length; i++) {
    loginPages[i].doSomething();
}

Je veux juste réutiliser la page de connexion. Mais selon https://stackoverflow.com/a/53354 , je devrais préférer la composition ici car je n'ai pas besoin de l'interface LoginPage, donc je n'utilise pas d'héritage ici:

public class HomePage {
    public LoginPage loginPage;
}

public class EditInfoPage {
    public LoginPage loginPage;
}

mais le problème vient ici, dans la nouvelle version, le code:

public LoginPage loginPage;

duplique quand une nouvelle classe est ajoutée. Et si LoginPage a besoin de setter et getter, plus de codes doivent être copiés:

public LoginPage loginPage;

private LoginPage getLoginPage() {
    return this.loginPage;
}
private void setLoginPage(LoginPage loginPage) {
    this.loginPage = loginPage;
}

Donc ma question est, est-ce que "la composition sur l'héritage" viole le "principe sec"?

36
ocomfd

Euh, tu es inquiet que la répétition

public LoginPage loginPage;

à deux endroits viole SEC? Par cette logique

int x;

ne peut désormais exister que dans un seul objet de la base de code entière. Bleh.

SEC est une bonne chose à garder à l'esprit mais allez. outre

... extends LoginPage

devient dupliqué dans votre alternative, donc même être anal à propos de DRY n'a pas de sens.

Valide DRY les préoccupations ont tendance à se concentrer sur le comportement identique nécessaire à plusieurs endroits étant défini à plusieurs endroits, de sorte qu'un besoin de changer ce comportement finit par nécessiter un changement à plusieurs endroits. Prenez des décisions en un seul endroit et vous n'aurez qu'à les modifier à un seul endroit. Cela ne signifie pas qu'un seul objet peut contenir une référence à votre LoginPage.

DRY ne doit pas être suivi aveuglément. Si vous dupliquez parce que copier-coller est plus facile que de trouver une bonne méthode ou un bon nom de classe, vous avez probablement tort.

Mais si vous voulez mettre le même code dans un endroit différent parce que cet endroit différent est soumis à une responsabilité différente et est susceptible d'avoir besoin de changer indépendamment, il est probablement sage de détendre votre application de DRY et laisser ce comportement identique avoir une identité différente. C'est le même type de pensée qui interdit les nombres magiques.

DRY ne concerne pas seulement l'apparence du code. Il s'agit de ne pas diffuser les détails d'une idée avec la répétition aveugle forçant les responsables à corriger les choses en utilisant la répétition stupide. C'est quand vous essayez de vous dire que la répétition stupide est juste votre convention que les choses vont dans le mauvais sens.

Ce que je pense que vous essayez vraiment de vous plaindre, c'est le code passe-partout. Oui, l'utilisation de la composition plutôt que de l'héritage exige un code passe-partout. Rien n'est exposé gratuitement, vous devez écrire du code qui l'expose. Avec ce passe-partout vient la flexibilité des états, la possibilité de rétrécir l'interface exposée, de donner aux choses des noms différents qui conviennent à leur niveau d'abstraction, une bonne indirection, et vous utilisez ce qui vous compose de l'extérieur, pas le à l'intérieur, vous êtes donc face à son interface normale.

Mais oui, c'est beaucoup de frappe au clavier supplémentaire. Tant que je peux empêcher le problème yo-yo de rebondir de haut en bas sur une pile d'héritage pendant que je lis le code, ça vaut le coup.

Maintenant, ce n'est pas que je refuse de jamais utiliser l'héritage. L'une de mes utilisations préférées est de donner de nouveaux noms aux exceptions:

public class MyLoginPageWasNull extends NullPointerException{}
46
candied_orange

Un malentendu courant avec le principe DRY est qu'il est en quelque sorte lié à la non répétition des lignes de code. Le principe DRY est "Chaque morceau de la connaissance doit avoir une représentation unique, non ambiguë et faisant autorité au sein d'un système " . Il s'agit de la connaissance, pas du code.

LoginPage sait comment dessiner la page pour se connecter. Si EditInfoPage savait comment faire cela, ce serait une violation. Inclure LoginPage via la composition n'est pas en aucune façon une violation du principe DRY).

Le principe DRY est peut-être le principe le plus mal utilisé en génie logiciel et il doit toujours être considéré non pas comme un principe pour ne pas dupliquer du code, mais comme un principe pour ne pas dupliquer les connaissances du domaine abstrait. En fait, dans de nombreux cas, si vous appliquez DRY correctement, alors vous dupliquerez le code , et ce n'est pas nécessairement une mauvaise chose .

127
wasatz

Réponse courte: oui, c'est le cas - à un degré mineur et acceptable.

À première vue, l'héritage peut parfois vous faire économiser quelques lignes de code, car il a pour effet de dire "ma classe de réutilisation contiendra toutes les méthodes et les attributs publics d'une manière 1: 1". Donc, s'il y a une liste de 10 méthodes dans un composant, il n'est pas nécessaire de les répéter dans le code de la classe héritée. Lorsque dans un scénario de composition, 9 de ces 10 méthodes doivent être exposées publiquement via un composant de réutilisation, alors il faut écrire 9 appels de délégation et laisser le reste, il n'y a aucun moyen de contourner cela.

Pourquoi est-ce tolérable? Regardez les méthodes qui sont répliquées dans un scénario de composition - ces méthodes sont exclusivement déléguant des appels à l'interface du composant, ne contenant donc aucune logique réelle.

Le cœur du principe DRY est d'éviter d'avoir deux endroits dans le code où les mêmes règles logiques sont encodées - parce que lorsque ces règles logiques changent, dans le code non DRY il est facile d'adapter l'un de ces endroits et d'oublier l'autre, ce qui introduit une erreur.

Mais comme la délégation des appels ne contient pas de logique, ceux-ci ne sont normalement pas soumis à un tel changement, ce qui ne pose pas de problème réel lors de la "préférence de la composition plutôt que de l'héritage". Et même si l'interface du composant change, ce qui pourrait induire des changements formels dans toutes les classes utilisant le composant, dans un langage compilé, le compilateur nous dira quand nous avons oublié de changer l'un des appelants.

Une note à votre exemple: je ne sais pas à quoi ressemblent votre HomePage et votre EditInfoPage, mais s'ils ont une fonctionnalité de connexion, et un HomePage (ou un EditInfoPage) est unLoginPage, alors l'héritage pourrait être le bon outil ici. Un exemple moins discutable, où la composition sera le meilleur outil d'une manière plus évidente, rendrait probablement les choses plus claires.

En supposant que ni HomePage ni EditInfoPage ne sont un LoginPage, et que l'on veut réutiliser ce dernier, comme vous l'avez écrit, il est fort probable que l'on ne nécessite que certaines parties du LoginPage, pas tout. Si tel est le cas, une meilleure approche que l’utilisation de la composition de la manière indiquée

  • extraire la partie réutilisable de LoginPage dans un composant qui lui est propre

  • réutiliser ce composant dans HomePage et EditInfoPage de la même manière qu'il est maintenant utilisé dans LoginPage

De cette façon, il deviendra généralement beaucoup plus clair pourquoi et quand la composition sur l'héritage est la bonne approche.

12
Doc Brown