Ursprünglich einfacher Code wird ebenfalls angehäuft. Durch Hinzufügen oder Ändern von Funktionen, Beheben von Fehlern usw. wird der Code allmählich kompliziert, was zu einer Erhöhung der Korrekturkosten und einer Verringerung der Qualität führt. Dies ist für ein Produkt einer bestimmten Größe ganz natürlich und muss bewusst so gestaltet werden, dass dies verhindert wird.
Hier werden wir die Methoden und Muster vorstellen, die uns normalerweise bekannt sind, um den Code nicht zu komplizieren. (Große Menge an DDD- und Clean Architecture-Komponenten) Der Code in diesem Artikel ist ebenfalls Java, aber ich denke, Sie können ihn bis zu einem gewissen Grad verstehen, ohne Java zu kennen.
Außerdem ist es lang, weil ich es in einem weiten Bereich geschrieben habe, ohne zusammenhängend zu sein ... Da es sich um einen Ankerlink für jeden Artikel unten handelt, bitte nur den Artikel, an dem Sie interessiert sind.
Der "Getter" bezieht sich hier auf eine Instanzvariable, auf die von außen verwiesen werden kann. Der Grund für die Beschränkung auf "wo es um Geschäftslogik geht" ist, dass es aufgrund von Rahmenbeschränkungen möglicherweise nicht möglich ist, bedingungslos damit umzugehen.
Klasse mit Getter
public class Employee {
private String name; //OK, da von außen nicht darauf verwiesen werden kann
private LocalDate contractDate;
public int salary; //Öffentliche Instanzvariablen sind verboten, da sie von außen referenziert werden können
...
//Verboten, da Instanzvariablen mit getter von außen referenziert werden können
public LocalDate getContractDate() {
return this.contractDate;
}
}
Der Prozess "getContractDate" in diesem Beispiel "das Vertragsdatum eines Mitarbeiters abrufen" hat keine geschäftliche Bedeutung. Sie wissen nicht, was Sie tun möchten, indem Sie das Vertragsdatum eines Mitarbeiters abrufen. Wurde hier erreicht, um Mitarbeiter anhand des Vertragsdatums zu identifizieren? Handelt es sich um eine Änderung der Mitarbeiterinformationen aufgrund des Vertragsdatums? ?? Und so weiter ist dies allein bedeutungslos geworden. Dies ist gleichbedeutend mit der Verwendung der erfassten Daten, die auf die Benutzerseite übertragen werden, und ist eine Verschiebung des Entwurfs.
Schauen wir uns nun die Seite an, die die Klasse verwendet, in der "Designverschiebung" auftritt.
Die Seite, die die Klasse verwendet, in der "Entwurfsverschiebung" auftritt
int salary;
if (employee.getContractDate().isBefore(LocalDate.now())) {
//Werden Sie für Vertragsangestellte bezahlt(Es enthält nicht den Tag, aber es gibt Details...)
salary = employee.salary;
}
Wie oben erwähnt, gelangt die Geschäftslogik zu der Klasse, die "Mitarbeiter" verwenden möchte. Wenn es Klassen gibt, die "Mitarbeiter" in Zukunft für denselben Zweck verwenden möchten, wird überall ein ähnlicher Kopiercode in Massenproduktion hergestellt. Um dies zu verhindern, schreiben Sie die Geschäftslogik direkt in die Klasse, die die unten gezeigten Daten enthält.
Klasse, die Getter verbietet
public class Employee {
private LocalDate contractDate;
private int salary;
...
public int calculateSalary() {
if (this.contractDate.isBefore(LocalDate.now())) {
return this.salary;
}
return 0; //Diese Art der Rückkehr ist nicht gut, aber als Beispiel...
}
}
Benutzerklasse
int salary = employee.calculateSalary();
Indem Sie getter
stoppen und Geschäftslogik in die Klasse schreiben, die die Daten enthält, können Sie die Massenproduktion von Kopiercode verhindern und die Geschäftsregeln kennen, indem Sie sich nur die entsprechende Klasse ansehen.
Der "Setter" bezieht sich hier auch auf eine Instanzvariable, deren Wert von außen geändert werden kann.
Klasse mit Setter
public class Employee {
public LocalDate contractDate; //Öffentliche Instanzvariablen können von außen zugewiesen werden, was verboten ist
private int salary;
...
//Verboten, da Instanzvariablen mit dem Setter extern zugewiesen werden können
public void setSalary(int salary) {
this.salary = salary;
}
}
Wieder ist die Logik "setSalary", was keine geschäftliche Bedeutung hat, "das Gehalt eines Mitarbeiters zu ändern". Zum Beispiel sollte es eine Methode sein, die den Geschäftsfluss und die Regeln ausdrückt, z. B. "Ich habe sie hinzugefügt, weil ich eine gute Leistung erbracht habe" und "Ich habe sie korrigiert, weil ich einen Fehler bei der Eingabe gemacht habe".
Die Tatsache, dass der Wert durch "Setter" geändert werden kann, bedeutet auch, dass sich der Status ändert, sodass er nicht sicher ist. Insbesondere wenn die Verarbeitung wie unten gezeigt tiefer wird, wird der Zustand, der vom Menschen leicht erkannt werden kann, überschritten und die Lesbarkeit verschlechtert sich.
methodA(Employee employee)
└ methodB(Employee employee)
└ methodC(Employee employee)
└ methodD(Employee employee) <-Es ist nicht möglich, den Mitarbeiterstatus hier neu zu schreiben!Kann nicht aufholen!
└ methodE(Employee employee)
Ein vollständiger Konstruktor bedeutet "Fixieren Sie die Werte aller Instanzvariablen mit dem Konstruktor". Insbesondere in Klassen, die sich mit Geschäftsregeln befassen, garantieren wir, dass der Konstruktor die folgenden Zustände erstellt.
Definieren Sie als einfaches Beispiel eine Klasse, die die Geschäftsregel ausdrückt, dass "Mitarbeiter immer einen Vertrag haben (es gibt ein Vertragsdatum) und sie können bis zu 3 Monate im Voraus einen Vertrag abschließen".
public class Employee {
private LocalDate contractDate;
public Employee(LocalDate contractDate) {
if (contractDate == null) {
throw new IllegalArgumentException("Vertragsdatum ist erforderlich");
}
LocalDate currentDate = LocalDate.now();
if (contractDate.isAfter(currentDate.plusMonths(3))) {
throw new IllegalArgumentException("Vertragsdaten über 3 Monate im Voraus sind ungültig");
}
this.contractDate = contractDate;
}
}
Indem der Konstruktor so gestaltet wird, dass das Vertragsdatum des Mitarbeiters unbedingt erforderlich ist und das Datum, das 3 Monate im Voraus überschreitet, auf diese Weise eingeschränkt wird, werden die Einschränkungen der Geschäftsregeln bei Verwendung dieser Instanz zwangsweise eingehalten. Es ist auch garantiert eine sichere Instanz. Darüber hinaus wird es zu einer sicheren und vorhersehbaren Instanz, unabhängig vom Wert, und der Benutzer kann sie verwenden, ohne sich um unnötige Dinge kümmern zu müssen.
Nach dem einmaligen Erstellen einer Instanz verringern Vorgänge wie das Ändern des Status oder das schrittweise Zusammenstellen einiger Felder die Sicherheit und Lesbarkeit. Die Instanzgenerierung sollte immer mit atomaren Operationen abgeschlossen werden.
Einführung statischer Factory-Methoden in Bezug auf Konstruktoren. Möglicherweise möchten Sie mehrere Konstruktoren erstellen, die in Bezug auf Geschäftsregeln überlastet sind. Wie im Kommentar des folgenden Beispiels beschrieben, ist es wie "dieser Konstruktor im Fall von ○○" und "dieser Konstruktor im Fall von △△".
public class SearchDateTime {
private LocalDate date;
private LocalTime time;
//Ich möchte, dass Sie diesen Konstruktor nur am Tag verwenden...
public SearchDateTime(LocalDate date, LocalTime time) {
this.date = date;
this.time = time;
}
//Ich möchte, dass Sie den Konstruktor nur am Tag verwenden...
public SearchDateTime() {
this.date = LocalDate.now();
this.time = LocalTime.now();
//Es spielt keine Rolle, aber lassen Sie uns den Konstruktor verketten!
// this(LocalDate.now(), LocalTime.now());
}
}
Wenn es für das oben Gesagte mehrere Konstruktoren gibt, wird dies im Kommentar geschrieben, aber es ist für den Benutzer schwierig zu verstehen, welchen er verwenden soll, allein aufgrund des Unterschieds in den Argumenten.
Die Lösung hierfür ist der Titel "Statische Factory-Methode anstelle des Konstruktors". Bereiten Sie eine statische Factory-Methode vor, die für jede Verwendung wie unten gezeigt benannt ist.
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());
}
//Der Konstruktor wird privat gemacht und nicht nach außen ausgesetzt
private SearchDateTime(LocalDate date, LocalTime time) {
this.date = date;
this.time = time;
}
}
Der ursprüngliche Konstruktor reduziert die Sichtbarkeit auf "privat" und verhindert, dass sie direkt von außen verwendet wird. Der Vorteil statischer Factory-Methoden besteht darin, dass sie ** benannt ** werden können. Indem Sie den Namen explizit angeben, können Sie dem Benutzer Ihre Absicht mitteilen, ohne ihn mehrdeutig zu machen. Als weiteres Anwendungsfallbeispiel ist es außerdem effektiv, die von der Administratorrolle und der normalen Rolle zu bedienenden Objekte einzuschränken und die Vorgänge zu klären, die nicht durch die Benennung und den Argumenttyp verwechselt werden dürfen.
Sie können die Sicherheit erhöhen, indem Sie sicherstellen, dass der Wert nach dem Festlegen nicht geändert wird. Durch Definieren des Modifikators "final" für die Instanzvariable ändert sich der Wert nach der Initialisierung im Konstruktor nicht.
public class Employee {
//Fügen Sie allen Instanzvariablen den letzten Modifikator hinzu
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 //Kompilierungsfehler!!
}
}
Der Hauptzweck des Beendens von Setter besteht darin, es zu einem vollständigen Konstruktor und unveränderlichen Objekt zu machen, damit Sie sich keine Gedanken über Statusänderungen machen müssen. Da die Menge an Informationen, an die sich eine Person vorübergehend erinnern kann, gering ist, ist es wichtig, die Menge an Bedenken so weit wie möglich zu reduzieren.
Jetzt haben Sie ein unveränderliches Objekt mit einem unveränderlichen Wert. Wenn Sie den Wert jedoch ändern möchten, erstellen Sie eine neue Instanz und geben Sie sie zurück. Es ändert seinen eigenen Wert nicht.
public class Employee {
private final String name;
private final LocalDate contractDate;
private final int salary;
...
//Vertragsdatum und Gehalt neu vergeben und ändern
public Employee contractRenewal(int salary) {
return new Employee(this.name, LocalDate.now(), salary);
}
}
//Benutzerseite
Employee employee = new Employee(...);
Employee extendedEmployee = employee.contractRenewal(200000); //Als separate Instanz behandeln.
Sowohl "Demeter's Law" als auch "Don't Ask" sind Gestaltungsprinzipien im Zusammenhang mit dem Verstecken von Informationen. Schauen wir uns zunächst ein Bild eines Beispiels an, in dem dieses Konstruktionsprinzip nicht eingehalten wird, und ein Beispiel, in dem es eingehalten wird.
Sie können sehen, dass Sie eine schöne V-Form erhalten, wenn Sie diesem Designprinzip folgen.
Schauen wir uns als nächstes den Code an. Als einfaches Beispiel zeigt es die Logik, "den Verkaufspreis eines Produkts mit einer Gutscheingebühr zu ermitteln".
Fragen Sie nicht nach Demeters Gesetz Benutzerlogik, die dem Befehl nicht folgt
Product product = new Product(something);
int sellingPrice;
if (product.price.couponExpiration.isAfore(LocalDate.now())) {
//Gibt den Gutscheinpreis des Artikels zurück, wenn der Gutschein gültig ist(Dies beinhaltet auch nicht den Tag, aber es gibt Details...)
sellingPrice = product.price.couponValue;
} else {
//Produktpreis zurückgeben, wenn das Ablaufdatum des Gutscheins ungültig ist
sellingPrice = product.price.value;
}
Oben wird eine Anfrage von if in "product.price.couponExpiration" generiert, um das Ablaufdatum des Gutscheins zu bestimmen. Wir greifen auch auf den "Preis" zu, der dem "Produkt" gehört.
Der Code, der durch Anwenden von "Demeters Gesetz" und "Nicht fragen" auf diesen Code verbessert wurde, lautet wie folgt.
Demeters Gesetz und nicht fragen, Benutzerlogik
Product product = new Product(something);
int sellingPrice = product.sellingPrice();
Die obige "Benutzerlogik, in der Demeters Gesetz und der Befehl, nicht zu fragen", wird mit einer Anweisungszeile vervollständigt. Der Benutzer erhält nur ** Mindestkenntnisse ** und muss nur den Befehl "Find the Selling Price" aufrufen.
Die "Produkt" -Klasse, die dieses "Demeter-Gesetz und nicht fragen" enthält, lautet wie folgt.
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;
}
}
Ursprünglich ist die Klasse "Preis" für die Beurteilungsverarbeitung zuständig, z. B. für die Verzweigung auf der Benutzerseite, und sie ist in der Klasse "Produkt" zusammengefasst. Geschäftslogik wie Berechnung und Beurteilung wird ** von der Klasse behandelt, die die Daten hat **. Darüber hinaus ist die Klasse "Preis" als paketprivat definiert, und die Methode "verkaufspreis" ist ebenfalls paketprivat. Mit anderen Worten, diese Klasse und Methode können von der Benutzerseite aus nicht gesehen werden (unter der Annahme eines anderen Pakets), und eine direkte Ausführung ist nicht zulässig. Auf diese Weise können Sie die ** Wissensexposition ** zwangsweise reduzieren, die Abhängigkeit verringern und lose koppeln.
Nach Demeters Gesetz ist dies das Bild.
In MVC und Layered Architecture wird oft über die Trennung von Interesse gesprochen, aber es ist ein Prinzip, das ich unabhängig von der Granularität beachten möchte. Angenommen, Sie haben eine Klasse, die sich mit "Mitarbeitern" befasst. In dieser "Mitarbeiter" -Klasse sind wir nicht besorgt darüber, wie Sie benachrichtigen sollen, wenn ein Geschäftsfehler auftritt. Wenn es sich um eine API handelt, sollte die Nachricht in einem Format wie JSON gepackt sein. Wenn ein Bildschirm wie eine Website angezeigt wird, sollte er geändert und ausgegeben werden, damit der Benutzer ihn leicht erkennen kann. In dieser Klasse "Mitarbeiter" sind "das Auftreten eines Geschäftsfehlers" und "der Inhalt des Geschäftsfehlers (Fehlermeldung)" die Bedenken, die bekannt sein sollten. Wie es ausgegeben und geändert wird, ist ein Anliegen einer anderen Klasse (z. B. Präsentationsebene). Wenn die Bildschirmverarbeitung auf der Benutzerseite in die Klasse "Mitarbeiter" geschrieben ist, ohne die oben genannte Interessentrennung zu kennen, sollte die Klasse "Mitarbeiter" nicht relevant sein, wenn sich die Bildschirmausgabemethode ändert. Wird auch korrigiert, wodurch der Einflussbereich erweitert wird. Es verstößt auch gegen eines der SOLID-Prinzipien, das ** Prinzip der Einzelverantwortung **. https://qiita.com/UWControl/items/98671f53120ae47ff93a
Durch die Trennung der Interessen nach Rollen, die Festlegung und Abgrenzung der Verantwortungsgrenzen ist es möglich, lose gekoppelte und sichere Anwendungen mit geringerer Abhängigkeit von außen zu entwerfen. Ist möglich. Dies führt auch zu einer Verringerung der ** Wissensexposition **.
TL;DR Weil es ein bisschen lang ist.
Eine "Komponente" ist ein Modul oder eine Einheit aus Glas oder Edelstein. Es ist jedoch nicht auf Komponenten beschränkt, sondern sollte auf jedes Objekt angewendet werden. Daher basiert die Erklärung hier auf der Klasse.
Das Komponentenabhängigkeitsdiagramm darf keine zirkulären Abhängigkeiten aufweisen
Es gibt ein Prinzip. Es sollte so gestaltet sein, dass es in keiner Struktur eine zirkuläre Abhängigkeit gibt, nicht nur in "Komponenten". Stellen Sie sicher, dass die Abhängigkeit nach innen gerichtet ist und nur von einer Richtung ohne Zirkulation abhängt. Es ist wichtig, das Äußere nicht von innen zu kennen und das Wissen und die Regeln von außen nicht nach innen zu bringen.
Abhängig von der Richtung der hohen Stabilität
In Bezug auf Klassen ist es umso stabiler, je weniger Klassen davon abhängen. Es ist auch stabil genug, um von seiner eigenen Klasse abhängig zu sein. Insbesondere aus dem letzteren Grund (je stabiler es von seiner eigenen Klasse abhängt) ist der Effekt der Änderung groß und es ist schwierig, sie zu ändern, daher muss sie stabil sein. (Abhängig von der Sprache denke ich, dass die Anzahl der Importe, Anforderungen und Einschlüsse ein bestimmter Standard sein wird.)
Daher sollte eine Klasse, die sich ändern soll, nicht von einer Klasse abhängig sein, die schwer zu ändern ist.
Der Abstraktionsgrad einer Komponente muss mit ihrer Stabilität vergleichbar sein
Das Prinzip ist, dass hochstabile Komponenten auch sehr abstrakt sein sollten und dass eine hohe Stabilität die Expansion nicht beeinträchtigen sollte. Umgekehrt sollten weniger stabile Komponenten spezifischer sein. Dies liegt daran, dass die geringe Stabilität das Ändern des darin enthaltenen Codes erleichtert.
Das "Prinzip der Stabilitätsabhängigkeit (SDP)" sollte von der Richtung höherer Stabilität abhängen, und das "Prinzip der Äquivalenz von Stabilität und Abstraktion (SAP)" bedeutet, dass die Stabilität proportional zum Abstraktionsgrad sein sollte. Mit anderen Worten, ** sollte von der Richtung der höheren Abstraktion ** abhängen.
Es ist notwendig, die Richtung der Abhängigkeit zu ändern, um das "Prinzip der Stabilitätsabhängigkeit (SDP)" und das "Prinzip der Äquivalenz von Stabilität und Abstraktion (SAP)" zu realisieren. Durch die Einführung einer Schnittstelle können wir die Richtung der Abhängigkeiten umkehren und dies erreichen.
Lassen Sie uns einen Blick darauf werfen, bevor Sie DIP anwenden. Im obigen "Vor der Änderung" hängt die "stabile Klasse" von der "instabilen Klasse mit vielen Modifikationen" ab. Wenn sich also die letztere Klasse ändert, wirkt sich dies auf die erstere Klasse aus. .. Im Einzelnen ist das Folgende ein Beispiel.
Hier ist ein Zustand, in dem die Abhängigkeit umgekehrt wird und das Problem für ein solches Problem gelöst ist.
Durch die Verwendung der Abstraktion über die Schnittstelle sind "instabile Klassen mit vielen Modifikationen" nicht mehr direkt abhängig. Unabhängig davon, wie oft Sie eine "instabile Klasse mit vielen Änderungen" reparieren, müssen Sie keine "stabile Klasse" ändern, solange Sie die Schnittstelle erben.
Was hier wichtig ist, wird auch in "Prinzip der Stabilitätsabhängigkeit (SDP)" und "Prinzip der Stabilität und Abstraktionsäquivalenz (SAP)" beschrieben, aber die abhängige abstrakte Klasse muss äußerst stabil sein. nicht. Daher ist das Design dieses Teils wichtig.
Persönlich halte ich es nicht für notwendig, dieses Prinzip der Abhängigkeitsumkehr (DIP) um jeden Preis anzuwenden. Insbesondere im Fall einer einfachen Struktur kann es derzeit schwierig sein, ein einschichtiges abstraktes Objekt einzuschließen.
Als konkretes Beispiel denke ich, dass es viele Fälle gibt, in denen Sie denken, "Ich möchte nicht von dieser Klasse abhängig sein", wenn Sie die Abhängigkeitsrichtung umkehren.
Als Projekt sollten Sie die Richtung bis zu einem gewissen Grad im Voraus festlegen, z. B. "Wie weit können Sie vom Framework gesperrt werden".
Im Allgemeinen ist "Duplikat ist schlecht" im Quellcode. "Wiederholen Sie sich nicht" Dies ist das DRY-Prinzip.
Angenommen, Sie haben mehrere Anwendungsfälle mit ähnlichen Bildschirmkonfigurationen. Nehmen wir zu Beginn der Herstellung an, dass dieselbe Verarbeitung standardisiert ist, um sie TROCKEN zu machen. Obwohl es zu diesem Zeitpunkt kein Problem gab, sind die Anforderungen für jeden Anwendungsfall unterschiedlich, sodass die ** gemeinsame Logik ** für jeden Benutzer einen if-Zweig mit seinen eigenen individuellen Absichten hat, und einige Jahre später. Kann sich in eine aufgeblähte ** gemeinsame Logik ** voller Urteilszweige verwandeln.
Um dies zu vermeiden, müssen die Problembereiche (Domänen) geklärt werden, mit denen sich das Produkt befasst, z. B. die Bereiche, an denen das Unternehmen interessiert ist. Darüber hinaus müssen Sie sorgfältig feststellen, ob es ** versehentlich ** oder ** wirklich dupliziert ** ist. Im ersteren Fall muss es nicht trocken gemacht werden, da es nur ** versehentlich dupliziert ** wird.
Dies ist ein Problem, das eher auftritt, wenn das Produkt größer wird. Es ist besonders wichtig zu beachten, dass Klassen, die Geschäftsregeln behandeln, Namen wie "common" und "base" haben können.
Aus irgendeinem Grund ist es die ** wichtigste Angelegenheit ** für die Programmierung. Der Inhalt, der sich auf den Namen bezieht, wird in vielen anderen Artikeln erwähnt, daher werde ich nur einen Überblick geben, aber ich denke, die folgenden Bücher werden sehr hilfreich sein.
Danach werde ich die Benennung von "dem Teil, der Geschäftsregeln behandelt" zusammenfassen.
In DDD gibt es "strategisches Design" und "taktisches Design".
Wichtig ist, dass "der richtige Name nur festgelegt werden kann, wenn das strategische Design festgelegt ist". Da ich es in DRY erwähnt habe, werde ich es erneut veröffentlichen. Was ist jedoch der Bereich, an dem unser Unternehmen interessiert ist, und wenn es etwas gibt, das in diesen Bereich usw. unterteilt werden kann, wird der vom Produkt behandelte Problembereich (Domäne) beschrieben. Die Arbeit, den Paketnamen und den Klassennamen danach zu klären und zu entscheiden, ist für eine gute Benennung notwendig.
Es ist wichtig, die bisher beschriebenen Methoden und Muster nicht einmal anzuwenden, sondern immer wieder zurückzublicken, wenn sie nicht passen. Zu dieser Zeit denke ich, dass selbst wenn Sie denken, dass Sie guten Code geschrieben haben, wenn Sie später darauf zurückblicken, es ein schlechter Code sein wird. Code, der sich eines solchen Designs bewusst war, sollte jedoch viel billiger zu modifizieren und einfacher zu ändern sein als zuvor. Es ist wichtig, ständig Verbesserungen vorzunehmen und kleine Refactorings zu wiederholen.
Recommended Posts