Ich verfolge diese Frage , aber ich konzentriere mich von Code auf ein Prinzip.
Nach meinem Verständnis des Liskov-Substitutionsprinzips (LSP) müssen alle Methoden in meiner Basisklasse in meiner Unterklasse implementiert werden, und laut dieser Seite, wenn Sie eine Methode in der Basisklasse überschreiben und sie nichts tut oder eine auslöst Ausnahme, Sie verstoßen gegen das Prinzip.
Nun kann mein Problem folgendermaßen zusammengefasst werden: Ich habe eine Zusammenfassung Weapon
class
und zwei Klassen Sword
und Reloadable
. Wenn es Reloadable
ein bestimmtes method
, genanntes Reload()
Element enthält , müsste ich einen Downcast durchführen, um darauf zuzugreifen method
, und im Idealfall möchten Sie dies vermeiden.
Ich dachte dann daran, das zu benutzen Strategy Pattern
. Auf diese Weise war sich jede Waffe nur der Aktionen bewusst, die sie ausführen kann, so dass beispielsweise eine Reloadable
Waffe offensichtlich nachladen kann, aber eine Sword
nicht und nicht einmal eine Reload class/method
. Wie ich in meinem Beitrag zum Stapelüberlauf angegeben habe, muss ich keinen Downcast durchführen und kann eine List<Weapon>
Sammlung verwalten.
In einem anderen Forum schlug die erste Antwort vor, Sword
sich bewusst zu sein Reload
, einfach nichts zu tun. Dieselbe Antwort wurde auf der Seite "Stapelüberlauf" gegeben, auf die ich oben verlinkt habe.
Ich verstehe nicht ganz warum. Warum gegen das Prinzip verstoßen und Sword erlauben, sich dessen bewusst zu sein Reload
und es leer zu lassen? Wie ich in meinem Beitrag zum Stapelüberlauf sagte, hat der SP meine Probleme so ziemlich gelöst.
Warum ist es keine praktikable Lösung?
public final Weapon{
private final String name;
private final int damage;
private final List<AttackStrategy> validactions;
private final List<Actions> standardActions;
private Weapon(String name, int damage, List<AttackStrategy> standardActions, List<Actions> attacks)
{
this.name = name;
this.damage = damage;
standardActions = new ArrayList<Actions>(standardActions);
validAttacks = new ArrayList<AttackStrategy>(validActions);
}
public void standardAction(String action){} // -- Can call reload or aim here.
public int attack(String action){} // - Call any actions that are attacks.
public static Weapon Sword(String name, damage, List<AttackStrategy> standardActions, List<Actions> attacks){
return new Weapon(name, damage,standardActions, attacks) ;
}
}
Angriffsschnittstelle und Implementierung:
public interface AttackStrategy{
void attack(Enemy enemy);
}
public class Shoot implements AttackStrategy {
public void attack(Enemy enemy){
//code to shoot
}
}
public class Strike implements AttackStrategy {
public void attack(Enemy enemy){
//code to strike
}
}
class Weapon { bool supportsReload(); void reload(); }
. Clients würden vor dem erneuten Laden testen, ob dies unterstützt wird.reload
wird vertraglich definiert, um iff zu werfen!supportsReload()
. Das entspricht dem LSP, wenn getriebene Klassen dem gerade beschriebenen Protokoll entsprechen.reload()
leer lassen oderstandardActions
keine Reload-Aktion enthalten, ist nur ein anderer Mechanismus. Es gibt keinen grundsätzlichen Unterschied. Sie können beides tun. => Ihre Lösung ist realisierbar (was Ihre Frage war).; Sword muss nichts über das Nachladen wissen, wenn Weapon eine leere Standardimplementierung enthält.Antworten:
Der LSP ist besorgt über Subtypisierung und Polymorphismus. Nicht jeder Code verwendet diese Funktionen tatsächlich. In diesem Fall ist der LSP irrelevant. Zwei häufige Anwendungsfälle von Vererbungssprachenkonstrukten, bei denen es sich nicht um Subtypen handelt, sind:
Vererbung, die verwendet wird, um die Implementierung einer Basisklasse zu erben, nicht jedoch deren Schnittstelle. In fast allen Fällen sollte die Zusammensetzung bevorzugt werden. Sprachen wie Java können die Vererbung von Implementierung und Schnittstelle nicht trennen, aber z. B. hat C ++
private
Vererbung.Vererbung zur Modellierung eines Summentyps / einer Summenunion, z. B.: A
Base
ist entwederCaseA
oderCaseB
. Der Basistyp deklariert keine relevante Schnittstelle. Um seine Instanzen zu verwenden, müssen Sie sie auf den richtigen Betontyp umwandeln. Das Casting kann sicher durchgeführt werden und ist nicht das Problem. Leider können viele OOP-Sprachen die Basisklassen-Subtypen nicht nur auf die beabsichtigten Subtypen beschränken. Wenn externer Code a erstellen kannCaseC
, dann Code unter der Annahme, dass aBase
nur a sein kannCaseA
oderCaseB
falsch ist. Scala kann dies mit seinemcase class
Konzept sicher tun . In Java kann dies modelliert werden, wennBase
es sich um eine abstrakte Klasse mit einem privaten Konstruktor handelt und verschachtelte statische Klassen dann von der Basis erben.Einige Konzepte wie konzeptionelle Hierarchien realer Objekte lassen sich sehr schlecht in objektorientierte Modelle abbilden. Gedanken wie „Eine Pistole ist eine Waffe, und ein Schwert ist eine Waffe, daher werde ich eine hat
Weapon
Basisklasse , von derGun
undSword
vererben“ sind irreführend: real-Wort-a - Beziehungen nicht so implizieren eine Beziehung in unserem Modell. Ein verwandtes Problem besteht darin, dass Objekte möglicherweise zu mehreren konzeptionellen Hierarchien gehören oder ihre Hierarchiezugehörigkeit während der Laufzeit ändern, was die meisten Sprachen nicht modellieren können, da die Vererbung normalerweise pro Klasse und nicht pro Objekt erfolgt und zur Entwurfszeit und nicht zur Laufzeit definiert wird.Beim Entwerfen von OOP-Modellen sollten wir nicht an die Hierarchie denken oder daran, wie eine Klasse eine andere „erweitert“. Eine Basisklasse ist kein Ort, um die gemeinsamen Teile mehrerer Klassen herauszufiltern . Überlegen Sie sich stattdessen, wie Ihre Objekte verwendet werden, dh welches Verhalten die Benutzer dieser Objekte benötigen.
Hier müssen Benutzer möglicherweise
attack()
mit Waffen und vielleicht auch mitreload()
ihnen. Wenn wir eine Typhierarchie erstellen möchten, müssen beide Methoden vom Basistyp sein, obwohl nicht nachladbare Waffen diese Methode möglicherweise ignorieren und beim Aufruf nichts tun. Die Basisklasse enthält also nicht die gemeinsamen Teile, sondern die kombinierte Schnittstelle aller Unterklassen. Die Unterklassen unterscheiden sich nicht in ihrer Schnittstelle, sondern nur in der Implementierung dieser Schnittstelle.Es ist nicht erforderlich, eine Hierarchie zu erstellen. Die beiden Typen
Gun
undSword
können völlig unabhängig sein. Während eineGun
Dosefire()
undreload()
eineSword
darf nurstrike()
. Wenn Sie diese Objekte polymorph verwalten müssen, können Sie das Adaptermuster verwenden, um die relevanten Aspekte zu erfassen. In Java 8 ist dies mit funktionalen Schnittstellen und Lambdas / Methodenreferenzen recht bequem möglich.Attack
Zum Beispiel könnten Sie eine Strategie haben, für die Sie liefernmyGun::fire
oder() -> mySword.strike()
.Schließlich ist es manchmal sinnvoll, Unterklassen überhaupt zu vermeiden, aber alle Objekte durch einen einzigen Typ zu modellieren. Dies ist besonders in Spielen relevant, da viele Spielobjekte nicht gut in eine Hierarchie passen und viele verschiedene Funktionen haben können. Zum Beispiel kann ein Rollenspiel einen Gegenstand haben, der sowohl ein Questgegenstand ist als auch Ihre Statistiken mit +2 Stärke verbessert, wenn er ausgerüstet ist, eine 20% ige Chance hat, erlittenen Schaden zu ignorieren, und einen Nahkampfangriff bietet. Oder vielleicht ein nachladbares Schwert, weil es * magisch * ist. Wer weiß, was die Geschichte erfordert.
Anstatt zu versuchen, eine Klassenhierarchie für dieses Durcheinander herauszufinden, ist es besser, eine Klasse zu haben, die Slots für verschiedene Funktionen bereitstellt. Diese Slots können zur Laufzeit geändert werden. Jeder Slot wäre eine Strategie / ein Rückruf wie
OnDamageReceived
oderAttack
. Mit Waffen, so können wir habenMeleeAttack
,RangedAttack
undReload
Slots. Diese Steckplätze können leer sein. In diesem Fall bietet das Objekt diese Funktion nicht. Die Slots werden dann bedingt aufgerufen :if (item.attack != null) item.attack.perform()
.quelle
Weil eine Strategie
attack
für Ihre Bedürfnisse nicht ausreicht. Sicher, es ermöglicht Ihnen zu abstrahieren, welche Aktionen der Gegenstand ausführen kann, aber was passiert, wenn Sie die Reichweite der Waffe kennen müssen? Oder die Munitionskapazität? Oder welche Art von Munition braucht es? Du bist zurück zum Downcasting, um das zu erreichen. Durch diese Flexibilität wird die Implementierung der Benutzeroberfläche etwas schwieriger, da ein ähnliches Strategiemuster erforderlich ist, um alle Funktionen nutzen zu können.Trotzdem stimme ich den Antworten auf Ihre anderen Fragen nicht besonders zu. Das
sword
Erben vonweapon
ist eine schreckliche, naive OO, die ausnahmslos zu No-Op-Methoden oder Typprüfungen führt, die über den Code verstreut sind.Aber an der Wurzel der Sache ist keine der beiden Lösungen falsch . Sie können beide Lösungen verwenden, um ein funktionierendes Spiel zu erstellen, das Spaß macht. Jeder hat seine eigenen Kompromisse, genau wie jede Lösung, die Sie auswählen.
quelle
Weapon
Klasse mit einer Schwert- und Waffeninstanz ist.WeaponBuilder
, die Schwerter und Waffen bauen könnte, indem sie eine Waffe aus Strategien zusammensetzt.Natürlich ist es eine praktikable Lösung; Es ist nur eine sehr schlechte Idee.
Das Problem ist nicht, wenn Sie diese einzelne Instanz haben, in der Sie Ihre Basisklasse neu laden. Das Problem ist, dass Sie auch die "Schaukel", "schießen", "parieren", "klopfen", "polieren", "zerlegen", "schärfen" und "die Nägel des spitzen Endes des Schlägers ersetzen" müssen. Methode auf Ihrer Basisklasse.
Der Punkt von LSP ist, dass Ihre Top-Level-Algorithmen funktionieren und Sinn machen müssen. Also, wenn ich Code wie diesen habe:
Wenn dies eine nicht implementierte Ausnahme auslöst und Ihr Programm zum Absturz bringt, ist dies eine sehr schlechte Idee.
Wenn Ihr Code so aussieht,
dann kann Ihr Code mit sehr spezifischen Eigenschaften überfüllt sein, die nichts mit der abstrakten "Waffen" -Idee zu tun haben.
Wenn Sie jedoch einen Ego-Shooter implementieren und alle Ihre Waffen außer diesem einen Messer schießen / nachladen können, ist es (in Ihrem speziellen Kontext) sehr sinnvoll, das Nachladen Ihres Messers nichts tun zu lassen, da dies die Ausnahme und die Wahrscheinlichkeit ist Es ist gering, wenn Ihre Basisklasse mit bestimmten Eigenschaften überfüllt ist.
Update: Versuchen Sie, über den abstrakten Fall / die abstrakten Begriffe nachzudenken. Zum Beispiel hat vielleicht jede Waffe eine "Vorbereitungs" -Aktion, die ein Nachladen für Waffen und eine Enthüllung für Schwerter ist.
quelle
Natürlich ist es in Ordnung, wenn Sie keine Unterklasse mit der Absicht erstellen, eine Instanz der Basisklasse zu ersetzen, sondern wenn Sie eine Unterklasse erstellen, indem Sie die Basisklasse als praktisches Repository für Funktionen verwenden.
Ob das eine gute Idee ist oder nicht, ist sehr umstritten. Wenn Sie jedoch die Unterklasse niemals durch die Unterklasse ersetzen, ist die Tatsache, dass sie nicht funktioniert, kein Problem. Möglicherweise haben Sie Probleme, aber LSP ist in diesem Fall nicht das Problem.
quelle
Der LSP ist gut, da der aufrufende Code sich keine Gedanken darüber machen muss, wie die Klasse funktioniert.
z.B. Ich kann Weapon.Attack () für alle auf meinem BattleMech montierten Waffen aufrufen und mache mir keine Sorgen, dass einige von ihnen eine Ausnahme auslösen und mein Spiel zum Absturz bringen könnten.
In Ihrem Fall möchten Sie jetzt Ihren Basistyp um neue Funktionen erweitern. Attack () ist kein Problem, da die Gun-Klasse ihre Munition verfolgen und aufhören kann zu schießen, wenn sie leer ist. Aber Reload () ist etwas Neues und gehört nicht dazu, eine Waffe zu sein.
Die einfache Lösung ist Downcasting. Ich glaube nicht, dass Sie sich übermäßig um die Leistung sorgen müssen. Sie werden es nicht in jedem Frame tun.
Alternativ können Sie Ihre Architektur neu bewerten und berücksichtigen, dass abstrakt alle Waffen nachladbar sind und einige Waffen einfach nie nachgeladen werden müssen.
Dann erweitern Sie die Klasse für Waffen nicht mehr oder verletzen den LSP.
Aber es ist auf lange Sicht problematisch, weil Sie sich zwangsläufig mehr Sonderfälle ausdenken müssen, Gun.SafteyOn (), Sword.WipeOffBlood () usw., und wenn Sie sie alle in Weapon einfügen, haben Sie eine super komplizierte verallgemeinerte Basisklasse, die Sie behalten sich ändern müssen.
bearbeiten: warum das Strategiemuster schlecht ist (tm)
Dies ist nicht der Fall, aber berücksichtigen Sie das Setup, die Leistung und den Gesamtcode.
Ich muss irgendwo eine Konfiguration haben, die mir sagt, dass eine Waffe nachladen kann. Wenn ich eine Waffe instanziiere, muss ich diese Konfiguration lesen und alle Methoden dynamisch hinzufügen, überprüfen, ob es keine doppelten Namen usw. Gibt
Wenn ich eine Methode aufrufe, muss ich diese Liste von Aktionen durchlaufen und eine Zeichenfolgenübereinstimmung durchführen, um zu sehen, welche aufgerufen werden sollen.
Wenn ich den Code kompiliere und Weapon.Do ("atack") anstelle von "attack" aufrufe, wird beim Kompilieren kein Fehler angezeigt.
Es kann eine geeignete Lösung für einige Probleme sein, zum Beispiel, dass Sie Hunderte von Waffen mit unterschiedlichen Kombinationen von Zufallsmethoden haben, aber Sie verlieren viele Vorteile von OO und starkem Tippen. Es erspart Ihnen nicht wirklich etwas über Downcasting
quelle
SafteyOn()
undSword
hättewipeOffBlood()
. Jede Waffe kennt die anderen Methoden nicht (und sollte es auch nicht sein)weapon.do("attack")
und der Typ-Safeweapon.attack.perform()
können Beispiele für das Strategiemuster sein. Das Nachschlagen von Strategien nach Namen ist nur erforderlich, wenn das Objekt aus einer Konfigurationsdatei konfiguriert wird. Die Verwendung von Reflection wäre jedoch ebenso typsicher.