J'ai vu de nombreuses implémentations du modèle Builder (principalement en Java). Tous ont une classe d'entité (disons une Person
classe) et une classe de constructeur PersonBuilder
. Le générateur "empile" une variété de champs et renvoie un new Person
avec les arguments passés. Pourquoi avons-nous explicitement besoin d'une classe de générateur, au lieu de mettre toutes les méthodes de générateur dans la Person
classe elle-même?
Par exemple:
class Person {
private String name;
private Integer age;
public Person() {
}
Person withName(String name) {
this.name = name;
return this;
}
Person withAge(int age) {
this.age = age;
return this;
}
}
Je peux simplement dire Person john = new Person().withName("John");
Pourquoi le besoin d'une PersonBuilder
classe?
Le seul avantage que je vois, c'est que nous pouvons déclarer les Person
champs comme final
, assurant ainsi l'immuabilité.
java
design-patterns
builder-pattern
Boyan Kushlev
la source
la source
chainable setters
: DwithName
renvoyé une copie de la personne avec seulement le champ de nom changé. En d'autres termes, celaPerson john = new Person().withName("John");
pourrait fonctionner même s'ilPerson
est immuable (et c'est un modèle courant en programmation fonctionnelle).void
méthodes. Ainsi, par exemple, siPerson
a une méthode qui imprime leur nom, vous pouvez toujours la chaîner avec une interface Fluentperson.setName("Alice").sayName().setName("Bob").sayName()
. Soit dit en passant, j'annote ceux de JavaDoc avec exactement votre suggestion@return Fluent interface
- c'est générique et assez clair quand il s'applique à n'importe quelle méthode qui le faitreturn this
à la fin de son exécution et c'est assez clair. Ainsi, un constructeur fera également une interface fluide.Réponses:
C'est pour que vous puissiez être immuable ET simuler des paramètres nommés en même temps.
Cela garde vos mitaines hors de la personne jusqu'à ce que son état soit défini et, une fois défini, ne vous permettra pas de le changer, mais chaque champ est clairement étiqueté. Vous ne pouvez pas faire cela avec une seule classe en Java.
Il semble que vous parliez de Josh Blochs Builder Pattern . Cela ne doit pas être confondu avec le modèle Gang of Four Builder . Ce sont des bêtes différentes. Ils résolvent tous deux des problèmes de construction, mais de manière assez différente.
Bien sûr, vous pouvez construire votre objet sans utiliser une autre classe. Mais alors vous devez choisir. Vous perdez soit la possibilité de simuler des paramètres nommés dans des langages qui n'en ont pas (comme Java), soit vous perdez la possibilité de rester immuable pendant toute la durée de vie des objets.
Exemple immuable, n'a pas de nom pour les paramètres
Ici, vous construisez tout avec un seul constructeur simple. Cela vous permettra de rester immuable mais vous perdrez la simulation des paramètres nommés. Cela devient difficile à lire avec de nombreux paramètres. Les ordinateurs s'en fichent mais c'est dur pour les humains.
Exemple de paramètre nommé simulé avec des setters traditionnels. Pas immuable.
Ici, vous construisez tout avec des setters et simulez des paramètres nommés mais vous n'êtes plus immuable. Chaque utilisation d'un setter change l'état de l'objet.
Donc, ce que vous obtenez en ajoutant la classe, c'est que vous pouvez faire les deux.
La validation peut être effectuée dans le
build()
cas où une erreur d'exécution pour un champ d'âge manquant vous suffit. Vous pouvez mettre à niveau et appliquer ce quiage()
est appelé avec une erreur de compilation. Mais pas avec le modèle de générateur Josh Bloch.Pour cela, vous avez besoin d'un langage spécifique au domaine interne (iDSL).
Cela vous permet d'exiger qu'ils appellent
age()
etname()
avant d'appelerbuild()
. Mais vous ne pouvez pas le faire simplement en revenant àthis
chaque fois. Chaque chose qui retourne renvoie une chose différente qui vous oblige à appeler la chose suivante.L'utilisation pourrait ressembler à ceci:
Mais ça:
provoque une erreur de compilation car
age()
n'est valide que pour appeler le type retourné parname()
.Ces iDSL sont extrêmement puissants ( JOOQ ou Java8 Streams par exemple) et sont très agréables à utiliser, surtout si vous utilisez un IDE avec complétion de code, mais ils sont un peu de travail à configurer. Je recommanderais de les enregistrer pour des choses qui auront un peu de code source écrit contre eux.
la source
AnonymousPersonBuilder
qui a son propre ensemble de règles.Pourquoi utiliser / fournir une classe de générateur:
la source
PersonBuilder
n'a pas de getters, et la seule façon d'inspecter les valeurs actuelles est d'appeler.Build()
pour retourner aPerson
. De cette façon .Build peut valider un objet correctement construit, n'est-ce pas? C'est-à-dire le mécanisme destiné à empêcher l'utilisation d'un objet "en construction"?Build
opération pour éviter les objets défectueux et / ou sous-construits.getAge
au générateur peut revenirnull
lorsque cette propriété n'a pas encore été spécifiée. En revanche, laPerson
classe peut imposer l'invariant que l'âge ne peut jamais êtrenull
, ce qui est impossible lorsque le constructeur et l'objet réel sont mélangés, comme avec l'new Person() .withName("John")
exemple de l'OP , qui crée heureusement unPerson
sans âge. Cela s'applique même aux classes mutables, car les setters peuvent appliquer des invariants, mais ne peuvent pas appliquer les valeurs initiales.L'une des raisons serait de garantir que toutes les données transmises suivent les règles métier.
Votre exemple ne prend pas cela en considération, mais disons que quelqu'un a passé une chaîne vide ou une chaîne composée de caractères spéciaux. Vous voudriez faire une sorte de logique basée sur la vérification que leur nom est en fait un nom valide (ce qui est en fait une tâche très difficile).
Vous pouvez mettre tout cela dans votre classe Person, surtout si la logique est très petite (par exemple, en vous assurant simplement qu'un âge n'est pas négatif) mais que la logique grandit, il est logique de le séparer.
la source
john
Un angle un peu différent à ce sujet de ce que je vois dans d'autres réponses.
L'
withFoo
approche ici est problématique car ils se comportent comme des setters mais sont définis d'une manière qui donne l'impression que la classe prend en charge l'immuabilité. Dans les classes Java, si une méthode modifie une propriété, il est habituel de démarrer la méthode avec 'set'. Je ne l'ai jamais aimé en tant que norme, mais si vous faites autre chose, cela va surprendre les gens et ce n'est pas bon. Il existe une autre façon de prendre en charge l'immuabilité avec l'API de base que vous avez ici. Par exemple:Il n'offre pas beaucoup de moyens pour empêcher les objets partiellement construits de manière incorrecte, mais empêche les modifications des objets existants. C'est probablement idiot pour ce genre de chose (tout comme le JB Builder). Oui, vous créerez plus d'objets, mais ce n'est pas aussi cher que vous le pensez.
Vous verrez principalement ce type d'approche utilisée avec des structures de données simultanées telles que CopyOnWriteArrayList . Et cela indique pourquoi l'immuabilité est importante. Si vous souhaitez rendre votre code threadsafe, l'immuabilité doit presque toujours être considérée. En Java, chaque thread est autorisé à conserver un cache local à état variable. Pour qu'un thread puisse voir les modifications apportées dans d'autres threads, un bloc synchronisé ou une autre fonctionnalité de concurrence doit être utilisé. N'importe lequel de ces éléments ajoutera une surcharge au code. Mais si vos variables sont finales, il n'y a rien à faire. La valeur sera toujours celle à laquelle elle a été initialisée et par conséquent, tous les threads voient la même chose, quoi qu'il arrive.
la source
Une autre raison qui n'a pas été explicitement mentionnée ici, est que la
build()
méthode peut vérifier que tous les champs sont des champs contenant des valeurs valides (soit définies directement, soit dérivées d'autres valeurs d'autres champs), ce qui est probablement le mode d'échec le plus probable. cela se produirait autrement.Un autre avantage est que votre
Person
objet finirait par avoir une durée de vie plus simple et un simple ensemble d'invariants. Vous savez qu'une fois que vous en avez unPerson p
, vous avez unp.name
et un validep.age
. Aucune de vos méthodes ne doit être conçue pour gérer des situations telles que "et si l'âge est défini mais pas le nom, ou si le nom est défini mais pas l'âge?" Cela réduit la complexité globale de la classe.la source
URI
, aFile
ou aFileInputStream
, et utilise tout ce qui a été fourni pour obtenir unFileInputStream
qui va finalement dans l'appel du constructeur en tant qu'arg.Un générateur peut également être défini pour renvoyer une interface ou une classe abstraite. Vous pouvez utiliser le générateur pour définir l'objet et le générateur peut déterminer la sous-classe concrète à renvoyer en fonction des propriétés définies ou de leur définition, par exemple.
la source
Le modèle de générateur est utilisé pour construire / créer l'objet étape par étape en définissant les propriétés et lorsque tous les champs obligatoires sont définis, puis renvoyez l'objet final à l'aide de la méthode de construction. L'objet nouvellement créé est immuable. Le point principal à noter ici est que l'objet n'est retourné que lorsque la méthode de construction finale est invoquée. Cela garantit que toutes les propriétés sont définies sur l'objet et que l'objet n'est donc pas dans un état incohérent lorsqu'il est renvoyé par la classe de générateur.
Si nous n'utilisons pas la classe de générateur et que nous plaçons directement toutes les méthodes de classe de générateur dans la classe Personne elle-même, nous devons d'abord créer l'objet, puis invoquer les méthodes de définition sur l'objet créé, ce qui conduira à l'état incohérent de l'objet entre la création de l'objet et la définition des propriétés.
Ainsi, en utilisant la classe builder (c'est-à-dire une entité externe autre que la classe Person elle-même), nous nous assurons que l'objet ne sera jamais dans l'état incohérent.
la source
Réutiliser l'objet générateur
Comme d'autres l'ont mentionné, l'immuabilité et la vérification de la logique métier de tous les champs pour valider l'objet sont les principales raisons d'un objet constructeur distinct.
Cependant, la réutilisation est un autre avantage. Si je veux instancier de nombreux objets très similaires, je peux apporter de petites modifications à l'objet générateur et continuer à instancier. Pas besoin de recréer l'objet générateur. Cette réutilisation permet au générateur de servir de modèle pour créer de nombreux objets immuables. C'est un petit avantage, mais il pourrait être utile.
la source
En fait, vous pouvez avoir les méthodes de génération sur votre classe elle-même et avoir toujours l'immuabilité. Cela signifie simplement que les méthodes du générateur renverront de nouveaux objets, au lieu de modifier ceux existants.
Cela ne fonctionne que s'il existe un moyen d'obtenir un objet initial (valide / utile) (par exemple à partir d'un constructeur qui définit tous les champs requis, ou une méthode d'usine qui définit les valeurs par défaut), et les méthodes de générateur supplémentaires retournent ensuite les objets modifiés en fonction sur l'existant. Ces méthodes de génération doivent s'assurer que vous n'obtenez pas d'objets invalides / incohérents en cours de route.
Bien sûr, cela signifie que vous aurez beaucoup de nouveaux objets, et vous ne devriez pas le faire si vos objets sont coûteux à créer.
Je l'ai utilisé dans le code de test pour créer des comparateurs Hamcrest pour l'un de mes objets métier. Je ne me souviens pas du code exact, mais il ressemblait à quelque chose comme ça (simplifié):
Je l'utiliserais alors comme ceci dans mes tests unitaires (avec des importations statiques appropriées):
la source
name
mais nonage
), le concept ne fonctionne pas. Eh bien, vous pourriez en fait retourner quelque chose commePartiallyBuiltPerson
s'il n'était pas valide, mais cela semble être un hack pour masquer le générateur.