In dieser Reihe von Blog-Posts beschreibt Eric Lippert ein Problem im objektorientierten Design am Beispiel von Zauberern und Kriegern.
abstract class Weapon { }
sealed class Staff : Weapon { }
sealed class Sword : Weapon { }
abstract class Player
{
public Weapon Weapon { get; set; }
}
sealed class Wizard : Player { }
sealed class Warrior : Player { }
und fügt dann ein paar Regeln hinzu:
- Ein Krieger kann nur ein Schwert benutzen.
- Ein Assistent kann nur einen Stab verwenden.
Anschließend demonstriert er die Probleme, auf die Sie stoßen, wenn Sie versuchen, diese Regeln mithilfe des C # -Systems durchzusetzen (z. B. indem Sie die Wizard
Klasse dafür verantwortlich machen, dass ein Assistent nur einen Stab verwenden kann). Sie verstoßen gegen das Liskov-Substitutionsprinzip, riskieren Laufzeitausnahmen oder erhalten Code, der schwer zu erweitern ist.
Die Lösung, die er findet, ist, dass die Player-Klasse keine Validierung durchführt. Es wird nur zum Verfolgen des Status verwendet. Dann, anstatt einem Spieler eine Waffe zu geben durch:
player.Weapon = new Sword();
Zustand wird durch Command
s und gemäß Rule
s geändert :
... wir machen ein
Command
Objekt namensWield
, das zwei Spielstatusobjekte nimmt, aPlayer
und aWeapon
. Wenn der Benutzer einen Befehl an das System ausgibt, "dieser Assistent sollte dieses Schwert führen", wird dieser Befehl im Kontext einer Menge vonRule
s ausgewertet , die eine Folge vonEffect
s erzeugt. Wir haben eineRule
, die besagt, dass, wenn ein Spieler versucht, eine Waffe einzusetzen, die vorhandene Waffe, falls vorhanden, fallen gelassen wird und die neue Waffe zur Waffe des Spielers wird. Wir haben eine andere Regel, die die erste Regel stärkt, die besagt, dass die Auswirkungen der ersten Regel nicht gelten, wenn ein Zauberer versucht, ein Schwert zu führen.
Ich mag diese Idee im Prinzip, habe aber Bedenken, wie sie in der Praxis angewendet werden könnte.
Nichts scheint einen Entwickler daran zu hindern, das Commands
und Rule
s zu umgehen, indem er einfach das Weapon
auf a setzt Player
. Die Weapon
Eigenschaft muss über den Wield
Befehl zugänglich sein , damit sie nicht erstellt werden kann private set
.
Was hindert einen Entwickler daran? Müssen sie sich nur daran erinnern, es nicht zu tun?
quelle
Antworten:
Das ganze Argument, zu dem eine Reihe von Blog-Posts führt, ist in Teil 5 :
Waffen, Charaktere, Monster und andere Spielobjekte sind nicht dafür verantwortlich zu überprüfen, was sie können oder nicht können. Dafür ist das Regelsystem verantwortlich. Das
Command
Objekt macht auch nichts mit den Spielobjekten. Es ist nur der Versuch , etwas mit ihnen zu tun. Das Regelsystem prüft dann, ob der Befehl möglich ist, und wenn es das Regelsystem ist, führt es den Befehl aus, indem es die entsprechenden Methoden für die Spielobjekte aufruft.Wenn ein Entwickler ein zweites Regelsystem erstellen möchte, das Dinge mit Charakteren und Waffen tut, die das erste Regelsystem nicht zulässt, kann er dies tun, da Sie in C # (ohne böse Reflexionshacks) nicht herausfinden können, woher ein Methodenaufruf kommt von.
Eine Abhilfe , die möglicherweise in Arbeit einigen Situationen setzt die Spielobjekte (oder deren Schnittstellen) in einer Baugruppe mit der Regelmaschine und markieren Sie alle Mutator-Methoden
internal
. Alle Systeme, die schreibgeschützten Zugriff auf Spielobjekte benötigen, befinden sich in einer anderen Baugruppe, dh sie können nur auf diepublic
Methoden zugreifen . Dies lässt immer noch die Lücke von Spielobjekten, die sich gegenseitig die internen Methoden aufrufen. Aber das wäre ein offensichtlicher Codegeruch, denn Sie waren sich einig, dass die Spielobjektklassen dumme Staatsinhaber sein sollen.quelle
Das offensichtliche Problem des ursprünglichen Codes besteht darin, dass Datenmodellierung anstelle von Objektmodellierung durchgeführt wird . Bitte beachten Sie, dass die tatsächlichen Geschäftsanforderungen im verlinkten Artikel absolut nicht erwähnt werden!
Ich würde zunächst versuchen, die tatsächlichen funktionalen Anforderungen zu ermitteln. Zum Beispiel: "Jeder Spieler kann jeden anderen Spieler angreifen, ...". Hier:
"Spieler können eine Waffe einsetzen, die für den Angriff verwendet wird, Zauberer können einen Stab einsetzen, Krieger ein Schwert":
"Jede Waffe fügt dem angegriffenen Feind Schaden zu". Ok, jetzt müssen wir eine gemeinsame Schnittstelle für Weapon haben:
Und so weiter ... Warum gibt es keine
Wield()
in derPlayer
? Weil es nicht erforderlich war, dass ein Spieler eine Waffe einsetzen kann.Ich kann mir vorstellen, dass es eine Anforderung geben würde, die besagt: "Jeder
Player
kann versuchen , einen zu führenWeapon
." Dies wäre jedoch eine ganz andere Sache. Ich würde es vielleicht so modellieren:Zusammenfassung: Modellieren Sie die Anforderungen und nur die Anforderungen. Führen Sie keine Datenmodellierung durch, dh keine Modellierung.
quelle
Eine Möglichkeit wäre, den
Wield
Befehl an die zu übergebenPlayer
. Der Spieler führt dann denWield
Befehl aus, der die entsprechenden Regeln überprüft und den zurückgibtWeapon
, mit dem erPlayer
dann sein eigenes Waffenfeld setzt. Auf diese Weise kann das Waffenfeld einen privaten Setter haben und kann nur durch Übergabe einesWield
Befehls an den Spieler eingestellt werden.quelle
Nichts hindert den Entwickler daran. Eigentlich hat Eric Lippert viele verschiedene Techniken ausprobiert, aber alle hatten Schwächen. Das war der springende Punkt dieser Serie, dass es nicht einfach ist, den Entwickler daran zu hindern, und alles, was er versuchte, hatte Nachteile. Schließlich entschied er, dass die Verwendung eines
Command
Objekts mit Regeln der richtige Weg ist.Mit Regeln können Sie die
Weapon
Eigenschaft von aWizard
auf a setzen,Sword
aber wenn Sie den bittenWizard
, die Waffe (Schwert) zu führen und anzugreifen, hat dies keine Wirkung und ändert daher keinen Zustand. Wie er unten sagt:Mit anderen Worten, wir können eine solche Regel nicht durch
type
Beziehungen durchsetzen, die er auf viele verschiedene Arten ausprobiert hat, aber entweder nicht mochte oder nicht funktionierte. Das Einzige, was er tun kann, ist, zur Laufzeit etwas dagegen zu unternehmen. Eine Ausnahme zu werfen war nicht gut, weil er es nicht als Ausnahme betrachtet.Er entschied sich schließlich für die obige Lösung. Diese Lösung besagt im Grunde, dass Sie jede Waffe einstellen können, aber wenn Sie sie abgeben, wenn nicht die richtige Waffe, wäre sie im Wesentlichen nutzlos. Aber es würde keine Ausnahme geworfen.
Ich denke, es ist eine gute Lösung. In einigen Fällen würde ich mich aber auch für das Try-Set-Muster entscheiden.
quelle
This solution basically says you can set any weapon but when you yield it, if not the right weapon, it would be essentially useless.
Ich habe es in dieser Serie nicht gefunden. Können Sie mir bitte zeigen, wo diese Lösung vorgeschlagen wird?that the existing weapon, if there is one, is dropped and the new weapon becomes the player’s weapon
. Während die zweite Regel, diethat strengthens the first rule, that says that the first rule’s effects do not apply when a wizard tries to wield a sword.
ich denke, dass es eine Regel gibt, die prüft, ob die Waffe ein Schwert ist, kann sie nicht von einem Zauberer geführt werden, so dass sie nicht festgelegt ist. Stattdessen ertönt eine traurige Posaune.Wield
hier interpretiert . Ich denke, es ist ein etwas irreführender Name für den Befehl. So etwasChangeWeapon
wäre genauer. Ich denke, Sie könnten ein anderes Modell haben, in dem Sie jede Waffe einstellen können, aber wenn Sie sie abgeben, wenn sie nicht die richtige Waffe ist, wäre sie im Wesentlichen nutzlos . Das klingt interessant, aber ich glaube nicht, dass Eric Lippert es beschreibt.Die erste verworfene Lösung des Autors bestand darin, die Regeln durch das Typsystem darzustellen. Das Typsystem wird zur Kompilierungszeit ausgewertet. Wenn Sie die Regeln vom Typsystem trennen, werden sie vom Compiler nicht mehr überprüft, sodass nichts einen Entwickler daran hindert, per se einen Fehler zu machen.
Dieses Problem tritt jedoch bei jeder Logik / Modellierung auf, die nicht vom Compiler überprüft wird, und die allgemeine Antwort darauf lautet (Einheits-) Test. Daher benötigt die vom Autor vorgeschlagene Lösung ein starkes Testgeschirr, um Fehler von Entwicklern zu umgehen. Um diesen Punkt zu unterstreichen, dass ein starkes Test-Harness für Fehler erforderlich ist, die nur zur Laufzeit erkannt werden, lesen Sie diesen Artikel von Bruce Eckel, in dem argumentiert wird, dass Sie die starke Typisierung gegen stärkere Tests in dynamischen Sprachen austauschen müssen.
Zusammenfassend lässt sich sagen, dass Entwickler nur durch eine Reihe von (Einheits-) Tests verhindern können, dass alle Regeln eingehalten werden.
quelle
Ich habe hier vielleicht eine Subtilität verpasst, bin mir aber nicht sicher, ob das Problem beim Typsystem liegt. Vielleicht ist es mit Konvention in C #.
Sie können diesen Typ beispielsweise vollständig sicher machen, indem Sie den
Weapon
Setter in schützenPlayer
. Dann fügensetSword(Sword)
undsetStaff(Staff)
zuWarrior
undWizard
jeweils diesen Ruf der geschützten Setter.Auf diese Weise wird die Beziehung
Player
/Weapon
statisch überprüft und Code, der sich nicht darum kümmert, kann einfach a verwendenPlayer
, um a zu erhaltenWeapon
.quelle
Weapon
zu a hinzuzufügenPlayer
. Es gibt jedoch kein Typsystem, bei dem Sie die konkreten Typen zur Kompilierungszeit nicht kennen, die zur Kompilierungszeit auf diese konkreten Typen einwirken können. Per Definition. Dieses Schema bedeutet, dass nur dieser Fall zur Laufzeit behandelt werden muss. Als solches ist es tatsächlich besser als jedes von Erics Schemata.Diese Frage ist praktisch dieselbe wie bei einem ziemlich heilig-kriegerischen Thema namens " Wo soll die Validierung erfolgen? " (Höchstwahrscheinlich auch ddd).
Bevor Sie diese Frage beantworten, sollten Sie sich fragen: Welche Art von Regeln möchten Sie befolgen? Sind sie in Stein gemeißelt und definieren die Entität? Führt der Verstoß gegen diese Regeln dazu, dass eine Entität nicht mehr das ist, was sie ist? Wenn ja, fügen Sie diese Regeln neben der Befehlsüberprüfung auch einer Entität hinzu. Wenn ein Entwickler vergisst, den Befehl zu validieren, befinden sich Ihre Entitäten nicht in einem ungültigen Zustand.
Wenn nein, bedeutet dies von Natur aus, dass diese Regeln befehlsspezifisch sind und sich nicht in Domänenentitäten befinden sollten. Ein Verstoß gegen diese Regeln führt zu Aktionen, die nicht hätten ausgeführt werden dürfen, jedoch nicht in einem ungültigen Modellstatus.
quelle