J'ai réécrit une méthode très longue dans laquelle certaines données sont interrogées dans une base de données, basée sur des informations sur un compte particulier qui est interrogé en premier.
J'ai scindé les informations du compte dans une classe interne immuable AccountInfo
. Ceci est une version très simplifiée et modifiée de ce que son constructeur ressemble à:
private AccountInfo(String accountId, Optional<String> extraInfo) {
final List<Project> projects;
if (extraInfo.isPresent()) {
// (I could probably improve this and remove the .get() call)
projects = projectsDAO.fetchProjects(accountId, extraInfo.get());
if (projects.isEmpty()) {
this.projectIds = new ArrayList<>();
this.projectNames = new ArrayList<>();
this.unitIds = new ArrayList<>();
return;
}
} else {
projects = projectsDAO.fetchProjects(accountId);
}
projectIds = projects.stream().map(Project::getProjectId).collect(toList());
projectNames = projects.stream().map(Project::getProjectName).collect(toList());
final List<ProjUnit> units = unitsDAO.fetchUnitIds(projectIds);
this.projectIds = projectIds;
this.projectNames = projectNames;
this.unitsIds = units.stream().map(ProjUnit::getUnitId).limit(1000).collect(toList());
}
Comme vous pouvez le constater, le constructeur interroge la base de données plusieurs fois, via les DAOS (dans le code réel, il s'agit de quatre DAOS au lieu de deux). Autant que je sache, c'est une mauvaise pratique d'avoir des calculs effectifs dans un constructeur (corrigez-moi s'il est faux). Il y a quatre solutions potentielles qui me viennent à l'esprit:
Si je n'ai pas insisté pour tout faire dans le constructeur, je pourrais d'abord créer une instance vide, puis appeler des méthodes pour le peupler:
accountInfo = new AccountInfo(accountId, extraInfo);
accountInfo.fetchProjects()
accountInfo.fetchUnits()
Problèmes :
Les classes immuables sont généralement préférées, ce qui semble particulièrement pertinent, car il s'agit d'une classe qui stocke des informations qui ne devraient vraiment pas être modifiées une fois interrogées.
Il est impossible de garantir que l'appelant appelle les méthodes fetch
après avoir appelé le constructeur.
Au lieu d'interroger la base de données à l'intérieur du constructeur, l'appelant pourrait fournir les données au constructeur via des paramètres.
Problèmes :
Cela supprimerait une partie de l'avantage gagné de la séparation de la classe de la méthode d'origine en premier lieu, car le point était de prendre cette fonctionnalité et de l'organiser comme une unité.
La deuxième requête dépend des résultats de la première, donc presque tout de la logique devrait être effectuée avant Le constructeur est appelé, ou si j'aurais besoin de deux classes immuables séparées, une fois les informations qui obtiennent des informations sur les projets et une seconde qui obtiennent cet objet et les informations interrogées sur les unités. (Éventuellement encore plus dans le vrai code.)
Au lieu de l'intérieur du constructeur, la requête pourrait être faite dans une méthode d'usine statique, laissant uniquement l'affectation aux champs réels au constructeur.
Problèmes :
Étant donné que les méthodes d'usine statiques sont très similaires dans la responsabilité des constructeurs, je ne sais pas si c'est mieux pour eux d'avoir des effets secondaires.
Les objets DAO font partie de l'instance de classe extérieure, une méthode statique devra donc les recevoir en tant que paramètres pour pouvoir les utiliser, ce qui semble quelque peu inélégant. Bien que cela puisse peut-être être considéré comme un avantage, car cela rendrait leur utilisation explicite.
Ce serait un peu similaire à l'option mutable, mais semble strictement meilleur, car il est plus sûr. Cela permettrait de faire quelque chose comme
AccountInfo accountInfo = AccountInfo.Builder(accountId, extraInfo, projectsDAO, unitsDAO).build()
Et comme le constructeur lui-même aurait le mauvais type, cela garantirait que l'appelant doit appeler build()
, qui gérerait tous les effets secondaires.
Problèmes :
Semblable à l'option de méthode d'usine statique, il semble que cela semble être une mauvaise pratique d'avoir des effets secondaires dans une fonction build
(bien que cela puisse peut-être être apaisé simplement en renommant Builder
et build
et ] _ à quelque chose qui n'évoque pas les mêmes attentes)
Je pense que cela nécessite également de donner au constructeur les DAOS en tant que paramètres (qui, à nouveau, pourrait potentiellement considéré comme un avantage)
"Les cours devraient être immuables à moins d'une très bonne raison de les rendre mutables."
[.____] Joshua Bloch - Java efficace
"Les principes doivent être suivis parce que vous comprenez ce que vous sortez de les suivre. Pas parce que vous pouvez les citer d'un célèbre livre de gars."
[.____] n gars anonyme sur Internet
Immutable ou non, encapsulé ou non, a constructeur qui fait trop de travail ou non la limitation réelle de cette conception est que le constructeur a une opinion sur la manière d'obtenir ces données. Cela signifie que si vous avez exactement ces données qui posaient quelque part ailleurs, cette classe est maintenant inutile pour vous.
S'il y a du travail qui doit être fait pour obtenir ces données, vous n'avez pas à emballer le code qui fonctionne dans votre constructeur. Vous pouvez écrire un constructeur qui prend les données. Ensuite, vous pouvez simplement transmettre les données dans. L'injection de dépendance est le terme de fantaisie pour cela, mais nous avons l'habitude de l'appeler de la référence de référence. Ne laissez pas ce nom vous faire travailler.
L'avantage est que la classe peut maintenant être un consommateur de ces données et non un cueilleur des données. Donc, si ces données deviennent ailleurs, prêtes à partir, cette classe est prête pour cela. L'inconvénient est maintenant que vous devez penser à un endroit pour mettre le code de collecte de données. Ce principe est appelé tilisation séparée de la construction .
Maintenant pragmatiquement parler, vous ne savez peut-être que de savoir où mettre ces données d'obtenir un comportement. Beaucoup ont simplement rejeté cette question lorsque vous étant paresseux, mais c'est un vrai problème. Les codesbases imposent leur style organisationnel sur vous et vous devez trouver des endroits pour mettre les choses où les gens peuvent les trouver plus tard. Donc, peut-être qu'une classe d'usine, un constructeur, un DSL ou tout autre modèle créé n'est tout simplement pas pratique pour vous. C'est bien, mais cela ne signifie toujours pas que vous devez forcer cette classe à toujours faire ce travail.
La voie la plus lazère (paresseuse peut être une bonne chose) que je sais éviter de forcer ce travail à arriver à chaque fois est simplement faire un autre constructeur et l'appelez de celui qui sait faire ce travail de collecte de données. Maintenant, vous pouvez contourner la collecte de données si vous avez une autre façon de le rassembler. La collecte de données est maintenant simplement un comportement par défaut ultrable.
Le coût de cela est que la classe doit toujours savoir comment recueillir ce qu'il a besoin et donc une dépendance codée dur sur tout ce qu'elle doit le faire. Cela aurait pu être séparé pour que la classe puisse être réutilisée, même si ce genre de choses s'en va. Ce que vous vous souciez seulement de violer [~ # ~ # ~] OCP [~ # ~] et réécrivez directement la classe lorsque vous devez faire cela est un problème. Cette chose est publiée dans une bibliothèque largement utilisée ou est-ce que votre groupe de développement n'est-il que les seuls à la voir directement? Si ce n'est pas le seul coût réel de violation d'OCP, c'est après que vous le rééchappiez, vous devez tester la classe de plus en plus parce que vous perdez votre code temporel intact et de combat à la réécriture.
Oh, et il s'avère le faire de cette façon, vous permet de toujours être immuables. Soigné, si vous vous souciez de cela.
Ce sont le genre de choses que vous devriez penser avant d'appliquer ces principes. Savoir toujours pourquoi tu te soucies.
En outre, si vous voulez vraiment écrire du code qui ressemble à ceci,
accountInfo = new AccountInfo(accountId, extraInfo); accountInfo.fetchProjects() accountInfo.fetchUnits()
tout en imposant une commande et permettant à CompteInfo d'être immuable, vous pouvez créer une langue spécifique du domaine interne ( IDSL ) qui exploite les idées de Joshua Bloch's Builder . Voir la norme de Java StringBuilder pour un exemple. Le constructeur de Josh vous donne un résultat immuable mais n'impose aucun ordre sur les méthodes appelées. Le DSL peut vous donner un résultat immuable tout en imposant une commande de construction. L'utilisation du code ressemblerait à ceci:
AccountInfo accountInfo = new AccountInfo.Builder(accountId, extraInfo)
.fetchProjects()
.fetchUnits()
.build()
;
Vous ne pouvez pas appeler ces extratures en dehors de l'ordre car dans cette DSL, ils sont suspendus de différents types et ils renvoient différents types. Une fois AccountInfo
existe, il est parfaitement immuable.
C'est l'autre extrême. La mise en place d'une DSL est beaucoup de travail. Beaucoup plus que d'être clés sur un autre constructeur. Tellement de sorte que je ne le recommanderais pas à moins que AccountInfo
est quelque chose que vous voyez être construit sur et sur votre base de code. Quand c'est le cas, bien que ce travail puisse économiser beaucoup de douleur.
Si vous regardez cela et pensez, "Ceci est lié à la mise en œuvre de la collecte de données autant que le constructeur d'origine" , bien tu es droit. Si vous le souhaitez, vous pouvez séparer les connaissances de la commande de bâtiment à partir des connaissances de mise en œuvre de la collecte de données.
public AccountInfo accountInfoFactoryMethod(AccountInfoBuilder accountInfoBuilderImp) {
accountInfoBuilderImp
.builder(accountId, extraInfo)
.fetchProjects()
.fetchUnits()
.build()
;
}
Vous dites que vous ne voulez pas déplacer le projectsDAO.fetchProjects
sorti de ce constructeur parce que "... presque toute la logique devrait être faite avant que le constructeur soit appelé ..." Mais il me semble que la logique en dehors de cette méthode est exactement ce qui déterminer la méthode de la facture fetchprojections appel.
Ce que je veux dire, c'est que vous avez la logique à l'extérieur de cette classe qui détermine si tout extraInfo
est transmis, et comme c'est la seule logique qui détermine la méthode de fetchprojects à appeler, il devrait être fait plus directement responsable.
Donc, j'irais avec votre option 2 car les problèmes que vous censés ne sont pas des problèmes introduits par l'option, mais ils existent plutôt quelle que soit l'option choisie.