Chaînage de méthodes vs encapsulation

17

Il y a le problème classique de POO de chaînage de méthodes vs méthodes de "point d'accès unique":

main.getA().getB().getC().transmogrify(x, y)

contre

main.getA().transmogrifyMyC(x, y)

La première semble avoir l'avantage que chaque classe n'est responsable que d'un ensemble plus petit d'opérations, et rend tout beaucoup plus modulaire - l'ajout d'une méthode à C ne nécessite aucun effort en A, B ou C pour l'exposer.

L'inconvénient, bien sûr, est une encapsulation plus faible , que le deuxième code résout. A contrôle maintenant toutes les méthodes qui la traversent et peut la déléguer à ses champs s'il le souhaite.

Je me rends compte qu'il n'y a pas de solution unique et cela dépend bien sûr du contexte, mais j'aimerais vraiment entendre des commentaires sur d'autres différences importantes entre les deux styles, et dans quelles circonstances devrais-je préférer l'un ou l'autre - parce qu'en ce moment, quand j'essaie pour concevoir du code, j'ai l'impression de ne pas utiliser les arguments pour décider d'une manière ou d'une autre.

Chêne
la source

Réponses:

25

Je pense que la loi de Demeter fournit une orientation importante à cet égard (avec ses avantages et ses inconvénients, qui, comme d'habitude, devraient être mesurés au cas par cas).

L'avantage de suivre la loi de Demeter est que le logiciel résultant a tendance à être plus maintenable et adaptable. Étant donné que les objets dépendent moins de la structure interne des autres objets, les conteneurs d'objets peuvent être modifiés sans retravailler leurs appelants.

Un inconvénient de la loi de Demeter est qu'elle nécessite parfois d'écrire un grand nombre de petites méthodes "wrapper" pour propager les appels de méthode aux composants. De plus, l'interface d'une classe peut devenir volumineuse car elle héberge des méthodes pour les classes contenues, résultant en une classe sans interface cohérente. Mais cela pourrait également être un signe de mauvaise conception OO.

Péter Török
la source
J'ai oublié cette loi, merci de me le rappeler. Mais ce que je demande ici, c'est principalement quels sont les avantages et les inconvénients, ou plus précisément comment dois-je décider d'utiliser un style plutôt qu'un autre.
Oak
@Oak, j'ai ajouté des citations décrivant les avantages et les inconvénients.
Péter Török
10

J'essaie en général de garder la méthode d'enchaînement aussi limitée que possible (basée sur la loi de Déméter )

La seule exception que je fais est pour les interfaces fluides / programmation de style DSL interne.

Martin Fowler fait en quelque sorte la même distinction dans les langages spécifiques au domaine, mais pour des raisons de séparation des requêtes de commande de violation, qui stipule:

que chaque méthode doit être soit une commande qui exécute une action, soit une requête qui renvoie des données à l'appelant, mais pas les deux.

Fowler dans son livre à la page 70 dit:

La séparation commande-requête est un principe extrêmement précieux en programmation, et j'encourage fortement les équipes à l'utiliser. L'une des conséquences de l'utilisation du chaînage de méthodes dans les DSL internes est qu'il rompt généralement ce principe - chaque méthode modifie l'état mais renvoie un objet pour continuer la chaîne. J'ai utilisé de nombreux décibels dénigrant les personnes qui ne suivent pas la séparation commande-requête, et je le ferai à nouveau. Mais les interfaces fluides suivent un ensemble de règles différent, donc je suis heureux de le permettre ici.

KeesDijk
la source
3

Je pense que la question est de savoir si vous utilisez une abstraction appropriée.

Dans le premier cas, nous avons

interface IHasGetA {
    IHasGetB getA();
}

interface IHasGetB {
    IHasGetC getB();
}

interface IHasGetC {
    ITransmogrifyable getC();
}

interface ITransmogrifyable {
    void transmogrify(x,y);
}

Où principal est de type IHasGetA. La question est: cette abstraction est-elle appropriée? La réponse n'est pas anodine. Et dans ce cas, cela semble un peu décalé, mais c'est quand même un exemple théorique. Mais pour construire un exemple différent:

main.getA(v).getB(w).getC(x).transmogrify(y, z);

Est souvent meilleur que

main.superTransmogrify(v, w, x, y, z);

Parce que dans ce dernier exemple, les deux this et maindépendent des types de v, w, x, yet z. De plus, le code ne semble pas vraiment meilleur, si chaque déclaration de méthode a une demi-douzaine d'arguments.

Un localisateur de services nécessite en fait la première approche. Vous ne voulez pas accéder à l'instance qu'il crée via le localisateur de services.

Ainsi, le fait de "traverser" un objet peut créer beaucoup de dépendances, d'autant plus s'il est basé sur les propriétés de classes réelles.
Cependant, créer une abstraction, c'est tout fournir un objet, c'est tout autre chose.

Par exemple, vous pourriez avoir:

class Main implements IHasGetA, IHasGetA, IHasGetA, ITransmogrifyable {
    IHasGetB getA() { return this; }
    IHasGetC getB() { return this; }
    ITransmogrifyable getC() { return this; }
    void transmogrify(x,y) {
        return x + y;//yeah!
    }
}

mainest une instance de Main. Si la classe sachant mainréduit la dépendance à la IHasGetAplace de Main, vous constaterez que le couplage est en fait assez faible. Le code appelant ne sait même pas qu'il appelle en fait la dernière méthode sur l'objet d'origine, ce qui illustre en fait le degré de découplage.
Vous atteignez le long d'un chemin d'abstractions concises et orthogonales, plutôt que profondément dans les internes d'une implémentation.

back2dos
la source
Point très intéressant sur la forte augmentation du nombre de paramètres.
Oak
2

le loi de Déméter , comme le souligne @ Péter Török, suggère la forme "compacte".

En outre, plus vous mentionnez explicitement de méthodes dans votre code, plus votre classe dépend de classes, ce qui augmente les problèmes de maintenance. Dans votre exemple, la forme compacte dépend de deux classes, tandis que la forme plus longue dépend de quatre classes. La forme plus longue ne contrevient pas seulement à la loi de Déméter; il vous fera également changer votre code chaque fois que vous modifiez l'une des quatre méthodes référencées (par opposition à deux dans le format compact).

CesarGon
la source
D'un autre côté, suivre aveuglément cette loi signifie que le nombre de méthodes Ava exploser avec beaucoup de méthodes que Avous voudrez peut-être déléguer de toute façon. Pourtant, je suis d'accord avec les dépendances - cela réduit considérablement la quantité de dépendances requises du code client.
Oak
1
@Oak: Faire quoi que ce soit à l' aveuglette n'est jamais bon. Il faut examiner les avantages et les inconvénients et prendre des décisions fondées sur des preuves. Cela inclut également la loi de Déméter.
CesarGon
2

J'ai moi-même lutté avec ce problème. L'inconvénient de "pénétrer" profondément dans différents objets est que lorsque vous refactorisez, vous finirez par devoir modifier énormément de code car il y a tellement de dépendances. De plus, votre code devient un peu gonflé et plus difficile à lire.

D'un autre côté, avoir des classes qui "transmettent" simplement les méthodes signifie également un surcoût de devoir déclarer un multiple de méthodes à plusieurs endroits.

Une solution qui atténue cela et qui est appropriée dans certains cas est d'avoir une classe d'usine qui construit des objets de façade en copiant les données / objets des classes appropriées. De cette façon, vous pouvez coder contre votre objet de façade et lorsque vous refactorisez, vous changez simplement la logique de l'usine.

Homde
la source
1

Je trouve souvent que la logique d'un programme est plus facile à comprendre avec des méthodes chaînées. Tome,customer.getLastInvoice().itemCount() s'inscrit mieux dans mon cerveau customer.countLastInvoiceItems().

Que cela vaille la peine de maintenance d'avoir le couplage supplémentaire dépend de vous. (J'aime aussi les petites fonctions dans les petites classes, donc j'ai tendance à enchaîner. Je ne dis pas que c'est juste - c'est juste ce que je fais.)

JT Grimes
la source
qui doit être IMO customer.NrLastInvoices ou customer.LastInvoice.NrItems. Cette chaîne n'est pas trop longue, donc cela ne vaut probablement pas la peine d'aplatir si le nombre de combinaisons est assez important
Homde