Lors de la mise en œuvre du modèle de constructeur, je suis souvent confus quant au moment de laisser échouer la construction et je parviens même à prendre des positions différentes sur le sujet tous les deux ou trois jours.
D'abord quelques explications:
- Par échec précoce, je veux dire que la construction d’un objet doit échouer dès qu’un paramètre non valide est transmis
SomeObjectBuilder
. - Par échec tardif, je veux dire que la construction d'un objet ne peut échouer que sur l'
build()
appel qui appelle implicitement un constructeur de l'objet à construire.
Puis quelques arguments:
- En faveur de l'échec tardif: une classe de générateur ne devrait être qu'une classe qui contient simplement des valeurs. De plus, cela entraîne moins de duplication de code.
- En faveur de l'échec précoce: Une approche générale de la programmation logicielle consiste à détecter les problèmes le plus tôt possible. Par conséquent, l'endroit le plus logique à vérifier serait dans le constructeur, la classe et la méthode de construction de la classe de construction.
Quel est le consensus général à ce sujet?
java
design-patterns
skiwi
la source
la source
null
objet en cas de problèmebuild()
.Réponses:
Regardons les options, où nous pouvons placer le code de validation:
build()
méthode.build()
méthode lors de la création de l'entité.L'option 1 nous permet de détecter les problèmes plus tôt, mais il peut y avoir des cas compliqués où nous pouvons valider une entrée ayant uniquement le contexte complet, réalisant ainsi au moins une partie de la validation dans la
build()
méthode. Ainsi, le choix de l'option 1 entraînera une incohérence du code, une partie de la validation étant effectuée à un endroit et une autre à un autre endroit.L'option 2 n'est pas bien pire que l'option 1, car, en règle générale, les paramètres de configuration dans le générateur sont appelés juste avant les
build()
interfaces particulièrement fluides. Ainsi, il est toujours possible de détecter un problème suffisamment tôt dans la plupart des cas. Toutefois, si le générateur n'est pas le seul moyen de créer un objet, cela entraînera une duplication du code de validation, car vous devrez l'avoir partout où vous créez un objet. La solution la plus logique dans ce cas sera de placer la validation le plus près possible de l’objet créé, c’est-à-dire à l’intérieur de celui-ci. Et c'est l' option 3 .Du point de vue de SOLID, placer la validation dans le générateur constitue également une violation de SRP: la classe de générateur a déjà la responsabilité d'agréger les données pour construire un objet. La validation consiste à établir des contrats sur son propre état interne. Il est de la nouvelle responsabilité de vérifier l'état d'un autre objet.
Ainsi, de mon point de vue, non seulement il vaut mieux échouer tard du point de vue de la conception, mais il est également préférable d'échouer à l'intérieur de l'entité construite, plutôt que dans le constructeur lui-même.
UPD: ce commentaire m'a rappelé une possibilité supplémentaire, lorsque la validation à l'intérieur du constructeur (option 1 ou 2) est logique. Cela a du sens si le constructeur a ses propres contrats sur les objets qu'il crée. Par exemple, supposons que nous ayons un générateur qui construit une chaîne avec un contenu spécifique, par exemple une liste de plages de nombres
1-2,3-4,5-6
. Ce constructeur peut avoir une méthode commeaddRange(int min, int max)
. La chaîne résultante ne sait rien de ces chiffres, elle ne devrait pas non plus avoir à le savoir. Le constructeur lui-même définit le format de la chaîne et les contraintes sur les nombres. Ainsi, la méthodeaddRange(int,int)
doit valider les nombres saisis et lever une exception si max est inférieur à min.Cela dit, la règle générale sera de ne valider que les contrats définis par le constructeur lui-même.
la source
Étant donné que vous utilisez Java, tenez compte des indications détaillées et faisant autorité fournies par Joshua Bloch dans l’article Création et destruction d’objets Java (la police en gras dans la citation ci-dessous est la mienne):
Notez que, selon l' explication de l'éditeur concernant cet article, les "éléments" cités ci-dessus font référence aux règles présentées dans Effective Java, Second Edition .
L'article n'aborde pas en profondeur les raisons pour lesquelles cela est recommandé, mais si vous y réfléchissez, les raisons sont assez évidentes. Un conseil générique sur cette compréhension est fourni ici même dans l'article, dans l'explication du lien entre le concept de générateur et celui de constructeur - et les invariants de classe doivent être vérifiés dans le constructeur, et non dans tout autre code pouvant précéder / préparer son invocation.
Pour une compréhension plus concrète de la raison pour laquelle vérifier les invariants avant d'invoquer une construction serait une erreur, considérons un exemple populaire de CarBuilder . Les méthodes de générateur peuvent être appelées dans un ordre arbitraire. Par conséquent, il est impossible de savoir si un paramètre particulier est valide jusqu'à la construction.
Considérez que la voiture de sport ne peut avoir plus de 2 sièges. Comment savoir si
setSeats(4)
ça va ou non? C'est seulement à la construction que l'on peut savoir avec certitude s'il asetSportsCar()
été invoqué ou non, ce qui signifie qu'il faut lancerTooManySeatsException
ou non.la source
Les valeurs non valides, car elles ne sont pas tolérées, doivent être immédiatement signalées à mon avis. En d'autres termes, si vous n'acceptez que des nombres positifs et qu'un nombre négatif est transmis, il n'est pas nécessaire d'attendre jusqu'à l'
build()
appel. Je ne considérerais pas ces types de problèmes que vous "attendez" vraisemblablement, ce qui est une condition préalable à l'appel de la méthode. En d'autres termes, vous ne dépendriez probablement pas de l'échec de la définition de certains paramètres. Il est plus probable que vous supposiez que les paramètres sont corrects ou que vous effectuiez vous-même une vérification.Cependant, pour des problèmes plus complexes qui ne sont pas aussi facilement validés, il peut être préférable de vous faire connaître lorsque vous appelez
build()
. Un bon exemple de ceci pourrait être l'utilisation des informations de connexion que vous fournissez pour établir une connexion à une base de données. Dans ce cas, alors que vous pouviez techniquement vérifier de telles conditions, cela n’est plus intuitif et cela ne fait que compliquer votre code. À mon avis, ce sont aussi les types de problèmes qui peuvent survenir et que vous ne pouvez pas anticiper tant que vous n’avez pas essayé. C'est un peu la différence entre faire correspondre une chaîne avec une expression régulière pour voir si elle peut être analysée comme un int et essayer simplement de l'analyser, en gérant les éventuelles exceptions pouvant en résulter.En général, je n'aime pas lancer des exceptions lors de la définition des paramètres, car cela signifie qu'il faut attraper toute exception levée. Je préfère donc la validation dans
build()
. Donc, pour cette raison, je préfère utiliser RuntimeException car, encore une fois, les erreurs dans les paramètres passés ne devraient généralement pas se produire.Cependant, il s’agit plus d’une meilleure pratique que tout. J'espère que cela répond à votre question.
la source
Autant que je sache, la pratique générale (ne pas savoir s’il existe un consensus) est d’échouer dès que possible pour découvrir une erreur. Cela rend également plus difficile l'utilisation abusive non intentionnelle de votre API.
S'il s'agit d'un attribut trivial pouvant être vérifié en entrée, tel qu'une capacité ou une longueur qui doit être non négative, il est préférable d'échouer immédiatement. En retenant l'erreur, la distance entre l'erreur et le retour d'information augmente, ce qui rend plus difficile la recherche de la source du problème.
Si vous avez le malheur de vous trouver dans une situation où la validité d'un attribut dépend d'autres attributs, vous avez le choix entre deux options:
build()
est-il appelé?Comme dans la plupart des cas, il s’agit d’une décision prise dans un contexte. Si le contexte rend difficile ou compliqué d’échouer tôt, il est possible de choisir un délai pour reporter les contrôles à une date ultérieure, mais l’échec-rapide devrait être la valeur par défaut.
la source
unsigned
,@NonNull
etc.X
sur une valeur non valide compte tenu de la valeur actuelle deY
, mais avant d'appelerbuild()
misY
à une valeur qui rendraitX
valide.Shape
et le constructeur aWithLeft
etWithRight
propriétés, et on souhaite ajuster un entrepreneur pour construire un objet dans un endroit différent, exigeant queWithRight
soit d' abord appelé lors du déplacement d' un droit d'objet, etWithLeft
lorsque vous le déplacez à gauche, ajouterait une complexité inutile par rapport à permettreWithLeft
de définir le bord gauche à droite de l'ancien bord droit, à condition queWithRight
le bord droit soit corrigé avant l'build
appel.La règle de base est "échouer tôt".
La règle légèrement plus avancée est "échouer le plus tôt possible".
Si une propriété est intrinsèquement invalide ...
... alors vous le rejetez immédiatement.
D'autres cas peuvent nécessiter une vérification combinée des valeurs et être mieux placés dans la méthode build ():
la source