Du code initialement simple s'empile: en ajoutant ou en modifiant des fonctions, en corrigeant des bugs, etc., le code se complique progressivement, entraînant une augmentation du coût de correction et une diminution de la qualité. C'est tout à fait naturel pour un produit d'une certaine taille, et vous devez le concevoir consciemment pour éviter que cela ne se produise.
Ici, nous présenterons les méthodes et les modèles dont nous avons généralement connaissance pour ne pas compliquer le code. (Grande quantité de composants DDD et Clean Architecture) De plus, le code qui apparaît dans cet article est Java, mais je pense que vous pouvez le comprendre dans une certaine mesure sans connaître Java.
Aussi, c'est long car je l'ai écrit dans une large gamme sans être cohérent ... Puisqu'il s'agit d'un lien d'ancrage pour chaque élément ci-dessous, veuillez uniquement l'élément qui vous intéresse.
Le getter
ici fait référence à une variable d'instance qui peut être référencée de l'extérieur. La raison pour laquelle il est limité "là où la logique métier est impliquée" est qu'il peut ne pas être possible de le traiter sans condition en raison des restrictions du cadre.
Classe utilisant getter
public class Employee {
private String name; //OK car il ne peut pas être référencé de l'extérieur
private LocalDate contractDate;
public int salary; //Les variables d'instance publiques sont interdites car elles peuvent être référencées de l'extérieur
...
//Interdit car les variables d'instance peuvent être référencées de l'extérieur avec getter
public LocalDate getContractDate() {
return this.contractDate;
}
}
Le processus «getContractDate» dans cet exemple, «obtenir la date du contrat d'un employé», n'a aucune implication commerciale. Vous ne savez pas ce que vous voulez faire en obtenant la date du contrat d'un employé. Qu'est-ce qui a été réalisé ici pour identifier les employés en utilisant la date du contrat? S'agit-il d'un changement des informations sur les employés en raison de la date du contrat? ?? Et ainsi de suite, cela seul est devenu insignifiant. Ceci est synonyme de l'utilisation des données acquises jetées du côté de l'utilisateur, et est un report de la conception.
Maintenant, regardons le côté qui utilise la classe dans laquelle le "report de conception" se produit.
Le côté qui utilise la classe dans laquelle le "report de conception" se produit
int salary;
if (employee.getContractDate().isBefore(LocalDate.now())) {
//Soyez payé pour les employés contractuels(Cela n'inclut pas le jour, mais il y a des détails...)
salary = employee.salary;
}
Comme mentionné ci-dessus, la logique métier fuit vers la classe qui souhaite utiliser «employé». Si une classe qui veut utiliser ʻemployee` dans le même but sort à l'avenir, un code de copie similaire sera produit en masse partout. Pour éviter cela, écrivez la logique métier directement dans la classe contenant les données comme indiqué ci-dessous.
Classe qui interdit le getter
public class Employee {
private LocalDate contractDate;
private int salary;
...
public int calculateSalary() {
if (this.contractDate.isBefore(LocalDate.now())) {
return this.salary;
}
return 0; //Cette façon de rentrer n'est pas bonne, mais à titre d'exemple...
}
}
Classe d'utilisateurs
int salary = employee.calculateSalary();
En arrêtant getter
et en écrivant la logique métier dans la classe qui contient les données, vous pouvez empêcher la production en masse de code de copie et vous pourrez connaître les règles métier simplement en regardant la classe correspondante.
Le setter
ici fait également référence à une variable d'instance dont la valeur peut être modifiée de l'extérieur.
Classe avec setter
public class Employee {
public LocalDate contractDate; //Les variables d'instance publique peuvent être affectées de l'extérieur, donc interdit
private int salary;
...
//Interdit car les variables d'instance peuvent être affectées en externe avec setter
public void setSalary(int salary) {
this.salary = salary;
}
}
Encore une fois, la logique est «setSalary», qui n'a aucune signification commerciale de «changer le salaire d'un employé». Par exemple, il doit s'agir d'une méthode qui exprime le flux et les règles métier, comme "Je l'ai ajouté parce que j'avais de bonnes performances" et "Je l'ai corrigé parce que j'ai fait une erreur dans l'entrée".
De plus, le fait que la valeur puisse être modifiée par setter
signifie que l'état va changer, donc ce n'est pas sûr. En particulier, à mesure que le traitement devient plus profond comme indiqué ci-dessous, l'état qui peut être facilement reconnu par les humains est dépassé et la lisibilité se détériore.
methodA(Employee employee)
└ methodB(Employee employee)
└ methodC(Employee employee)
└ methodD(Employee employee) <-C'est impossible si le statut de salarié est réécrit ici!Je ne peux pas me rattraper!
└ methodE(Employee employee)
Un constructeur complet signifie "fixer les valeurs de toutes les variables d'instance avec le constructeur". Surtout dans les classes qui traitent des règles métier, nous garantissons que le constructeur crée les états suivants.
--Les valeurs de toutes les variables d'instance sont déterminées
À titre d'exemple simple, définissez une classe qui exprime la règle métier selon laquelle «les employés ont toujours un contrat (il y a une date de contrat), et ils peuvent contracter jusqu'à 3 mois à l'avance».
public class Employee {
private LocalDate contractDate;
public Employee(LocalDate contractDate) {
if (contractDate == null) {
throw new IllegalArgumentException("La date du contrat est requise");
}
LocalDate currentDate = LocalDate.now();
if (contractDate.isAfter(currentDate.plusMonths(3))) {
throw new IllegalArgumentException("Les dates de contrat de plus de 3 mois à l'avance ne sont pas valides");
}
this.contractDate = contractDate;
}
}
En concevant le constructeur de manière à ce que la date du contrat de l'employé soit toujours requise et que la date dépassant 3 mois à l'avance soit ainsi limitée, les restrictions sur les règles commerciales sont observées de force lors de l'utilisation de cette instance. Il est également garanti qu'il s'agit d'une instance sécurisée. De plus, il devient une instance sûre et prévisible quelle que soit la valeur ajoutée, et l'utilisateur peut l'utiliser sans se soucier des choses inutiles.
Après avoir créé une instance une fois, des opérations telles que la modification de l'état ou l'assemblage progressif de certains champs réduisent la sécurité et la lisibilité. La génération d'instance doit toujours être complétée par des opérations atomiques.
Présentation des méthodes de fabrique statiques en relation avec les constructeurs. Vous souhaiterez peut-être créer plusieurs constructeurs surchargés en termes de règles métier. Comme décrit dans le commentaire de l'exemple ci-dessous, cela ressemble à "ce constructeur pour XX" et "ce constructeur pour △△".
public class SearchDateTime {
private LocalDate date;
private LocalTime time;
//Je veux que vous utilisiez ce constructeur sauf le jour...
public SearchDateTime(LocalDate date, LocalTime time) {
this.date = date;
this.time = time;
}
//Je veux que vous n'utilisiez le constructeur que le jour...
public SearchDateTime() {
this.date = LocalDate.now();
this.time = LocalTime.now();
//Peu importe, mais enchaînons le constructeur!
// this(LocalDate.now(), LocalTime.now());
}
}
S'il y a plusieurs constructeurs pour ce qui précède, cela est écrit dans le commentaire, mais il est difficile pour l'utilisateur de comprendre lequel utiliser uniquement par la différence des arguments.
La solution à cela est le titre "Méthode de fabrique statique au lieu de constructeur". Préparez une méthode d'usine statique nommée pour chaque utilisation comme indiqué ci-dessous.
public class SearchDateTime {
private LocalDate date;
private LocalTime time;
public static SearchDateTime of(LocalDate date, LocalTime time) {
return new SearchDateTime(date, time);
}
public static SearchDateTime ofToday() {
return new SearchDateTime(LocalDate.now(), LocalTime.now());
}
//Le constructeur est rendu privé et non exposé à l'extérieur
private SearchDateTime(LocalDate date, LocalTime time) {
this.date = date;
this.time = time;
}
}
Le constructeur d'origine réduit la visibilité à «private» et l'empêche d'être utilisée directement de l'extérieur. L'avantage des méthodes de fabrique statiques est qu'elles peuvent être ** nommées **. En rendant le nom explicite, vous pouvez transmettre votre intention à l'utilisateur sans le rendre ambigu. De plus, comme autre exemple de cas d'utilisation, il est efficace de limiter les objets devant être gérés par le rôle d'administrateur et le rôle normal, et de clarifier les opérations qui ne doivent pas être confondues avec la dénomination et le type d'argument.
Vous pouvez augmenter la sécurité en vous assurant que la valeur n'est pas modifiée une fois qu'elle est fixée.
En définissant le modificateur final
sur la variable d'instance, la valeur ne changera pas après l'initialisation dans le constructeur.
public class Employee {
//Ajouter le modificateur final à toutes les variables d'instance
private final String name;
private final LocalDate contractDate;
private final int salary;
public Employee(String name, LocalDate contractDate, int salary) {
this.name = name;
this.contractDate = contractDate;
this.salary = salary;
}
public void addSalary(int salary) {
this.salary += salary //Erreur de compilation!!
}
}
Le but principal de quitter setter est d'en faire un constructeur complet et un objet immuable afin que vous n'ayez pas à vous soucier des changements d'état. Étant donné que la quantité d'informations dont une personne peut se souvenir temporairement est faible, il est important de réduire autant que possible la quantité de préoccupation.
Vous avez maintenant un objet immuable avec une valeur immuable, mais que se passe-t-il si vous souhaitez modifier la valeur, c'est de créer une nouvelle instance et de la renvoyer. Cela ne change pas sa propre valeur.
public class Employee {
private final String name;
private final LocalDate contractDate;
private final int salary;
...
//Retraiter et modifier la date du contrat et le salaire
public Employee contractRenewal(int salary) {
return new Employee(this.name, LocalDate.now(), salary);
}
}
//Côté utilisateur
Employee employee = new Employee(...);
Employee extendedEmployee = employee.contractRenewal(200000); //Traitez comme une instance distincte.
«La loi de Demeter» et «Ne pas demander» sont des principes de conception liés à la dissimulation d'informations. Tout d'abord, regardons les images d'exemples où ce principe de conception n'est pas suivi et les cas où il est suivi.
--Le service demande
--Je commande
Vous pouvez voir que si vous suivez ce principe de conception, vous obtiendrez une belle forme en V.
Ensuite, regardons le code. À titre d'exemple simple, il montre la logique de «trouver le prix de vente d'un produit avec des frais de coupon».
Ne demandez pas la logique d'utilisateur de la loi de Demeter qui ne suit pas la commande
Product product = new Product(something);
int sellingPrice;
if (product.price.couponExpiration.isAfore(LocalDate.now())) {
//Renvoie le prix du coupon de l'article si le coupon est valide(Cela n'inclut pas non plus le jour, mais il y a des détails...)
sellingPrice = product.price.couponValue;
} else {
//Renvoyer le prix du produit si la date d'expiration du coupon n'est pas valide
sellingPrice = product.price.value;
}
Dans ce qui précède, une instruction if est générée dans product.price.couponExpiration
pour déterminer la date d'expiration du coupon. Nous accédons également au «prix» appartenant au «produit».
Le code amélioré en appliquant «la loi de Demeter» et «Ne pas demander» à ce code est le suivant.
La loi de Demeter et ne pas demander, logique utilisateur
Product product = new Product(something);
int sellingPrice = product.sellingPrice();
La "logique utilisateur ci-dessus dans laquelle la loi de Demeter et la commande de ne pas demander" est complétée par une ligne d'instructions. L'utilisateur ne dispose que de ** connaissances minimales ** et n'a qu'à appeler la commande "trouver le prix de vente".
La classe Product
qui incorpore cette" loi de Demeter et ne pas demander "est la suivante.
public class Product {
private Price price;
...
int sellingPrice() {
return price.sellingPrice();
}
}
class Price {
private int value;
private int couponValue;
private LocalDate couponExpiration;
...
int sellingPrice() {
if expirationDate.isAfore(LocalDate.now()) {
return couponValue;
}
return value;
}
}
À l'origine, la classe «Price» est en charge du traitement du jugement tel que le branchement qui était du côté de l'utilisateur, et il est composé dans la classe «Product». La logique métier telle que le calcul et le jugement est ** gérée par la classe qui contient les données **.
De plus, la classe Price
est définie comme package-private, et la méthode sellingPrice
est également package-private. En d'autres termes, cette classe et cette méthode ne peuvent pas être vues du côté utilisateur (en supposant un autre package), et l'exécution directe n'est pas autorisée. En faisant cela, vous pouvez réduire de force ** l'exposition aux connaissances **, réduire la dépendance et les coupler vaguement.
Selon la loi de Déméter, c'est l'image.
La séparation des intérêts est souvent évoquée dans MVC et l'architecture en couches, mais c'est un principe dont je veux être conscient quelle que soit la granularité. Par exemple, supposons que vous ayez une classe qui traite des «employés». Dans cette catégorie «employé», nous ne nous soucions pas de «comment notifier» si une erreur commerciale se produit. S'il s'agit d'une API, le message doit être emballé dans un format tel que JSON, et s'il s'agit de quelque chose qui affiche un écran comme un site Web, il doit être modifié et généré afin que l'utilisateur puisse le reconnaître facilement. Dans cette classe «employé», «l'occurrence d'une erreur commerciale» et «le contenu de l'erreur commerciale (message d'erreur)» sont les préoccupations qui doivent être connues. Comment le sortir et comment le modifier est une préoccupation d'une autre classe (par exemple, couche de présentation). Si le traitement d'écran côté utilisateur est écrit dans la classe "employé" sans avoir connaissance de la séparation des intérêts ci-dessus, s'il y a un changement dans la méthode de sortie d'écran, la classe "employé" ne devrait pas être pertinente. Sera également corrigé, élargissant la gamme d'influence. Il viole également l'un des principes SOLID, le ** principe de responsabilité unique **. https://qiita.com/UWControl/items/98671f53120ae47ff93a
En séparant les intérêts selon les rôles, en décidant et en délimitant les limites de la responsabilité, il est possible de concevoir des applications faiblement couplées et sécurisées avec moins de dépendance vis-à-vis de l'extérieur. Est possible. Cela conduit également à une réduction de ** l'exposition aux connaissances **.
TL;DR Parce que c'est un peu long.
Un "composant" est un module, ou une unité de pot ou de gemme. Cependant, il n'est pas limité aux composants, mais doit être appliqué à n'importe quel objet. Par conséquent, l'explication ici est basée sur la classe.
Le graphe de dépendance des composants ne doit pas avoir de dépendances circulaires
Il y a un principe. Il doit être conçu de manière à ce qu'il n'y ait aucune dépendance circulaire dans aucune structure, pas seulement des «composants». Assurez-vous que la dépendance est vers l'intérieur et ne dépend que d'une seule direction sans circulation. Il est important de ne pas connaître l'extérieur de l'intérieur et de ne pas amener les connaissances et les règles de l'extérieur vers l'intérieur.
Dépend de la direction de haute stabilité
En termes de classes, moins il y a de classes qui en dépendent, plus il est stable. Il est également suffisamment stable pour dépendre de sa propre classe. Surtout pour cette dernière raison (plus elle dépend de sa propre classe), l'effet du changement est important et il est difficile de le changer, il doit donc être stable. (Selon la langue, je pense que le nombre d'importations, d'exiger et d'inclure sera une certaine norme.)
Par conséquent, une classe censée changer ne doit pas dépendre d'une classe difficile à changer.
Le degré d'abstraction d'un composant doit être comparable à sa stabilité
Le principe est que les composants hautement stables doivent également être très abstraits et qu'une stabilité élevée ne doit pas interférer avec l'expansion. À l'inverse, les composants moins stables devraient être plus spécifiques. En effet, la faible stabilité facilite la modification du code spécifique à l'intérieur.
Dans le "Principe de Stabilité Dépendance (SDP)", cela devrait dépendre de la direction de stabilité plus élevée, et dans le "Principe de Stabilité / Abstrait Equivalence (SAP)", la stabilité devrait être proportionnelle au degré d'abstraction. En d'autres termes, ** devrait dépendre de la direction de l'abstraction supérieure **.
Il est nécessaire de changer la direction de la dépendance afin de réaliser le "principe de dépendance de stabilité (SDP)" et "principe d'équivalence de stabilité et d'abstraction (SAP)". En introduisant une interface comme moyen de le faire, nous pouvons inverser le sens des dépendances et y parvenir.
Jetons un coup d'œil avant d'appliquer DIP. Dans ce qui précède "avant changement", la "classe stable" dépend de la "classe instable avec de nombreuses modifications", donc s'il y a un changement dans la dernière classe, cela affectera la première classe. .. Plus précisément, ce qui suit est un exemple.
Voici un état où la dépendance est inversée et le problème est résolu pour un tel problème.
En s'appuyant sur l'abstraction via l'interface, les «classes instables avec de nombreuses modifications» ne sont plus directement dépendantes. Peu importe combien vous corrigez une "classe instable avec de nombreuses modifications", vous n'avez pas besoin de modifier une "classe stable" tant que vous héritez de l'interface.
Ce qui est important ici est également décrit dans «Principe de dépendance à la stabilité (SDP)» et «Principe d'équivalence de stabilité et d'abstraction (SAP)», mais la classe abstraite dépendante doit être extrêmement stable. ne pas. Par conséquent, la conception de cette pièce est importante.
Personnellement, je ne pense pas qu'il soit nécessaire d'appliquer à tout prix ce principe de renversement de dépendance (DIP). Particulièrement dans le cas d'une structure simple à l'heure actuelle, il peut être difficile à comprendre en incluant un objet abstrait à une seule couche.
À titre d'exemple concret, je pense qu'il existe de nombreux cas où vous pouvez penser "Je ne veux pas dépendre de cette classe" en inversant la direction de la dépendance.
--Il existe une classe qui n'est pas stable en raison de changements fréquents de spécifications ou de nombreux bogues, et une autre classe est affectée par elle.
Cela devrait être décidé à l'avance dans une certaine mesure en tant que projet, par exemple "jusqu'où peut-il être verrouillé par le cadre".
Généralement, "duplicate is bad" dans le code source. "Ne vous répétez pas" C'est le principe DRY.
Par exemple, supposons que vous ayez plusieurs cas d'utilisation avec des configurations d'écran similaires. Au début de la fabrication, supposons que le même traitement soit standardisé pour le rendre DRY. Cependant, bien que le code était correct à ce moment-là, les exigences de chaque cas d'utilisation sont différentes, donc la ** logique commune ** a une branche if pour chaque utilisateur avec ses propres intentions individuelles, et quelques années plus tard. Peut se transformer en une ** logique commune ** gonflée pleine de branches de jugement.
Pour éviter cela, il est nécessaire de clarifier les domaines problématiques (domaines) auxquels le produit traite, tels que les domaines qui intéressent l'entreprise. En plus de cela, vous devez déterminer soigneusement s'il est ** accidentellement dupliqué ** ou ** vraiment dupliqué **. Dans le premier cas, il n'est pas nécessaire de le faire DRY car il n'est que ** accidentellement dupliqué **.
Il s'agit d'un problème qui est plus susceptible de se produire lorsque le produit devient plus gros. Il est particulièrement important de noter que les classes qui gèrent les règles métier peuvent avoir des noms tels que «common» et «base».
Pour une raison quelconque, c'est la ** question la plus importante ** pour la programmation. Le contenu lié au nom est mentionné dans de nombreux autres articles, je ne donnerai donc qu'un aperçu, mais je pense que les livres suivants seront très utiles.
Après cela, je résumerai la dénomination de «la partie qui gère les règles métier».
Dans DDD, il y a «conception stratégique» et «conception tactique».
L'important est que "le nom propre ne peut être décidé que si la conception stratégique est décidée". Puisque je l'ai mentionné dans l'article de DRY, je le republierai, mais quel est le domaine qui intéresse notre entreprise, et s'il y a quelque chose qui peut être divisé dans ce domaine, etc., le domaine problématique (domaine) traité par le produit est décrit. La tâche de clarifier et de décider du nom du paquet et du nom de la classe en fonction de celui-ci est nécessaire pour une bonne dénomination.
Il est important de ne pas terminer les méthodes et modèles décrits jusqu'à présent avec une seule application, mais de regarder en arrière encore et encore si cela ne convient pas. À ce moment-là, je pense que même si vous pensez avoir écrit du bon code, si vous y repensez plus tard, ce sera un mauvais code. Cependant, avec un code qui a été conscient d'une telle conception, le coût de la modification devrait être beaucoup plus bas qu'auparavant et il devrait être plus facile à changer. Il est important d'apporter constamment des améliorations et de répéter de petites refactorisations.