Pourquoi un type serait-il couplé à son constructeur?

20

J'ai récemment supprimé une de mes réponses sur Code Review , qui a commencé comme ceci:

private Person(PersonBuilder builder) {

Arrêtez. Drapeau rouge. Un PersonBuilder construirait une personne; il connaît une personne. La classe Person ne doit rien savoir sur un PersonBuilder - c'est juste un type immuable. Vous avez créé un couplage circulaire ici, où A dépend de B qui dépend de A.

La personne doit simplement prendre ses paramètres; un client qui est prêt à créer une personne sans la construire devrait être en mesure de le faire.

J'ai été giflé avec un downvote, et a dit que (citant) le drapeau rouge, pourquoi? L'implémentation ici a la même forme que celle de Joshua Bloch dans son livre "Effective Java" (article # 2).

Il semble donc que la seule façon d'implémenter un modèle de générateur en Java consiste à faire du générateur un type imbriqué (ce n'est pas de cela qu'il s'agit cependant), puis de créer le produit (la classe de l'objet en cours de construction ) prendre une dépendance sur le générateur , comme ceci:

private StreetMap(Builder builder) {
    // Required parameters
    origin      = builder.origin;
    destination = builder.destination;

    // Optional parameters
    waterColor         = builder.waterColor;
    landColor          = builder.landColor;
    highTrafficColor   = builder.highTrafficColor;
    mediumTrafficColor = builder.mediumTrafficColor;
    lowTrafficColor    = builder.lowTrafficColor;
}

https://en.wikipedia.org/wiki/Builder_pattern#Java_example

La même page Wikipedia pour le même modèle Builder a une implémentation très différente (et beaucoup plus flexible) pour C #:

//Represents a product created by the builder
public class Car
{
    public Car()
    {
    }

    public int Wheels { get; set; }

    public string Colour { get; set; }
}

Comme vous pouvez le voir, le produit ici ne sait rien d'une Builderclasse, et pour tout ce qui le concerne, il pourrait être instancié par un appel direct de constructeur, une usine abstraite, ... ou un constructeur - pour autant que je le comprenne, le produit d'un modèle de création n'a jamais besoin de savoir quoi que ce soit qui le crée.

On m'a servi le contre-argument (qui est apparemment explicitement défendu dans le livre de Bloch) selon lequel un modèle de générateur pourrait être utilisé pour retravailler un type qui aurait un constructeur gonflé de dizaines d'arguments facultatifs. Donc, au lieu de m'en tenir à ce que je pensais savoir, j'ai fait des recherches un peu sur ce site et j'ai découvert que, comme je le soupçonnais, cet argument ne tient pas la route .

Alors, quel est le problème? Pourquoi proposer des solutions sur-conçues à un problème qui ne devrait même pas exister en premier lieu? Si nous enlevons Joshua Bloch de son piédestal pendant une minute, pouvons-nous trouver une seule bonne raison valable pour coupler deux types de béton et l'appeler une meilleure pratique?

Tout cela me fait penser à une programmation culte du fret .

Mathieu Guindon
la source
12
Et cette "question" pue d'être juste une diatribe ...
Philip Kendall
2
@PhilipKendall peut-être un peu. Mais je suis vraiment intéressé à comprendre pourquoi chaque implémentation de constructeur Java a ce couplage étroit.
Mathieu Guindon
19
@PhilipKendall Ça sent la curiosité pour moi.
mât
8
@PhilipKendall Il se lit comme une diatribe, oui, mais à la base il y a une question sur le sujet valide. Peut-être que la diatribe pourrait être éditée?
Andres F.
Je ne suis pas impressionné par l'argument "trop ​​d'arguments constructeurs". Mais je suis encore moins impressionné par ces deux exemples de constructeur. C'est peut-être parce que les exemples sont trop simples pour démontrer un comportement non trivial, mais l'exemple Java se lit comme une interface fluide alambiquée , et l'exemple C # n'est qu'un moyen détourné pour implémenter des propriétés. Aucun des deux exemples ne fait aucune forme de validation des paramètres; un constructeur validerait au moins le type et le nombre d'arguments fournis, ce qui signifie que le constructeur avec trop d'arguments gagne.
Robert Harvey

Réponses:

22

Je ne suis pas d'accord avec votre affirmation selon laquelle il s'agit d'un drapeau rouge. Je suis également en désaccord avec votre représentation du contrepoint, qu'il existe une seule bonne façon de représenter un modèle de générateur.

Pour moi, il y a une question: le générateur est-il une partie nécessaire de l'API du type? Ici, le PersonBuilder est-il une partie nécessaire de l'API Person?

Il est tout à fait raisonnable qu'une classe de personne à validité vérifiée, immuable et / ou étroitement encapsulée soit nécessairement créée via un générateur qu'il fournit (indépendamment du fait que ce générateur soit imbriqué ou adjacent). Ce faisant, la personne peut garder tous ses champs privés ou package-private et final, et également laisser la classe ouverte pour modification s'il s'agit d'un travail en cours. (Il peut finir fermé pour modification et ouvert pour extension, mais ce n'est pas important pour le moment, et est particulièrement controversé pour les objets de données immuables.)

Si une personne est nécessairement créée via un PersonBuilder fourni dans le cadre d'une conception d'API unique au niveau du package, un couplage circulaire est très bien et un drapeau rouge n'est pas justifié ici. Notamment, vous l'avez déclaré comme un fait incontestable et non comme une opinion ou un choix de conception d'API, ce qui peut avoir contribué à une mauvaise réponse. Je ne vous aurais pas rétrogradé, mais je ne blâme pas quelqu'un d'autre de le faire, et je vais laisser le reste de la discussion sur "ce qui justifie un downvote" au centre d'aide et à la méta de révision de code . Les downvotes arrivent; ce n'est pas une "claque".

Bien sûr, si vous voulez laisser beaucoup de façons ouvertes pour créer un objet Personne, alors le PersonBuilder pourrait devenir un consommateur ou un utilitairede la classe Person, dont la personne ne devrait pas dépendre directement. Ce choix semble plus flexible - qui ne voudrait pas plus d'options de création d'objets? - mais pour s'accrocher à des garanties (comme l'immuabilité ou la sérialisation), la Personne doit soudainement exposer un type différent de technique de création publique, comme un constructeur très long . Si le constructeur est censé représenter ou remplacer un constructeur par un grand nombre de champs facultatifs ou un constructeur ouvert à modification, alors le constructeur long de la classe peut être un détail d'implémentation qu'il vaut mieux laisser caché. (Gardez également à l'esprit qu'un constructeur officiel et nécessaire ne vous empêche pas d'écrire votre propre utilitaire de construction, juste que votre utilitaire de construction peut consommer le constructeur en tant qu'API comme dans ma question initiale ci-dessus.)


ps: je note que l'exemple de révision de code que vous avez répertorié a une personne immuable, mais le contre-exemple de Wikipedia répertorie une voiture mutable avec des getters et des setters. Il peut être plus facile de voir les machines nécessaires omises de la voiture si vous gardez la langue et les invariants cohérents entre eux.

Jeff Bowman soutient Monica
la source
Je pense que je vois votre point sur le traitement du constructeur comme un détail d'implémentation. Il ne semble tout simplement pas que Java efficace transmette correctement ce message (je n'ai pas / n'ai pas lu ce livre), laissant une tonne de développeurs Java juniors (?) En supposant que "c'est comme ça que ça se passe", indépendamment du bon sens . Je suis d'accord que les exemples de Wikipédia sont mauvais pour la comparaison .. mais bon c'est Wikipédia .. et FWIW je ne me soucie pas vraiment du downvote; la réponse supprimée est assise avec un score net positif de toute façon (et il y a ma chance de finalement marquer un [badge: pression des pairs]).
Mathieu Guindon
1
Pourtant, je n'arrive pas à me débarrasser de l'idée qu'un type avec trop d'arguments constructeurs est un problème de conception (odeurs de rupture de SRP), qu'un modèle de constructeur enfile rapidement sous un tapis.
Mathieu Guindon
3
@ Mat'sMug Mon article ci-dessus considère que la classe a besoin d'autant d'arguments constructeurs . Je le traiterais également comme une odeur de conception si nous parlions d'un composant de logique métier arbitraire, mais pour un objet de données, il n'est pas question pour un type d'avoir dix ou vingt attributs directs. Par exemple, ce n'est pas une violation de SRP qu'un objet représente un seul enregistrement fortement typé dans un journal .
Jeff Bowman soutient Monica
1
C'est suffisant. Je ne comprends toujours pas le String(StringBuilder)constructeur, qui semble obéir à ce "principe" où il est normal qu'un type ait une dépendance envers son constructeur. J'ai été sidéré quand j'ai vu cette surcharge du constructeur. Ce n'est pas comme un Stringa 42 paramètres constructeurs ...
Mathieu Guindon
3
@Luaan Odd. Je ne l'ai littéralement jamais vu utilisé et je ne savais pas qu'il existait; toString()est universel.
chrylis -en grève-
7

Je comprends à quel point cela peut être ennuyeux lorsque «l'appel à l'autorité» est utilisé dans un débat. Les arguments devraient être valables pour leur propre OMI et bien qu'il ne soit pas faux de signaler les conseils d'une personne aussi respectée, cela ne peut vraiment pas être considéré comme un argument complet en soi, car nous savons que le Soleil voyage autour de la terre parce qu'Aristote l'a dit. .

Cela dit, je pense que la prémisse de votre argument est la même. Nous ne devrions jamais coupler deux types de béton et appeler cela une meilleure pratique parce que ... Quelqu'un l'a dit?

Pour que l'argument selon lequel le couplage est problématique soit valide, il doit y avoir des problèmes spécifiques qu'il crée. Si cette approche n'est pas soumise à ces problèmes, vous ne pouvez pas logiquement affirmer que cette forme de couplage est problématique. Donc, si vous voulez faire valoir votre point de vue ici, je pense que vous devez montrer comment cette approche est soumise à ces problèmes et / ou comment le découplage du constructeur améliorera une conception.

Offhand, j'ai du mal à voir comment l'approche couplée va créer des problèmes. Les classes sont complètement couplées, bien sûr, mais le générateur existe uniquement pour créer des instances de la classe. Une autre façon de voir les choses serait comme une extension du constructeur.

JimmyJames
la source
Donc ... couplage plus élevé, cohésion plus faible => fin heureuse? hmm ... Je vois le point sur le constructeur "étendre le constructeur", mais pour évoquer un autre exemple, je ne vois pas ce qui Stringse passe avec une surcharge du constructeur prenant un StringBuilderparamètre (bien qu'il soit discutable de savoir si StringBuilderest un constructeur , mais c'est une autre histoire).
Mathieu Guindon
4
@ Mat'sMug Pour être clair, je n'ai pas vraiment un fort sentiment à ce sujet. Je n'ai pas tendance à utiliser le modèle de générateur car je ne crée pas vraiment de classes qui ont beaucoup d'entrée d'état. Je décomposerais généralement une telle classe pour des raisons auxquelles vous faites allusion ailleurs. Tout ce que je dis, c'est que si vous pouvez montrer comment cette approche conduit à un problème, peu importe si Dieu est descendu et a remis ce modèle de constructeur à Moïse sur une tablette de pierre. Vous gagnez'. D'un autre côté si vous ne pouvez pas ...
JimmyJames
Une utilisation légitime du modèle de générateur que j'ai dans ma propre base de code est pour se moquer de l'API VBIDE; en gros, vous fournissez le contenu de la chaîne d'un module de code, et le générateur vous donne une VBEmaquette, complète avec des fenêtres, un volet de code actif, un projet et un composant avec un module qui contient la chaîne que vous avez fournie. Sans cela, je ne sais pas comment je pourrais écrire 80% des tests dans Rubberduck , au moins sans me poignarder dans les yeux à chaque fois que je les regarde.
Mathieu Guindon
@ Mat'sMug Il peut être très utile pour démasquer des données en objets immuables. Ces types d'objets ont tendance à être des sacs de propriétés, c'est-à-dire pas vraiment OO. Tout cet espace de problème est un tel PITA que tout ce qui peut être fait sera utilisé, qu'il respecte ou non les bonnes pratiques.
JimmyJames
4

Pour savoir pourquoi un type serait couplé avec un générateur, il est nécessaire de comprendre pourquoi quelqu'un utiliserait un générateur en premier lieu. Plus précisément: Bloch recommande d'utiliser un générateur lorsque vous avez un grand nombre de paramètres de constructeur. Ce n'est pas la seule raison d'utiliser un constructeur, mais je pense que c'est la plus courante. En bref, le constructeur remplace ces paramètres de constructeur et souvent le constructeur est déclaré dans la même classe qu'il construit. Ainsi, la classe englobante a déjà connaissance du générateur et le transmettre comme argument constructeur ne change rien à cela. C'est pourquoi un type serait couplé avec son constructeur.

Pourquoi le fait d'avoir un grand nombre de paramètres implique-t-il que vous devez utiliser un générateur? Eh bien, avoir un grand nombre de paramètres n'est pas rare dans le monde réel, ni ne viole le principe de responsabilité unique. Cela craint également lorsque vous avez dix paramètres String et que vous essayez de vous rappeler lesquels sont lesquels et si c'est la cinquième ou la sixième String qui doit être nulle. Un constructeur facilite la gestion des paramètres et peut également faire d'autres choses intéressantes que vous devriez lire dans le livre pour en savoir plus.

Quand utilisez-vous un générateur qui n'est pas associé à son type? Généralement, lorsque les composants d'un objet ne sont pas immédiatement disponibles et que les objets qui ont ces pièces n'ont pas besoin de connaître le type que vous essayez de construire ou que vous ajoutez un générateur pour une classe sur laquelle vous n'avez pas de contrôle .

Old Fat Ned
la source
Bizarrement, les seules fois où j'ai eu besoin d' un générateur, c'était quand j'avais un type qui n'avait pas de forme définitive, comme ce MockVbeBuilder , qui construit une maquette de l'API d'un IDE; un test peut simplement avoir besoin d'un module de code simple, un autre test peut nécessiter deux formulaires et un module de classe - IMO, c'est à cela que sert le modèle de générateur de GoF. Cela dit, je pourrais commencer à l'utiliser pour une autre classe avec un constructeur fou ... mais le couplage ne me semble pas du tout correct.
Mathieu Guindon
1
@ Mug's Mug La classe que vous avez présentée semble plus représentative du modèle de conception Builder (de Design Patterns by Gamma, et al.), Pas nécessairement une classe de constructeur simple. Ils construisent tous les deux des choses, mais ce n'est pas la même chose. De plus, vous n'avez jamais "besoin" d'un constructeur, cela facilite simplement d'autres choses.
Old Fat Ned
1

le produit d'un modèle de création n'a jamais besoin de savoir quoi que ce soit qui le crée.

La Personclasse ne sait pas ce qui le crée. Il n'a qu'un constructeur avec un paramètre, alors quoi? Le nom du type du paramètre se termine par ... Builder qui ne force vraiment rien. C'est juste un nom après tout.

Le générateur est un objet de configuration glorifié 1 . Et un objet de configuration regroupe vraiment des propriétés qui appartiennent les unes aux autres. S'ils n'appartiennent les uns aux autres que pour être transmis au constructeur d'une classe, tant pis! Les deux originet destinationde l' StreetMapexemple sont de type Point. Serait-il possible de transmettre les coordonnées individuelles de chaque point à la carte? Bien sûr, mais les propriétés d'un Pointappartiennent ensemble, et vous pouvez avoir toutes sortes de méthodes qui aident à leur construction:

1 La différence est que le générateur n'est pas seulement un simple objet de données, mais permet ces appels de setter chaînés. Le Builderest principalement concerné par la façon de se construire.

Ajoutons maintenant un peu de modèle de commande au mélange, car si vous regardez de près, le générateur est en fait une commande:

  1. Il encapsule une action: appeler une méthode d'une autre classe
  2. Il contient les informations nécessaires pour effectuer cette action: les valeurs des paramètres de la méthode

La partie spéciale pour le constructeur est que:

  1. Cette méthode à appeler est en fait le constructeur d'une classe. Mieux vaut l'appeler un modèle de création maintenant.
  2. La valeur du paramètre est le constructeur lui-même

Ce qui est transmis au Personconstructeur peut le construire, mais ce n'est pas nécessairement le cas. Il peut s'agir d'un ancien objet de configuration simple ou d'un générateur.

nul
la source
0

Non, ils ont complètement tort et vous avez absolument raison. Ce modèle de constructeur est tout simplement stupide. Ce n'est pas l'affaire de la personne d'où proviennent les arguments du constructeur. Le constructeur est juste une commodité pour les utilisateurs et rien de plus.

DeadMG
la source
4
Merci ... Je suis sûr que cette réponse collecterait plus de votes si elle se développait un peu plus, elle ressemble à une réponse inachevée =)
Mathieu Guindon
Oui, je ferais +1, une approche alternative au constructeur qui offre les mêmes garanties et garanties sans le couplage
matt freake
3
Pour un contrepoint à cela, voir la réponse acceptée , qui mentionne les cas où PersonBuilderest en fait une partie essentielle de l' PersonAPI. Dans l'état actuel des choses, cette réponse ne fait pas valoir ses arguments.
Andres F.