Zwei Schnittstellen mit identischen Signaturen

13

Ich versuche ein Kartenspiel zu modellieren, bei dem Karten zwei wichtige Funktionen haben:

Der erste ist ein Effekt. Dies sind die Änderungen am Spielstatus, die auftreten, wenn Sie die Karte spielen. Die Schnittstelle für den Effekt ist wie folgt:

boolean isPlayable(Player p, GameState gs);
void play(Player p, GameState gs);

Und Sie könnten die Karte nur dann als spielbar ansehen, wenn Sie ihre Kosten decken können und alle ihre Effekte spielbar sind. Wie so:

// in Card class
boolean isPlayable(Player p, GameState gs) {
    if(p.resource < this.cost) return false;
    for(Effect e : this.effects) {
        if(!e.isPlayable(p,gs)) return false;
    }
    return true;
}

Okay, soweit ganz einfach.

Die anderen Funktionen der Karte sind Fähigkeiten. Diese Fähigkeiten sind Änderungen des Spielstatus, die Sie nach Belieben aktivieren können. Als ich die Schnittstelle für diese erstellte, stellte ich fest, dass sie eine Methode zum Bestimmen, ob sie aktiviert werden können oder nicht, und eine Methode zum Implementieren der Aktivierung benötigten. Es endet zu sein

boolean isActivatable(Player p, GameState gs);
void activate(Player p, GameState gs);

Und mir ist klar, dass ich mit der Ausnahme, dass ich es "aktivieren" anstatt "spielen" nenne Abilityund Effectgenau die gleiche Signatur habe.


Ist es eine schlechte Sache, mehrere Schnittstellen mit einer identischen Signatur zu haben? Sollte ich einfach eine verwenden und zwei Sätze der gleichen Schnittstelle haben? Wie so:

Set<Effect> effects;
Set<Effect> abilities;

Wenn ja, welche Refactoring- Schritte sollte ich unternehmen, wenn sie nicht mehr identisch sind (da mehr Features veröffentlicht werden), insbesondere wenn sie divergent sind (dh, beide erhalten etwas, was der andere nicht sollte, im Gegensatz zu nur einem Gewinn) und die andere ist eine vollständige Teilmenge)? Ich bin besonders besorgt, dass das Kombinieren nicht nachhaltig sein wird, sobald sich etwas ändert.

Das Kleingedruckte:

Ich bin mir bewusst, dass diese Frage von der Spieleentwicklung ausgeht, aber ich denke, dass diese Art von Problem bei der Entwicklung außerhalb des Spiels genauso leicht auftreten kann, insbesondere, wenn versucht wird, die Geschäftsmodelle mehrerer Clients in einer Anwendung unterzubringen, wie dies bei nahezu der Fall ist Jedes Projekt, das ich jemals mit mehr als einem geschäftlichen Einfluss durchgeführt habe ... Auch die verwendeten Snippets sind Java-Snippets, aber dies könnte genauso gut auf eine Vielzahl objektorientierter Sprachen zutreffen.

corsiKa
quelle
Folgen Sie einfach KISS und YAGNI und es sollte Ihnen gut gehen.
Bernard
2
Der einzige Grund, warum diese Frage überhaupt auftaucht, ist, dass Ihre Funktionen aufgrund der unglaublich breiten und uneingeschränkten Player- und GameState-Parameter auf viel zu viele Status zugreifen können.
Lars Viklund

Antworten:

18

Nur weil zwei Schnittstellen den gleichen Vertrag haben, heißt das nicht, dass sie die gleiche Schnittstelle sind.

Das Liskov-Substitutionsprinzip lautet:

Sei q (x) eine Eigenschaft, die für Objekte x vom Typ T beweisbar ist. Dann sollte q (y) für Objekte y vom Typ S beweisbar sein, wobei S ein Subtyp von T ist.

Oder mit anderen Worten: Alles, was für eine Schnittstelle oder einen Supertyp zutrifft, sollte für alle Subtypen zutrifft.

Wenn ich Ihre Beschreibung richtig verstehe, ist eine Fähigkeit kein Effekt und ein Effekt ist keine Fähigkeit. Sollte einer seinen Vertrag ändern, ist es unwahrscheinlich, dass der andere sich mit ihm ändert. Ich sehe keinen guten Grund, sie aneinander zu binden.

pdr
quelle
2

Aus Wikipedia : " Interface wird oft verwendet, um einen abstrakten Typ zu definieren, der keine Daten enthält, aber als Methoden definierte Verhaltensweisen offenlegt ". Meiner Meinung nach wird eine Schnittstelle verwendet, um ein Verhalten zu beschreiben. Wenn Sie also unterschiedliche Verhaltensweisen haben, ist es sinnvoll, unterschiedliche Schnittstellen zu verwenden. Als ich Ihre Frage las, hatte ich den Eindruck, dass Sie über unterschiedliche Verhaltensweisen sprechen, sodass unterschiedliche Benutzeroberflächen der beste Ansatz sein können.

Ein weiterer Punkt, den Sie selbst gesagt haben, ist, dass sich eines dieser Verhaltensweisen ändert. Was passiert dann, wenn Sie nur eine Schnittstelle haben?

Joqus
quelle
1

Wenn die Regeln Ihres Kartenspiels zwischen "Effekten" und "Fähigkeiten" unterscheiden, müssen Sie sicherstellen, dass es sich um unterschiedliche Schnittstellen handelt. Dies verhindert, dass Sie versehentlich einen von ihnen verwenden, wenn der andere benötigt wird.

Das heißt, wenn sie extrem ähnlich sind, kann es sinnvoll sein, sie von einem gemeinsamen Vorfahren abzuleiten. Überlegen Sie sorgfältig: Haben Sie Grund haben , dass „Effekte“ und „Fähigkeiten“ zu glauben , immer notwendigerweise die gleiche Schnittstelle haben? Wenn Sie der effectSchnittstelle ein Element hinzufügen , müssen Sie dann dasselbe Element zur abilitySchnittstelle hinzufügen ?

In diesem Fall können Sie solche Elemente in einer gemeinsamen featureSchnittstelle platzieren, von der beide abgeleitet sind. Wenn nicht, sollten Sie nicht versuchen, sie zu vereinheitlichen - Sie verschwenden Ihre Zeit damit, Dinge zwischen Basis- und abgeleiteten Schnittstellen zu verschieben. Da Sie jedoch nicht beabsichtigen, die gemeinsame Basisschnittstelle für irgendetwas anderes als "Wiederholen Sie sich nicht" zu verwenden, macht dies möglicherweise keinen großen Unterschied. Und wenn Sie an dieser Absicht festhalten, ist es meines Erachtens relativ einfach, das Problem später zu beheben, wenn Sie zu Beginn die falsche Wahl treffen.

kommendes Gewitter
quelle
0

Was Sie sehen, ist im Grunde ein Artefakt der eingeschränkten Ausdruckskraft von Typensystemen.

Theoretisch wären die Signaturen der beiden Schnittstellen unterschiedlich, wenn Sie das Verhalten dieser beiden Schnittstellen in Ihrem Typsystem genau spezifizieren könnten, da sie sich unterschiedlich verhalten. In der Praxis ist die Aussagekraft von Typsystemen durch fundamentale Einschränkungen wie das Halteproblem und den Satz von Rice begrenzt, sodass nicht jede Facette des Verhaltens ausgedrückt werden kann.

Heutzutage haben verschiedene Typensysteme unterschiedliche Ausdrucksstärken, aber es wird immer etwas geben, das nicht ausgedrückt werden kann.

Beispiel: Zwei Schnittstellen mit unterschiedlichem Fehlerverhalten haben möglicherweise dieselbe Signatur in C #, jedoch nicht in Java (da in Java Ausnahmen Teil der Signatur sind). Zwei Schnittstellen, deren Verhalten sich nur in ihren Nebenwirkungen unterscheidet, haben in Java möglicherweise dieselbe Signatur, in Haskell jedoch unterschiedliche Signaturen.

Es ist also immer möglich, dass Sie für unterschiedliche Verhaltensweisen dieselbe Signatur erhalten. Wenn Sie der Meinung sind, dass es wichtig ist, zwischen diesen beiden Verhaltensweisen auf mehr als nur einer nominalen Ebene zu unterscheiden, müssen Sie zu einem aussagekräftigeren System (oder einem anderen System) wechseln.

Jörg W. Mittag
quelle