J'ai récemment supprimé une de mes réponses java 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; }
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 Builder
classe, 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 .
la source
Réponses:
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.
la source
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 unString
a 42 paramètres constructeurs ...toString()
est universel.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.
la source
String
se passe avec une surcharge du constructeur prenant unStringBuilder
paramètre (bien qu'il soit discutable de savoir siStringBuilder
est un constructeur , mais c'est une autre histoire).VBE
maquette, 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.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 .
la source
La
Person
classe 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
origin
etdestination
de l'StreetMap
exemple sont de typePoint
. Serait-il possible de transmettre les coordonnées individuelles de chaque point à la carte? Bien sûr, mais les propriétés d'unPoint
appartiennent 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
Builder
est 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:
La partie spéciale pour le constructeur est que:
Ce qui est transmis au
Person
constructeur 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.la source
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.
la source
PersonBuilder
est en fait une partie essentielle de l'Person
API. Dans l'état actuel des choses, cette réponse ne fait pas valoir ses arguments.