Objekte in der objektorientierten Programmierung sind Abhängig von der Implementierungsmethode gibt es mehrere interne Zustandsverwaltungsmethoden.
Durch korrektes Verständnis dieser internen Zustandsverwaltungsmethode Fehler sind schwer zu verursachen, und selbst wenn sie auftreten, kann die Ursache leicht identifiziert werden. Sie können auch Programme erstellen, die einfach zu testen sind.
In diesem Artikel werde ich die Unveränderlichkeit des inneren Zustands erläutern. Es wird auch kurz auf die Nebenwirkungen der Methode eingegangen.
――Ich möchte sagen, dass objektorientiert im Allgemeinen ... aber Leute, die die Java-Sprache verwenden
In dieser Erklärung wird der Code am Beispiel von "Janken" erstellt. Die folgenden Janken-Hände (Strategien) und Schiedsrichter werden im Voraus definiert. (Das Urteil von Janken hat nichts mit diesem Thema zu tun, daher tut mir die grobe Umsetzung leid.)
public enum HandStrategy {
/**Schmiere*/
ROCK,
/**Par*/
PAPER,
/**Choki*/
SCISSORS
}
public class Referee {
public static enum Result {
LEFT_WIN,
RIGHT_WIN,
DRAW
}
public static Result battle(HandStrategy left, HandStrategy right) {
if (left == right)
return Result.DRAW;
if (left == HandStrategy.ROCK)
return right == HandStrategy.PAPER ? Result.RIGHT_WIN : Result.LEFT_WIN;
else if (left == HandStrategy.PAPER)
return right == HandStrategy.SCISSORS ? Result.RIGHT_WIN : Result.LEFT_WIN;
else
return right == HandStrategy.ROCK ? Result.RIGHT_WIN : Result.LEFT_WIN;
}
}
Der interne Status eines Objekts ist der Wert (oder eine Kombination aus mehreren, falls vorhanden) Elementvariablen (in Java auch als Instanzfelder bezeichnet), über die jedes instanziierte Objekt verfügt.
Zum Beispiel in den folgenden Fällen
public class Hand {
private HandStrategy strategy;
public void setStrategy(HandStrategy strategy) {
this.strategy = strategy;
}
public static void main(String[] args) {
Hand left = new Hand();
Hand right = new Hand();
left.setStrategy(HandStrategy.ROCK);
right.setStrategy(HandStrategy.PAPER);
}
}
Der interne Zustand ("Strategie") des Objekts "links" ist goo ("ROCK"). Der interne Zustand ("Strategie") des Objekts "rechts" ist par ("PAPIER"). Es wird sein.
Der Inhalt der Variablen ist für das Thema "Unveränderlichkeit", das wir dieses Mal diskutieren werden, nicht wichtig. Es klassifiziert, wann und wie sich der Inhalt ändert oder nicht.
Invariante Klassifikationen "veränderlich" und "unveränderlich", Und ich werde ein kleines spezielles "Staatenloses" erklären.
Veränderlich bedeutet "variabel" Ein Objekt, dessen interner Zustand sich in der Mitte ändern kann.
public class MutableHand {
private HandStrategy strategy;
public void setStrategy(HandStrategy strategy) {
this.strategy = strategy;
}
public HandStrategy getStrategy() {
return strategy;
}
}
Das Objekt hat einen Status (= es gibt ein Instanzfeld) und es gibt eine Methode zum Ändern des Status. Dieses Mal gibt es eine Methode, um den Zustand absichtlich umzuschreiben. Einige Methoden können unbeabsichtigt umgeschrieben werden. Mehr dazu im Kapitel Unveränderlich.
private static void execMutable() {
final MutableHand left = new MutableHand();
final MutableHand right = new MutableHand();
left.setStrategy(HandStrategy.ROCK);
right.setStrategy(HandStrategy.PAPER);
Referee.battle(left.getStrategy(), right.getStrategy());
//Das zweite Mal wechseln wir den Besitzer und spielen
left.setStrategy(HandStrategy.SCISSORS);
Referee.battle(left.getStrategy(), right.getStrategy());
}
Die Variable, in der das Objekt gespeichert ist, ist "final" -qualifiziert. Es ist wichtig zu beachten, dass sich dies nicht ändert und der interne Status des Objekts geändert werden kann.
Erstellen Sie beim Spielen des zweiten Spiels kein neues Objekt Der Status des vorhandenen Objekts wird neu geschrieben und ausgeführt.
Wenn Sie eine Klasse ohne unveränderliches Verständnis implementieren, erhalten Sie wahrscheinlich diese Mutable-Implementierung. Dies liegt daran, dass es am einfachsten zu implementieren ist und nur wenige Dinge zu beachten sind. Andererseits hat die unveränderliche Implementierung, die später erläutert wird, einige Überlegungen und ist ziemlich mühsam.
Im Allgemeinen sind veränderbare Implementierungen schwer zu schreibende Tests, und wenn Fehler eingeführt werden, sind sie in der Regel weniger reproduzierbar und dauern länger, um die Ursache zu identifizieren. (Siehe Nachteilsspalte) Sie sollten sich also nicht aggressiv für eine Mutable-Implementierung entscheiden. Es wird jedoch aus negativen Gründen ausgewählt, hauptsächlich wenn es schwierig ist, unveränderlich zu machen. (Warum es später schwierig sein kann)
** weniger implementiert ** Wenn Sie nur die Daten speichern möchten, können Sie sie mit einer minimalen Implementierung erstellen. (Es ist fraglich, ob es das beste ist)
** Kann in einen anderen Status geändert werden, ohne die Instanz neu zu erstellen ** In Bezug auf die Speichereffizienz ist es effizient, da nur Felder geändert werden müssen und weniger zusätzlicher Speicher benötigt wird. Es kann auch die Kosten für die Instanziierung senken, was in leistungskritischen Situationen gut sein kann.
** Die Ergebnisse der Methodenausführung unterscheiden sich je nach internem Status ** Da das Ausführungsergebnis der Methode möglicherweise vom internen Status abhängt, ist es möglicherweise nicht möglich, es abzudecken, ohne so viele Testfälle zu erstellen, wie es Kombinationen von Status gibt. Es ist möglicherweise nicht möglich, den Test abzudecken, insbesondere in Situationen, in denen sich der interne Zustand von Moment zu Moment ändert.
** Ergebnisse ändern sich in der Reihenfolge der Verarbeitung ** Wenn sich beim Testen mehrerer Methoden der interne Status der Methode ändert, die unmittelbar zuvor für den Test ausgeführt wurde, wirkt sich dies auf den nächsten Test aus. Da erwartet wird, dass die Verarbeitung während der Produktion komplizierter wird, können Fehler leicht gemischt werden, wenn sie in der richtigen Reihenfolge betroffen sind.
** Es muss bekannt sein, wann sich der Status geändert hat ** Wenn Sie versuchen, zu reproduzieren, wenn ein Fehler auftritt, können Sie ihn nur reproduzieren, wenn Sie wissen, wo und welche Art von internem Zustand sich das Ursachenobjekt geändert hat, oder es ist schwierig, "wo sich der interne Status geändert hat" zu finden und zu untersuchen Braucht oft Zeit.
** Es ist schwierig, bereits verwendete Objekte wiederzuverwenden ** Wenn es bereits verwendet wurde (auf das verwiesen wird), ist es schwierig, es an anderer Stelle wiederzuverwenden. Dies liegt daran, dass das Risiko besteht, dass ein schwer zu identifizierendes Problem auftritt, wenn der interne Status unbeabsichtigt neu geschrieben wird.
Unveränderlich bedeutet "unveränderlich" Ein Objekt, dessen interner Zustand sich nach der Instanziierung nicht ändert.
public class ImmutableHand {
private final HandStrategy strategy;
public ImmutableHand(HandStrategy strategy) {
this.strategy = strategy;
}
public HandStrategy getStrategy() {
return strategy;
}
}
Das Objekt hat einen Status, es gibt jedoch keine Methode zum Ändern des Status. Der Zustand wird beim Instanziieren vom Konstruktor bestimmt. Danach darf der Zustand nicht mehr geändert werden.
private static void execImmutable() {
final ImmutableHand left = new ImmutableHand(HandStrategy.ROCK);
final ImmutableHand right = new ImmutableHand(HandStrategy.PAPER);
Referee.battle(left.getStrategy(), right.getStrategy());
//Erstellen Sie beim zweiten Mal eine neue Instanz
final ImmutableHand left2 = new ImmutableHand(HandStrategy.SCISSORS);
Referee.battle(left2.getStrategy(), right.getStrategy());
}
Nach der Instanziierung kann das Objekt seinen internen Status nicht ändern Wenn Sie Ihre Hand ein zweites Mal wechseln möchten, benötigen Sie eine neue Instanziierung. Wenn Sie jedoch nicht den Besitzer wechseln müssen, ist garantiert, dass das zuvor verwendete Objekt sicher wiederverwendet werden kann. Wenn es unveränderlich ist, ändert sich der Inhalt nicht.
Unveränderliche Implementierungen werden im Allgemeinen als überlegen gegenüber veränderlichen Implementierungen angesehen. Die unveränderliche Implementierung kann einige der Testbarkeits- und fehlerspezifischen Probleme der veränderlichen Implementierung lösen. (Siehe Verdienstspalte)
Sie können Parameter übergeben, um den internen Status beim Instanziieren zu bestimmen, aber Sie können den internen Status zu anderen Zeiten nicht ändern. Dies garantiert, dass sich der interne Status unabhängig davon, was Sie mit dem bereits instanziierten Objekt tun, nicht ändert, dh das Objekt wird vor oder nach seiner Wiederverwendung nicht beeinflusst.
Die unveränderliche Implementierung klingt aber gut Sie müssen sehr strenge Einschränkungen einhalten, um die Vorteile nutzen zu können.
Zum einen muss der interne Zustand beim Instanziieren wie oben beschrieben bestimmt werden. Das andere ist, dass die Instanzfelder von unveränderlichen Objekten ebenfalls unveränderlich sein müssen.
Ist die folgende Implementierung unveränderlich?
public class NonImmutable {
private final String[] word = {"Apfel", "Gorilla", "Rap"};
public String[] getWord() {
return word;
}
}
Auf den ersten Blick gibt es keine Methode, um den internen Zustand zu ändern. Wenn jedoch wie folgt verwendet wird,
private static void execNonImmutable() {
NonImmutable non = new NonImmutable();
non.getWord()[1] = "Godzilla";
System.out.println(non.getWord()[1]);
}
Wird die Zeichenfolge auf der Konsole "Gorilla" oder "Godzilla" angezeigt?
Die richtige Antwort lautet "Godzilla".
Diese Implementierung ist nicht unveränderlich, da ein veränderbares Feld außerhalb des Objekts unverändert geändert werden kann.
Es ist zu beachten, dass der interne Status des Instanzfelds bei einer unveränderlichen Implementierung nicht neu geschrieben werden kann. (Es ist notwendig, dass alles unveränderlich ist, indem man zum inneren Zustand des inneren Zustands zurückkehrt ...)
Die Java-Bibliothek verfügt über einige Funktionen, die sie unveränderlich machen.
Zum Beispiel
Collections.unmodifiableList(list)
Mit der obigen Methode können Sie eine unveränderliche Liste aus einer vorhandenen Liste erstellen.
Es ist eine gute Idee, diese Komfortfunktionen zu verwenden, um versehentliche Änderungen zu verhindern.
** Ergebnisse hängen nicht von der Reihenfolge der Methodenausführung ab ** Selbst wenn Sie mehrere Methoden in einem Test testen, sind sie in der Reihenfolge nicht betroffen.
** Leicht zu verstehender Status ** Der Status ist leicht zu verstehen, da nur die Parameter zum Zeitpunkt der Instanziierung den internen Status beeinflussen.
** Einfache Wiederverwendung von Objekten ** Es gibt nichts zu beachten, da garantiert ist, dass der Status des Objekts auch bei Wiederverwendung nicht beeinträchtigt wird.
** Speichereffizienz kann gut sein ** Die Speichereffizienz kann verbessert werden, indem die zu verwendenden Objekte im Voraus erstellt und implementiert werden, damit sie wiederverwendet werden können. Dies ist jedoch der Fall, wenn das Zustandsmuster begrenzt und vorhersehbar ist. (Wenn es nicht vorhersehbar ist, ist es ein Nachteil)
** Die Speichereffizienz ist möglicherweise schlecht ** Nach der Instanziierung kann der interne Status nicht mehr geändert werden. Daher müssen Sie grundsätzlich ein neues Objekt mit einem anderen Status erstellen. Dies wirkt gegen viele Muster von Bedingungen.
** Die Ergebnisse der Methodenausführung unterscheiden sich je nach internem Status ** Da das Ausführungsergebnis der Methode möglicherweise vom internen Status abhängt, ist es möglicherweise nicht möglich, es abzudecken, ohne so viele Testfälle zu erstellen, wie es Kombinationen von Status gibt. Es ist jedoch immer noch einfacher zu testen als Mutable.
** Einschränkungen erschweren (oder unmöglich) die Implementierung ** Unveränderliche Implementierungsfelder können nicht der Außenwelt ausgesetzt werden, was komplexe Implementierungen wie defensives Kopieren erzwingen kann. Unveränderlich ist schwierig, wenn der Zustand zum Zeitpunkt der Instanziierung nicht angegeben werden kann.
Staatenlos bedeutet "hat keinen Zustand" Dies bedeutet, dass das Objekt keinen Status hat, unabhängig davon, ob es instanziiert ist oder nicht.
Staatenlos ist auch unveränderlich als Voraussetzung. Unveränderlich wird als unveränderlich klassifiziert, aber da es einige zusätzliche Funktionen hat, wird es absichtlich als separater Rahmen festgelegt.
public abstract class StatelessHand {
private static final Rock ROCK_IMPL = new Rock();
private static final Paper PAPER_IMPL = new Paper();
private static final Scissors SCISSORS_IMPL = new Scissors();
public static StatelessHand of(HandStrategy strategy) {
switch(strategy) {
case ROCK:
return ROCK_IMPL;
case PAPER:
return PAPER_IMPL;
case SCISSORS:
return SCISSORS_IMPL;
}
throw new IllegalArgumentException();
}
public abstract HandStrategy getStrategy();
public abstract String getName();
private static class Rock extends StatelessHand {
@Override
public HandStrategy getStrategy() {
return HandStrategy.ROCK;
}
@Override
public String getName() {
return "Schmiere";
}
}
private static class Paper extends StatelessHand {
@Override
public HandStrategy getStrategy() {
return HandStrategy.PAPER;
}
@Override
public String getName() {
return "Par";
}
}
private static class Scissors extends StatelessHand {
@Override
public HandStrategy getStrategy() {
return HandStrategy.SCISSORS;
}
@Override
public String getName() {
return "Choki";
}
}
}
Das Objekt hat keinen Status. (Kein Instanzfeld) Die Methoden jeder Implementierungsklasse ("Rock", "Paper", "Scissors") geben einen festen Wert zurück. (= Das Ergebnis ändert sich nicht)
Ein weiteres zustandsloses Objekt ist bereits erschienen. Es ist "Schiedsrichter".
private static void execStateless() {
final StatelessHand left = StatelessHand.of(HandStrategy.ROCK);
final StatelessHand right = StatelessHand.of(HandStrategy.PAPER);
System.out.println(left.getName() + " VS " + right.getName());
Referee.battle(left.getStrategy(), right.getStrategy());
//Holen Sie sich beim zweiten Mal eine neue Instanz
final StatelessHand left2 = StatelessHand.of(HandStrategy.SCISSORS);
System.out.println(left2.getName() + " VS " + right.getName());
Referee.battle(left2.getStrategy(), right.getStrategy());
}
Zustandslose Implementierungen erfordern fast immer keine Instanziierung. "Schiedsrichter" führt die "Kampf" -Methode aus, ohne sie zu instanziieren. Der Status des Objekts hat keinen Einfluss auf das Ergebnis (Rückgabewert).
Auf den ersten Blick scheint die Implementierung von Stateless nicht instanziiert werden zu müssen. Unterklassen von "StatelessHand" werden instanziiert. Dies ist eine effektive Methode, wenn Sie auch mit einer zustandslosen Implementierung Polymorphismus erzielen möchten.
Zustandslose Objektmethoden haben das gleiche Ergebnis, wenn die Argumente gleich sind. Es scheint jedoch nicht dasselbe Ergebnis zu liefern, wenn Sie nicht argumentative (z. B. statische) Objekte innerhalb der Methode verwenden. Dies ist syntaktisch kein Argument, sondern fungiert als "verstecktes Argument" für die Ausführung der Methode. Es ist ein anderes Problem als die Tatsache, dass das Ergebnis nicht vom Status des zustandslosen Objekts abhängt.
Bei der Implementierung von "strict Stateless [^ 1]" (nur die Argumente wirken sich auf das Ergebnis aus) sollten Sie andere unveränderliche Objekte als die Argumente verwenden.
Unterklassen von StatelessHand
sind streng Stateless, da sie in ihren Methoden die unveränderlichen Objekte Enum
und String
verwenden.
[^ 1]: "Strict Stateless" ist ein Wort, das ich ohne Erlaubnis geprägt habe, damit es von anderen Menschen nicht verstanden werden kann (Schweiß)
** Die Speichereffizienz ist fast am höchsten ** Eine Instanz kann die einzige in einer Anwendung sein, da zustandslose Objekte beliebig oft wiederverwendet werden können. Abhängig von Ihrer Implementierung müssen Sie sie möglicherweise nicht einmal instanziieren.
** Ergebnisse hängen nicht von der Reihenfolge der Methodenausführung ab ** Selbst wenn Sie mehrere Methoden in einem Test testen, sind sie in der Reihenfolge nicht betroffen. (Es gibt Hinweise. Siehe Spalte mit den Nebenwirkungen.)
** Die Methode gibt immer das gleiche Ergebnis zurück, wenn die Argumente gleich sind ** Das Erstellen von Testfällen ist häufig einfach, da das Ergebnis einer Methode in jedem Status der Anwendung vom Argument abhängt. (Sie müssen jedoch vorsichtig mit "versteckten Argumenten" sein)
** Kann nicht gekapselt werden ** Die Kapselung (hier bedeutet das Kombinieren von Daten und Verarbeitung), ein Merkmal und Vorteil der objektorientierten Programmierung, kann in zustandslosen Implementierungen nicht durchgeführt werden, da keine Instanzfelder vorhanden sind.
** Es gibt nur wenige Kurven (sieht aus wie) ** Es gibt fast keine andere Wendung als die Utility-Klasse. Was Sie jedoch als Klasse definieren, bedeutet. Tatsächlich werden in Java 8 und höher häufig zustandslose Objekte verwendet.
Es gab eine Zeit, in der zustandslose Objekte vor einem Jahrzehnt selten in der objektorientierten Programmierung verwendet wurden oder eher als Anti-Patterns bezeichnet wurden. (Da es keinen Status gibt, wird die Methode statisch definiert, ohne sie zu instanziieren usw.)
Die objektorientierte Einführung von funktionalem Programmier-Know-how hat Stateless jedoch eine wichtige Position eingeräumt. Ich werde nicht auf das gesamte hier eingebrachte Know-how eingehen, aber ein Beispiel ist ** Lambda **.
Lambda in Java verwendet tatsächlich Objekte mit einer zustandslosen Implementierung.
Das folgende Beispiel ist eine Methode, die a und b als Argumente übergibt, und ein Lambda-Ausdruck, der eine Berechnung für diese beiden Werte durchführt.
calc(a, b, (x,y)->x+y)
Dies passiert, wenn Sie diese Methode ohne Verwendung von Lambdas aufrufen.
calc(a, b, new BiFunction<Integer, Integer, Integer>() {
@Override
public Integer apply(Integer x, Integer y) {
return x + y;
}
});
Wir instanziieren und übergeben eine Klasse, die eine Schnittstelle namens "BiFunction" implementiert. Lambda ist eine Beschreibungsmethode, die die Implementierung und Instanziierung einer Klasse bis zum Äußersten vereinfacht, und das Objekt ist eine zustandslose Implementierung.
final int offset = /*Offsetwert*/;
calc(a, b, (x,y)->x+y+offset);
calc(a, b, new BiFunction<Integer, Integer, Integer>() {
@Override
public Integer apply(Integer x, Integer y) {
return x + y + offset;
}
});
Im Fall eines Lambda-Ausdrucks ist klar, dass ein anderes Objekt als das Argument verwendet wird, daher denke ich, dass die Lesbarkeit gut ist.
Lambda und Methodenreferenzen, die in Java 8 erschienen sind (Details werden weggelassen), haben andere grammatikalische Merkmale als herkömmliches Java, und es ist ein Mechanismus, der für Anfänger schwer zu verstehen ist. Mit ihrem Aufkommen hat Stateless einen bedeutenden Wert gefunden, daher ist es eine gute Idee, darüber nachzudenken, ohne zu denken, dass es keinen Nutzen hat oder ein Anti-Muster ist.
Es gibt einen "Nebeneffekt", der häufig in der funktionalen Programmierung verwendet wird (oder sich anfühlt).
Die zustandslose Objektmethode empfängt ein Argument und gibt das Ergebnis als Rückgabewert zurück. Wenn Sie diese Methode übergeben, ändert sich jedoch der interne Status des als Argument übergebenen Objekts oder das Innere des oben erläuterten "versteckten Arguments" Die Änderung des Zustands wird als "Nebenwirkung" bezeichnet.
Wenn dieser Nebeneffekt auftritt, kann es sein, dass der zustandslose Vorteil "Methoden geben immer das gleiche Ergebnis zurück" nicht befolgt wird. Tatsächlich wird es mit geändertem internen Status des Arguments ausgeführt, sodass es nicht "wenn das Argument dasselbe ist" ist, aber es besteht die Gefahr, dass dies übersehen wird und ein Fehler verursacht wird, der schwer zu reproduzieren ist.
Nicht alle Nebenwirkungen sind schlecht. (Zumindest in der objektorientierten Programmierung) Unveränderliche Implementierungen ändern ihren Status nicht, können jedoch den Status des im Argument angegebenen variablen Objekts neu schreiben. (Die Methode zum Einrichten des internen Status des als Argument übergebenen Objekts ist eine gängige Technik.)
Bei der Implementierung von Immutable and Stateless ist es erforderlich, die Methode frei von Nebenwirkungen zu machen oder die Nebenwirkungen in Kommentaren anzugeben.
Veränderbare Implementierungen sind mit wenigen Einschränkungen einfach zu erstellen. Wenn die Vorteile die Nachteile überwiegen, können Sie es in die Optionen aufnehmen, aber es wird Sie später glücklich machen, wenn Sie zuerst überlegen, ob es in Immutable implementiert werden kann.
Unveränderliche Implementierungen sind zu restriktiv, um sie unwissentlich zu erstellen, aber Sie können die Vorteile nutzen, die sie verdienen. Auch das Erfolgserlebnis bei erfolgreicher Implementierung ist nur Programmierern ein Vergnügen (lacht). Selbst in der Codeüberprüfung sollten Sie von Ihren Senioren als "dieser Typ, tun Sie es nicht" angesehen werden können. Das größte Problem, das defensive Kopieren, kann durch die Kombination unveränderlicher Implementierungen gemildert werden. Dies ist auch ein Grund, Mutable zu eliminieren.
Die zustandslose Implementierung scheint mit vielen Vorsichtsmaßnahmen anfälliger für Fehler (daher als Anti-Pattern bezeichnet) zu sein. Bei ordnungsgemäßer Handhabung kann es sich jedoch um eine hochgradig wiederverwendbare und nicht verblassende Implementierung handeln. Ich bin.
Es ist auch wichtig zu beachten, dass die Implementierung von Methoden mit Nebenwirkungen, selbst unveränderlichen Objekten, andere veränderbare Objekte beeinflussen kann.
Wenn Sie so weit gelesen haben, denke ich, dass Sie bereits bewusst programmieren können, indem Sie die Wörter Veränderlich (variabel) und Unveränderlich (unveränderlich) lernen. Wenn Sie den Code überprüfen, den Sie bisher geschrieben haben, ist es jetzt nicht besser?
Wenn Sie mehr über Unveränderlichkeit und andere gute Implementierungsmethoden erfahren möchten, die nur Java verwenden, empfehle ich das Buch "Effective Java".
Recommended Posts