web-dev-qa-db-fra.com

Modèle de générateur: quand échouer?

Lors de la mise en œuvre du modèle de générateur, je me retrouve souvent confus avec le moment où laisser la construction échouer et je parviens même à prendre des positions différentes sur la question tous les quelques jours.

Première explication:

  • Avec échec précoce je veux dire que la construction d'un objet devrait échouer dès qu'un paramètre invalide est passé. Donc à l'intérieur du SomeObjectBuilder.
  • Avec échec tardif je veux dire que la construction d'un objet seulement peut échouer sur l'appel build() 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 doit pas être plus qu'une classe qui contient simplement des valeurs. De plus, cela conduit à moins de duplication de code.
  • Pour échouer tôt: Une approche générale de la programmation logicielle est que vous voulez détecter les problèmes le plus tôt possible et donc l'endroit le plus logique à vérifier serait dans la classe constructeur `` constructeur, '' setters 'et finalement dans la méthode de construction.

Quel est le consensus général à ce sujet?

47
skiwi

Examinons les options, où nous pouvons placer le code de validation:

  1. À l'intérieur des setters dans builder.
  2. À l'intérieur de la méthode build().
  3. A l'intérieur de l'entité construite: elle sera invoquée dans la méthode build() 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 lorsque nous pouvons valider l'entrée uniquement en ayant le contexte complet, donc en faisant au moins une partie de validation dans la méthode build(). Ainsi, le choix de l'option 1 entraînera un code incohérent avec une partie de la validation effectuée à un endroit et une autre partie effectuée à un autre endroit.

L'option 2 n'est pas significativement pire que l'option 1, car, généralement, les setters dans le constructeur sont invoqués juste avant la build(), en particulier , dans des interfaces fluides. Ainsi, il est toujours possible de détecter un problème suffisamment tôt dans la plupart des cas. Cependant, si le générateur n'est pas le seul moyen de créer un objet, cela entraînera la 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 ceci est l'option 3 .

Du point de vue SOLID, mettre la validation dans le générateur viole également SRP: la classe constructeur 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, c'est un 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 vaut également mieux é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é de plus, lorsque la validation à l'intérieur du générateur (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 avons un générateur qui construit une chaîne avec un contenu spécifique, disons une liste de plages de nombres 1-2,3-4,5-6. Ce générateur peut avoir une méthode comme addRange(int min, int max). La chaîne résultante ne sait rien de ces nombres, elle ne devrait pas non plus avoir à le savoir. Le générateur lui-même définit le format de la chaîne et les contraintes sur les nombres. Ainsi, la méthode addRange(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.

35
Ivan Gammel

Étant donné que vous utilisez Java, tenez compte des conseils détaillés et faisant autorité fournis par Joshua Bloch dans l'article Création et destruction Java Objets (la police en gras dans la citation ci-dessous est la mienne):

Comme un constructeur, un constructeur peut imposer des invariants à ses paramètres. La méthode de génération peut vérifier ces invariants. Il est essentiel qu'ils soient vérifiés après avoir copié les paramètres du générateur vers l'objet, et qu'ils soient vérifiés sur les champs d'objet plutôt que sur les champs de générateur (Point 39). Si des invariants sont violés, la méthode de construction doit lancer un IllegalStateException (Item 60). La méthode de détail de l'exception devrait indiquer quel invariant est violé (article 63).

Une autre façon d'imposer des invariants impliquant plusieurs paramètres consiste à demander aux méthodes de définition de prendre des groupes entiers de paramètres sur lesquels certains invariants doivent tenir. Si l'invariant n'est pas satisfait, la méthode setter lance un IllegalArgumentException. Cela présente l'avantage de détecter l'échec invariant dès que les paramètres non valides sont passés, au lieu d'attendre que la génération soit invoquée.

Remarque selon explication de l'éditeur sur cet article, les "éléments" dans la citation ci-dessus font référence aux règles présentées dans Effective Java, Second Edition .

L'article ne plonge pas profondément dans l'explication de pourquoi cela est recommandé, mais si vous y pensez, les raisons sont assez évidentes. Une astuce générique sur la compréhension de cela est fournie ici dans l'article, dans l'explication de la façon dont le concept de générateur est connecté à celui du constructeur - et les invariants de classe devraient être vérifiés dans le constructeur, pas dans tout autre code pouvant précéder/préparer son invocation.

Pour une compréhension plus concrète de la raison pour laquelle la vérification des invariants avant d'invoquer une construction serait incorrecte, considérons un exemple populaire de CarBuilder . Les méthodes du générateur peuvent être invoquées dans un ordre arbitraire et, par conséquent, on ne peut pas vraiment savoir si un paramètre particulier est valide jusqu'à la génération.

Considérez que la voiture de sport ne peut pas avoir plus de 2 sièges, comment savoir si setSeats(4) va bien ou pas? Ce n'est qu'à la compilation que l'on peut savoir avec certitude si setSportsCar() a été invoquée ou non, ce qui signifie s'il faut lancer TooManySeatsException ou non.

34
gnat

Les valeurs non valides qui ne sont pas valables car elles ne sont pas tolérées doivent être immédiatement communiqué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 que build() soit appelée. Je ne considérerais pas ces types de problèmes comme vous vous "attendriez" à se produire, car c'est une condition préalable à l'appel de la méthode pour commencer. En d'autres termes, vous ne feriez probablement pas dépendre de l'échec de la définition de certains paramètres. Il est plus probable que vous présumiez que les paramètres sont corrects ou que vous effectuiez vous-même une vérification.

Cependant, pour des problèmes plus compliqués qui ne sont pas aussi facilement validés, il est préférable de vous faire connaître lorsque vous appelez build(). Un bon exemple de cela pourrait être d'utiliser les informations de connexion que vous fournissez pour établir une connexion à une base de données. Dans ce cas, alors que techniquement pourriez vérifier de telles conditions, ce n'est plus intuitif et cela ne fait que compliquer votre code. Selon moi, ce sont également les types de problèmes qui pourraient réellement se produire et que vous ne pouvez pas vraiment anticiper avant de les essayer. C'est en quelque sorte la différence entre faire correspondre une chaîne avec une expression régulière pour voir si elle pourrait être analysée en tant qu'int et simplement essayer de l'analyser, en gérant toutes les exceptions potentielles qui peuvent survenir en conséquence.

Je n'aime généralement pas lever des exceptions lors de la définition des paramètres car cela signifie avoir à intercepter toute exception levée, j'ai donc tendance à privilégier la validation dans build(). Donc, pour cette raison, je préfère utiliser RuntimeException car, encore une fois, les erreurs de paramètres passées ne devraient généralement pas se produire.

Cependant, c'est plus une meilleure pratique qu'autre chose. J'espère que cela répond à votre question.

19
Neil

Pour autant que je sache, la pratique générale (je ne sais pas s'il y a consensus) est d'échouer dès que vous pouvez découvrir une erreur. Cela rend également plus difficile une mauvaise utilisation involontaire de votre API.

Si c'est un attribut trivial qui peut être vérifié en entrée, comme une capacité ou une longueur qui ne devrait pas être négative, alors il vaut mieux échouer immédiatement. Retenir l'erreur augmente la distance entre l'erreur et la rétroaction, ce qui rend plus difficile la recherche de la source du problème.

Si vous avez le malheur d'être dans une situation où la validité d'un attribut dépend des autres, alors vous avez deux choix:

  • Exiger que les deux (ou plusieurs) attributs soient fournis simultanément (c.-à-d. Invocation d'une méthode unique).
  • Testez la validité dès que vous savez qu'il n'y a plus de changements entrants: lorsque build() ou plus est appelé.

Comme pour la plupart des choses, c'est une décision prise dans un contexte. Si le contexte rend difficile ou compliqué l'échec précoce, un compromis peut être fait pour reporter les vérifications à une date ultérieure, mais l'échec rapide doit être la valeur par défaut.

11
JvR

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 ...

CarBuilder.numberOfWheels( -1 ). ...  

... alors vous le rejetez immédiatement.

D'autres cas peuvent nécessiter des valeurs à vérifier en combinaison et peuvent être mieux placés dans la méthode build ():

CarBuilder.numberOfWheels( 0 ).type( 'Hovercraft' ). ...  
0
Phill W.