Chaque fois que je vois une méthode dont le comportement active le type de son paramètre, je considère immédiatement si cette méthode appartient réellement au paramètre de la méthode. Par exemple, au lieu d'avoir une méthode comme:
public void sort(List values) {
if (values instanceof LinkedList) {
// do efficient linked list sort
} else { // ArrayList
// do efficient array list sort
}
}
Je ferais ceci:
values.sort();
// ...
class ArrayList {
public void sort() {
// do efficient array list sort
}
}
class LinkedList {
public void sort() {
// do efficient linked list sort
}
}
Nous déplaçons le comportement à l'endroit qui sait quand l'utiliser. Nous créons une véritable abstraction où vous n'avez pas besoin de connaître les types ou les détails de l'implémentation. Pour votre situation, il peut être plus judicieux de déplacer cette méthode de la classe d'origine (que j'appellerai O
) pour la taper A
et la remplacer dans type B
. Si la méthode est appelée doIt
sur un objet, déplacez-vous doIt
vers A
et remplacez par le comportement différent dans B
. S'il y a des bits de données d'où doIt
est appelé à l' origine, ou si la méthode est utilisée en assez de place, vous pouvez laisser la méthode originale et déléguée:
class O {
int x;
int y;
public void doIt(A a) {
a.doIt(this.x, this.y);
}
}
Nous pouvons cependant plonger un peu plus profondément. Examinons la suggestion d'utiliser un paramètre booléen à la place et voyons ce que nous pouvons apprendre sur la façon dont votre collègue pense. Sa proposition est de faire:
public void doIt(A a, boolean isTypeB) {
if (isTypeB) {
// do B stuff
} else {
// do A stuff
}
}
Cela ressemble énormément à celui que instanceof
j'ai utilisé dans mon premier exemple, sauf que nous extériorisons cette vérification. Cela signifie que nous devrions l'appeler de deux manières:
o.doIt(a, a instanceof B);
ou:
o.doIt(a, true); //or false
Dans le premier cas, le point d'appel n'a aucune idée de son type A
. Par conséquent, devrions-nous passer des booléens tout le long? Est-ce vraiment un modèle que nous voulons partout dans la base de code? Que se passe-t-il s'il existe un troisième type dont nous devons tenir compte? Si c'est ainsi que la méthode est appelée, nous devons la déplacer vers le type et laisser le système choisir l'implémentation pour nous de manière polymorphe.
Dans le deuxième cas, nous devons déjà connaître le type de a
point d'appel. Habituellement, cela signifie que nous y créons l'instance ou prenons une instance de ce type comme paramètre. La création d'une méthode O
qui prend un B
ici fonctionnerait. Le compilateur saurait quelle méthode choisir. Lorsque nous traversons des changements comme celui-ci, la duplication est préférable à la création de la mauvaise abstraction , du moins jusqu'à ce que nous sachions où nous allons vraiment. Bien sûr, je suggère que nous n'avons pas vraiment terminé, peu importe ce que nous avons changé jusqu'à présent.
Nous devons examiner de plus près la relation entre A
et B
. En général, on nous dit que nous devrions privilégier la composition plutôt que l'héritage . Ce n'est pas vrai dans tous les cas, mais c'est vrai dans un nombre surprenant de cas une fois que nous creusons. B
Hérite de A
, ce qui signifie que nous croyons B
être un A
. B
devrait être utilisé comme A
, sauf qu'il fonctionne un peu différemment. Mais quelles sont ces différences? Pouvons-nous donner un nom plus concret aux différences? N'est-ce pas B
un A
, mais a vraiment A
un X
qui pourrait être A'
ou B'
? À quoi ressemblerait notre code si nous faisions cela?
Si nous déplacions la méthode A
comme suggéré précédemment, nous pourrions injecter une instance de X
dans A
et déléguer cette méthode à X
:
class A {
X x;
A(X x) {
this.x = x;
}
public void doIt(int x, int y) {
x.doIt(x, y);
}
}
Nous pouvons mettre en œuvre A'
et B'
, et nous en débarrasser B
. Nous avons amélioré le code en donnant un nom à un concept qui aurait pu être plus implicite, et nous nous sommes permis de définir ce comportement lors de l'exécution au lieu de la compilation. A
est également devenu moins abstrait. Au lieu d'une relation d'héritage étendue, il appelle des méthodes sur un objet délégué. Cet objet est abstrait, mais plus axé uniquement sur les différences de mise en œuvre.
Il y a cependant une dernière chose à regarder. Revenons à la proposition de votre collègue. Si sur tous les sites d'appels nous connaissons explicitement le type que A
nous avons, alors nous devrions faire des appels comme:
B b = new B();
o.doIt(b, true);
Nous avons supposé plus tôt lors de la composition qui A
a un X
qui est soit A'
ou B'
. Mais peut-être même que cette hypothèse n'est pas correcte. Est-ce le seul endroit où cette différence entre A
et B
importe? Si c'est le cas, nous pourrions peut-être adopter une approche légèrement différente. Nous avons toujours un X
qui est A'
ou B'
, mais il n'appartient pas A
. Ne O.doIt
s'en soucie que, alors passons-le uniquement à O.doIt
:
class O {
int x;
int y;
public void doIt(A a, X x) {
x.doIt(a, x, y);
}
}
Maintenant, notre site d'appel ressemble à:
A a = new A();
o.doIt(a, new B'());
Une fois de plus, B
disparaît et l'abstraction se déplace vers le plus concentré X
. Cette fois, cependant, A
est encore plus simple en sachant moins. C'est encore moins abstrait.
Il est important de réduire la duplication dans une base de code, mais nous devons considérer pourquoi la duplication se produit en premier lieu. La duplication peut être le signe d'abstractions plus profondes qui tentent de sortir.