Ich habe daran gearbeitet, Code zu überarbeiten, und ich glaube, ich habe den ersten Schritt im Kaninchenbau getan. Ich schreibe das Beispiel in Java, aber ich nehme an, es könnte agnostisch sein.
Ich habe eine Schnittstelle Foo
definiert als
public interface Foo {
int getX();
int getY();
int getZ();
}
Und eine Implementierung als
public final class DefaultFoo implements Foo {
public DefaultFoo(int x, int y, int z) {
this.x = x;
this.y = y;
this.z = z;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getZ() {
return z;
}
private final int x;
private final int y;
private final int z;
}
Ich habe auch eine Schnittstelle MutableFoo
, die passende Mutatoren bietet
/**
* This class extends Foo, because a 'write-only' instance should not
* be possible and a bit counter-intuitive.
*/
public interface MutableFoo extends Foo {
void setX(int newX);
void setY(int newY);
void setZ(int newZ);
}
Es gibt einige Implementierungen davon MutableFoo
(ich habe sie noch nicht implementiert). Einer von ihnen ist
public final class DefaultMutableFoo implements MutableFoo {
/**
* A DefaultMutableFoo is not conceptually constructed
* without all values being set.
*/
public DefaultMutableFoo(int x, int y, int z) {
this.x = x;
this.y = y;
this.z = z;
}
public int getX() {
return x;
}
public void setX(int newX) {
this.x = newX;
}
public int getY() {
return y;
}
public void setY(int newY) {
this.y = newY;
}
public int getZ() {
return z;
}
public void setZ(int newZ) {
this.z = newZ;
}
private int x;
private int y;
private int z;
}
Der Grund, warum ich diese aufgeteilt habe, ist, dass es für jeden gleich wahrscheinlich ist, dass sie verwendet werden. Das heißt, es ist ebenso wahrscheinlich, dass jemand, der diese Klassen verwendet, eine unveränderliche Instanz wünscht, wie es ist, dass er eine veränderbare Instanz will.
Der primäre Anwendungsfall, den ich habe, ist eine Schnittstelle namens StatSet
, die bestimmte Kampfdetails für ein Spiel darstellt (Trefferpunkte, Angriff, Verteidigung). Die "effektiven" Statistiken oder die tatsächlichen Statistiken sind jedoch ein Ergebnis der Basisstatistiken, die niemals geändert werden können, und der trainierten Statistiken, die erhöht werden können. Diese beiden sind verwandt mit
/**
* The EffectiveStats can never be modified independently of either the baseStats
* or trained stats. As such, this StatSet must never provide mutators.
*/
public StatSet calculateEffectiveStats() {
int effectiveHitpoints =
baseStats.getHitpoints() + (trainedStats.getHitpoints() / 4);
int effectiveAttack =
baseStats.getAttack() + (trainedStats.getAttack() / 4);
int effectiveDefense =
baseStats.getDefense() + (trainedStats.getDefense() / 4);
return StatSetFactory.createImmutableStatSet(effectiveHitpoints, effectiveAttack, effectiveDefense);
}
Die trainierten Statistiken werden nach jedem Kampf wie erhöht
public void grantExperience() {
int hitpointsReward = 0;
int attackReward = 0;
int defenseReward = 0;
final StatSet enemyStats = enemy.getEffectiveStats();
final StatSet currentStats = player.getEffectiveStats();
if (enemyStats.getHitpoints() >= currentStats.getHitpoints()) {
hitpointsReward++;
}
if (enemyStats.getAttack() >= currentStats.getAttack()) {
attackReward++;
}
if (enemyStats.getDefense() >= currentStats.getDefense()) {
defenseReward++;
}
final MutableStatSet trainedStats = player.getTrainedStats();
trainedStats.increaseHitpoints(hitpointsReward);
trainedStats.increaseAttack(attackReward);
trainedStats.increaseDefense(defenseReward);
}
aber sie werden nicht gleich nach dem Kampf erhöht. Wenn Sie bestimmte Gegenstände verwenden, bestimmte Taktiken anwenden und das Schlachtfeld geschickt einsetzen, können Sie unterschiedliche Erfahrungen sammeln.
Nun zu meinen Fragen:
- Gibt es einen Namen für die Aufteilung von Schnittstellen durch Accessoren und Mutatoren in separate Schnittstellen?
- Ist Splitting sie auf diese Weise der ‚richtige‘ Ansatz , wenn sie gleich wahrscheinlich sind verwendet werden, oder gibt es ein anderes, akzeptierten Muster , dass ich stattdessen verwenden sollte (z. B. ,
Foo foo = FooFactory.createImmutableFoo();
die zurückkehren könnteDefaultFoo
oderDefaultMutableFoo
aber weil verstecktcreateImmutableFoo
kehrtFoo
)? - Gibt es unmittelbar vorhersehbare Nachteile bei der Verwendung dieses Musters, abgesehen von der Komplikation der Schnittstellenhierarchie?
Der Grund, warum ich angefangen habe, es so zu entwerfen, ist, dass ich der Meinung bin, dass alle Implementierer einer Schnittstelle die einfachste mögliche Schnittstelle einhalten und nichts mehr bereitstellen sollten. Durch Hinzufügen von Setzern zur Benutzeroberfläche können jetzt die effektiven Statistiken unabhängig von ihren Teilen geändert werden.
Das Erstellen einer neuen Klasse für das EffectiveStatSet
macht wenig Sinn, da wir die Funktionalität in keiner Weise erweitern. Wir könnten die Implementierung ändern und EffectiveStatSet
eine Mischung aus zwei verschiedenen erstellen StatSets
, aber ich denke, das ist nicht die richtige Lösung.
public class EffectiveStatSet implements StatSet {
public EffectiveStatSet(StatSet baseStats, StatSet trainedStats) {
// ...
}
public int getHitpoints() {
return baseStats.getHitpoints() + (trainedStats.getHitpoints() / 4);
}
}
Antworten:
Mir scheint, Sie haben eine Lösung, die nach einem Problem sucht.
Das mag ein wenig provozieren, aber eigentlich würde ich es "Überdesign" oder "Überkomplizieren" nennen. Indem Sie eine veränderbare und eine unveränderliche Variante derselben Klasse anbieten, bieten Sie zwei funktional äquivalente Lösungen für dasselbe Problem, die sich nur in nicht funktionalen Aspekten wie Leistungsverhalten, API und Sicherheit gegen Nebenwirkungen unterscheiden. Ich denke, das liegt daran, dass Sie Angst haben, eine Entscheidung zu treffen, die Sie bevorzugen, oder dass Sie versuchen, die "const" -Funktion von C ++ in C # zu implementieren. Ich denke, in 99% aller Fälle wird es keinen großen Unterschied machen, ob der Benutzer die veränderliche oder die unveränderliche Variante auswählt, er kann seine Probleme mit der einen oder der anderen lösen. Also "Wahrscheinlichkeit einer Klasse zu verwenden"
Die Ausnahme ist, wenn Sie eine neue Programmiersprache oder ein Mehrzweck-Framework entwerfen, das von etwa zehntausenden Programmierern oder mehr verwendet wird. Dann kann es tatsächlich besser skaliert werden, wenn Sie unveränderliche und veränderbare Varianten von Allzweckdatentypen anbieten. Aber dies ist eine Situation, in der Sie Tausende verschiedener Nutzungsszenarien haben werden - was ist wahrscheinlich nicht das Problem, mit dem Sie konfrontiert sind, denke ich?
Das "akzeptiertere Muster" heißt KISS - halte es einfach und dumm. Treffen Sie eine Entscheidung für oder gegen die Veränderbarkeit für die bestimmte Klasse / Schnittstelle in Ihrer Bibliothek. Wenn Ihr "StatSet" beispielsweise ein Dutzend oder mehr Attribute enthält und diese meist einzeln geändert werden, würde ich die veränderbare Variante bevorzugen und die Basisstatistiken nur nicht ändern, wenn sie nicht geändert werden sollten. Für so etwas wie eine
Foo
Klasse mit den Attributen X, Y, Z (ein dreidimensionaler Vektor) würde ich wahrscheinlich die unveränderliche Variante bevorzugen.Überkomplizierte Designs erschweren das Testen, die Wartung und die Weiterentwicklung von Software.
quelle
Es könnte einen Namen dafür geben, wenn diese Trennung nützlich ist und einen Vorteil bietet , den ich nicht sehe . Wenn die Separatons keinen Nutzen bringen, ergeben die beiden anderen Fragen keinen Sinn.
Können Sie uns einen geschäftlichen Anwendungsfall nennen, bei dem die beiden separaten Schnittstellen uns Vorteile bringen, oder handelt es sich bei dieser Frage um ein akademisches Problem (YAGNI)?
Ich kann mir vorstellen, mit einem veränderlichen Einkaufswagen (Sie können mehr Artikel einlegen) einzukaufen, der zu einer Bestellung werden kann, bei der die Artikel vom Kunden nicht mehr geändert werden können. Der Auftragsstatus ist weiterhin veränderbar.
Implementierungen, die ich gesehen habe, trennen die Schnittstellen nicht
Die ReadOnly-Version verwendet das "WritableInterface" und löst eine Ausnahme aus, wenn eine Schreibmethode verwendet wird
quelle
Die Trennung einer veränderlichen Erfassungsschnittstelle und einer schreibgeschützten Erfassungsschnittstelle ist ein Beispiel für das Prinzip der Schnittstellentrennung. Ich glaube nicht, dass jeder Anwendung des Prinzips spezielle Namen gegeben werden.
Beachten Sie hier einige Worte: "Nur-Lese-Vertrag" und "Sammlung".
Ein schreibgeschützter Vertrag bedeutet, dass die Klasse
A
der KlasseB
einen schreibgeschützten Zugriff gewährt , jedoch nicht impliziert, dass das zugrunde liegende Sammlungsobjekt tatsächlich unveränderlich ist. Unveränderlich bedeutet, dass es sich in Zukunft niemals ändern wird, egal von welchem Agenten. Ein schreibgeschützter Vertrag besagt lediglich, dass der Empfänger ihn nicht ändern darf. jemand anderes (insbesondere KlasseA
) darf es ändern.Um ein Objekt unveränderlich zu machen, muss es wirklich unveränderlich sein - es muss Versuche ablehnen, seine Daten zu ändern, unabhängig davon, welcher Agent es anfordert.
Das Muster wird höchstwahrscheinlich bei Objekten beobachtet, die eine Sammlung von Datenlisten, Sequenzen, Dateien (Streams) usw. darstellen.
Das Wort "unveränderlich" wird zur Modeerscheinung, aber das Konzept ist nicht neu. Und es gibt viele Möglichkeiten, Unveränderlichkeit zu nutzen, sowie viele Möglichkeiten, mit etwas anderem (dh seinen Konkurrenten) ein besseres Design zu erzielen.
Hier ist mein Ansatz (nicht basierend auf Unveränderlichkeit).
hitpoints
,attack
,defense
.A
ein DTO an die Klasse übergeben mussB
, erstellt sie eine Kopie davon und übergibt stattdessen die Kopie. SieB
können das DTO also nach Belieben verwenden (darauf schreiben), ohne das DTO zu beeinflussen, an dem esA
festhält.grantExperience
in zwei Teile zu unterteilende Funktion:calculateNewStats
increaseStats
calculateNewStats
nimmt die Eingaben von zwei DTO, von denen einer die Spielerstatistiken und einer die feindlichen Statistiken darstellt, und führt die Berechnungen durch.hitpoints
,attack
,defense
) speichert die Menge erhöht werden für diese Fähigkeit.increaseStats
ist eine Methode für den Spieler (nicht für das DTO), die ein DTO "Betrag zum Inkrementieren" benötigt und dieses Inkrement auf das DTO anwendet, das dem Spieler gehört und das trainierbare DTO des Spielers darstellt.Falls
calculateNewStats
festgestellt wird, dass keine Informationen von anderen Spielern oder Feinden abhängen (über die Werte im DTO mit zwei Eingaben hinaus), kann sich diese Methode möglicherweise an einer beliebigen Stelle im Projekt befinden.Wenn
calculateNewStats
festgestellt wird, dass das Objekt vollständig von den Objekten des Spielers und des Feindes abhängig ist (dh zukünftige Objekte des Spielers und des Feindes haben möglicherweise neue Eigenschaften undcalculateNewStats
müssen aktualisiert werden, um so viel wie möglich von ihren neuen Eigenschaften zu verbrauchen),calculateNewStats
müssen diese beiden akzeptiert werden Objekte, nicht nur das DTO. Das Berechnungsergebnis ist jedoch weiterhin das Inkrement-DTO oder ein einfaches Datenübertragungsobjekt, das die Informationen enthält, die zum Ausführen eines Inkrements / Upgrades verwendet werden.quelle
Hier gibt es ein großes Problem: Nur weil eine Datenstruktur unveränderlich ist, heißt das nicht, dass wir keine modifizierten Versionen davon benötigen . Die wirkliche unveränderliche Version einer Datenstruktur bereitstellt
setX
,setY
undsetZ
Methoden - sie nur eine Rückkehr neue Struktur stattdessen das Objekt modifizieren Sie sie auf genannt.Wie können Sie einem Teil Ihres Systems die Möglichkeit geben, ihn zu ändern und gleichzeitig andere Teile einzuschränken? Mit einem veränderlichen Objekt, das einen Verweis auf eine Instanz der unveränderlichen Struktur enthält. Grundsätzlich statt Ihrer Spielerklasse der Lage, das Statistik - Objekt zu mutieren , während jeder zweiten Klasse einen unveränderlichen Blick auf der Statistik zu geben, seine Statistiken sind unveränderlich und der Spieler ist wandelbar. Anstatt von:
Sie hätten:
Sieh den Unterschied? Das Statistikobjekt ist vollständig unveränderlich, aber die aktuellen Statistiken des Spielers sind eine veränderbare Referenz auf das unveränderliche Objekt. Sie müssen überhaupt keine zwei Schnittstellen erstellen - machen Sie einfach die Datenstruktur vollständig unveränderlich und verwalten Sie einen veränderlichen Verweis darauf, wo Sie ihn gerade zum Speichern des Status verwenden.
Dies bedeutet, dass andere Objekte in Ihrem System sich nicht auf einen Verweis auf das Statistikobjekt verlassen können. Das Statistikobjekt, das sie haben, ist nicht dasselbe Statistikobjekt, das der Spieler hat, nachdem der Spieler seine Statistiken aktualisiert hat.
Dies ist ohnehin sinnvoller, da sich konzeptionell nicht wirklich die Statistiken ändern, sondern die aktuellen Statistiken des Spielers . Nicht das Statistikobjekt. Der Verweis auf das Statistikobjekt, über das das Spielerobjekt verfügt. Andere Teile Ihres Systems, die von dieser Referenz abhängen, sollten alle explizit referenzieren
player.currentStats()
oder solche, anstatt das zugrunde liegende Statistikobjekt des Spielers in den Griff zu bekommen, es irgendwo zu speichern und sich darauf zu verlassen, dass es durch Mutationen aktualisiert wird.quelle
Wow ... das bringt mich wirklich zurück. Ich habe die gleiche Idee ein paar Mal versucht. Ich war zu stur, um es aufzugeben, weil ich dachte, es gäbe etwas Gutes zu lernen. Ich habe andere gesehen, die dies auch im Code versucht haben. Die meisten Implementierungen, die ich gesehen habe, werden als schreibgeschützte Schnittstellen wie Ihre
FooViewer
oderReadOnlyFoo
und die schreibgeschützte Schnittstelle alsFooEditor
oderWriteableFoo
oder in Java bezeichnet. Ich glaube, ich habe sieFooMutator
einmal gesehen. Ich bin mir nicht sicher, ob es ein offizielles oder sogar allgemeines Vokabular gibt, um Dinge auf diese Weise zu tun.Dies hat mir an den Orten, an denen ich es ausprobiert habe, nie etwas Nützliches gebracht. Ich würde es ganz vermeiden. Ich würde tun, was andere vorschlagen, und einen Schritt zurücktreten und überlegen, ob Sie diesen Begriff wirklich in Ihrer größeren Idee brauchen. Ich bin mir nicht sicher, ob es einen richtigen Weg gibt, da ich nie den Code behalten habe, den ich während des Versuchs erstellt habe. Jedes Mal zog ich mich nach erheblichen Anstrengungen zurück und vereinfachte die Dinge. Und damit meine ich etwas in der Art, wie andere über YAGNI, KISS und DRY gesagt haben.
Ein möglicher Nachteil: Wiederholung. Sie müssen diese Schnittstellen möglicherweise für viele Klassen erstellen, und selbst für nur eine Klasse müssen Sie jede Methode benennen und jede Signatur mindestens zweimal beschreiben. Von all den Dingen, die mich beim Codieren verbrennen, bringt es mich am meisten in Schwierigkeiten, mehrere Dateien auf diese Weise ändern zu müssen. Am Ende vergesse ich schließlich, die Änderungen an dem einen oder anderen Ort vorzunehmen. Sobald Sie eine Klasse namens Foo und Schnittstellen namens FooViewer und FooEditor haben, müssen Sie, wenn Sie entscheiden, dass es besser ist, wenn Sie sie Bar nennen, stattdessen dreimal umgestalten-> umbenennen, es sei denn, Sie haben eine wirklich erstaunlich intelligente IDE. Ich fand das sogar so, als ich einen beträchtlichen Teil des Codes von einem Codegenerator hatte.
Persönlich mag ich es nicht, Schnittstellen für Dinge zu erstellen, die auch nur eine einzige Implementierung haben. Wenn ich im Code herumreise, kann ich nicht einfach direkt zur einzigen Implementierung springen, sondern muss zur Schnittstelle und dann zur Implementierung der Schnittstelle springen oder zumindest ein paar zusätzliche Tasten drücken, um dorthin zu gelangen. Diese Dinge summieren sich.
Und dann ist da noch die Komplikation, die Sie erwähnt haben. Ich bezweifle, dass ich mit meinem Code jemals wieder diesen bestimmten Weg gehen werde. Selbst für den Ort, an dem dies am besten in meinen Gesamtplan für den Code (ein von mir erstelltes ORM) passt, habe ich dies gezogen und durch etwas ersetzt, mit dem ich leichter codieren kann.
Ich würde wirklich mehr über die von Ihnen erwähnte Komposition nachdenken. Ich bin gespannt, warum Sie das für eine schlechte Idee halten. Ich hätte erwartet,
EffectiveStats
komponiert und unveränderlich zu sein,Stats
und so etwasStatModifiers
würde einige Sätze vonStatModifier
s weiter komponieren , die alles darstellen, was Statistiken ändern könnte (temporäre Effekte, Gegenstände, Position in einem Verbesserungsbereich, Müdigkeit), aber dasEffectiveStats
müssten Sie nicht verstehen, weilStatModifiers
würde Verwalten Sie, was diese Dinge waren und wie viel Wirkung und welche Art sie auf welche Statistiken haben würden. DasStatModifier
wäre eine Schnittstelle für die verschiedenen Dinge und könnte Dinge wie "Bin ich in der Zone", "Wann lässt die Medizin nach" usw. wissen, müsste aber nicht einmal andere Objekte über solche Dinge informieren. Es müsste nur gesagt werden, welcher Status und wie modifiziert er gerade war. Besser nochStatModifier
könnte man einfach eine Methode zur Herstellung einer neuen unveränderlichenStats
auf der Grundlage einer anderenStats
aufdecken, die anders wäre, weil sie entsprechend geändert worden wäre. Sie könnten dann so etwas tuncurrentStats = statModifier2.modify(statModifier1.modify(baseStats))
und allesStats
kann unveränderlich sein. Ich würde das nicht einmal direkt codieren, sondern wahrscheinlich alle Modifikatoren durchlaufen und sie jeweils auf das Ergebnis der vorherigen Modifikatoren anwenden, beginnend mitbaseStats
.quelle