3 Stimmen

Design-Problem bei Typ-Slicing mit vielen verschiedenen Unterklassen

Ein grundlegendes Problem, auf das ich recht häufig stoße, für das ich aber noch nie eine saubere Lösung gefunden habe, besteht darin, dass man ein Verhalten für die Interaktion zwischen verschiedenen Objekten einer gemeinsamen Basisklasse oder Schnittstelle programmieren möchte. Um es ein wenig konkreter zu machen, füge ich ein Beispiel ein;

Bob hat an einem Strategiespiel gearbeitet, das "coole geografische Effekte" unterstützt. Dabei handelt es sich um einfache Einschränkungen, z. B. werden Truppen, die im Wasser laufen, um 25 % verlangsamt. Wenn sie auf Gras laufen, werden sie um 5% verlangsamt, und wenn sie auf Pflaster laufen, werden sie um 0% verlangsamt.

Die Geschäftsleitung hat Bob mitgeteilt, dass sie eine neue Art von Truppen benötigt. Es sollte Jeeps, Boote und auch Hovercrafts geben. Außerdem sollten die Jeeps Schaden nehmen, wenn sie ins Wasser fahren, und Hovercrafts sollten alle drei Geländetypen ignorieren. Gerüchten zufolge könnte ein weiterer Geländetyp hinzukommen, der noch mehr Funktionen hat, als Einheiten zu verlangsamen und Schaden zu nehmen.

Es folgt ein sehr grobes Pseudo-Code-Beispiel:

public interface ITerrain
{
    void AffectUnit(IUnit unit);
}

public class Water : ITerrain
{
    public void AffectUnit(IUnit unit)
    {
        if (unit is HoverCraft)
        {
            // Don't affect it anyhow
        }
        if (unit is FootSoldier)
        {
            unit.SpeedMultiplier = 0.75f;
        }
        if (unit is Jeep)
        {
            unit.SpeedMultiplier = 0.70f;
            unit.Health -= 5.0f;
        }
        if (unit is Boat)
        {
            // Don't affect it anyhow
        }
        /*
         * List grows larger each day...
         */
    }
}
public class Grass : ITerrain
{
    public void AffectUnit(IUnit unit)
    {
        if (unit is HoverCraft)
        {
            // Don't affect it anyhow
        }
        if (unit is FootSoldier)
        {
            unit.SpeedMultiplier = 0.95f;
        }
        if (unit is Jeep)
        {
            unit.SpeedMultiplier = 0.85f;
        }
        if (unit is Boat)
        {
            unit.SpeedMultiplier = 0.0f;
            unit.Health = 0.0f;
            Boat boat = unit as Boat;
            boat.DamagePropeller();
            // Perhaps throw in an explosion aswell?
        }
        /*
         * List grows larger each day...
         */
    }
}

Wie Sie sehen können, wäre es besser gewesen, wenn Bob von Anfang an ein solides Designdokument gehabt hätte. Mit der wachsenden Anzahl von Einheiten und Geländetypen steigt auch die Komplexität des Codes. Bob muss sich nicht nur Gedanken darüber machen, welche Elemente der Einheitenschnittstelle hinzugefügt werden müssen, sondern er muss auch eine Menge Code wiederholen. Es ist sehr wahrscheinlich, dass neue Geländetypen zusätzliche Informationen benötigen, die über das hinausgehen, was über die grundlegende IUnit-Schnittstelle abgerufen werden kann.

Jedes Mal, wenn wir eine weitere Einheit ins Spiel bringen, muss jedes Gelände aktualisiert werden, um mit der neuen Einheit fertig zu werden. Dies führt natürlich zu einer Menge Wiederholungen, ganz zu schweigen von der hässlichen Laufzeitprüfung, die den Typ der zu behandelnden Einheit bestimmt. Ich habe in diesem Beispiel die Aufrufe an die spezifischen Subtypen weggelassen, aber diese Art von Aufrufen sind notwendig, um sie durchzuführen. Ein Beispiel wäre, dass die Schiffsschraube beschädigt wird, wenn ein Boot auf Land trifft. Nicht alle Einheiten haben Propeller.

Ich bin mir nicht sicher, wie diese Art von Problem genannt wird, aber es handelt sich um eine Many-to-many-Abhängigkeit, die ich nur schwer entkoppeln kann. Ich habe keine Lust, 100 Überladungen für jede IUnit-Unterklasse auf ITerrain zu haben, da ich eine saubere Kopplung erreichen möchte.

Jeder Hinweis auf dieses Problem ist sehr willkommen. Vielleicht denke ich zu weit weg von der Umlaufbahn?

1voto

Tim Williscroft Punkte 3687

Terrain hat ein Terrain-Attribut

Terrain-Attribute sind mehrdimensional.

Die Einheiten haben einen Antrieb.

Der Antrieb ist mit den Terrain-Attributen kompatibel.

Einheiten bewegen sich durch einen Geländebesuch mit Antrieb als Argument. Das wird an die Propulsion delegiert.

Einheiten Mai im Rahmen des Besuchs durch das Terrain beeinträchtigt werden.

Der Einheitscode weiß nichts über den Antrieb. Geländetypen können sich ändern, ohne dass etwas anderes als die Geländeeigenschaften und der Antrieb geändert wird. Die Konstrukteure von Propuslion schützen bestehende Einheiten vor neuen Reisemethoden.

1voto

munificent Punkte 11450

Die Einschränkung, auf die Sie hier stoßen, ist, dass C# im Gegensatz zu einigen anderen OOP-Sprachen keine Mehrfachversand .

Mit anderen Worten, angesichts dieser Basisklassen:

public class Base
{
    public virtual void Go() { Console.WriteLine("in Base"); }
}

public class Derived : Base
{
    public virtual void Go() { Console.WriteLine("in Derived"); }
}

Diese Funktion:

public void Test()
{
    Base obj = new Derived();
    obj.Go();
}

korrekt "in Derived" aus, obwohl der Verweis "obj" vom Typ Base ist. Der Grund dafür ist während der Laufzeit C# findet korrekt das am meisten abgeleitete Go() zum Aufrufen.

Da C# jedoch eine Single-Dispatch-Sprache ist, gilt dies nur für den "ersten Parameter", der in einer OOP-Sprache implizit "this" ist. Der folgende Code macht no wie oben beschrieben funktionieren:

public class TestClass
{
    public void Go(Base b)
    {
        Console.WriteLine("Base arg");
    }

    public void Go(Derived d)
    {
        Console.WriteLine("Derived arg");
    }

    public void Test()
    {
        Base obj = new Derived();
        Go(obj);
    }
}

Dies führt zur Ausgabe "Base arg", da außer "this" alle anderen Parameter statisch dispatched, d.h. sie sind zur Kompilierzeit an die aufgerufene Methode gebunden. Zur Kompilierzeit kennt der Compiler nur den deklarierten Typ des übergebenen Arguments ("Base obj") und nicht dessen tatsächlichen Typ, so dass der Methodenaufruf an den Typ Go(Base b) gebunden ist.

Eine Lösung für Ihr Problem ist dann, im Grunde Hand-Autor eine kleine Methode Dispatcher:

public class Dispatcher
{
    public void Dispatch(IUnit unit, ITerrain terrain)
    {
        Type unitType = unit.GetType();
        Type terrainType = terrain.GetType();

        // go through the list and find the action that corresponds to the
        // most-derived IUnit and ITerrain types that are in the ancestor
        // chain for unitType and terrainType.
        Action<IUnit, ITerrain> action = /* left as exercise for reader ;) */

        action(unit, terrain);
    }

    // add functions to this
    public List<Action<IUnit, ITerrain>> Actions = new List<Action<IUnit, ITerrain>>();
}

Sie können die Reflexion verwenden, um die allgemeinen Parameter jeder übergebenen Aktion zu untersuchen und dann die am weitesten abgeleitete Funktion auszuwählen, die der angegebenen Einheit und dem Terrain entspricht, und diese Funktion dann aufrufen. Die zu Actions hinzugefügten Funktionen können überall sein, sogar über mehrere Baugruppen verteilt.

Interessanterweise bin ich schon ein paar Mal mit diesem Problem konfrontiert worden, aber nie außerhalb des Kontextes von Spielen.

1voto

Steven A. Lowe Punkte 59247

Die Interaktionsregeln von den Klassen Unit und Terrain entkoppeln; Interaktionsregeln sind allgemeiner als das. Beispielsweise könnte eine Hash-Tabelle verwendet werden, wobei der Schlüssel ein Paar interagierender Typen und der Wert eine "Effektor"-Methode ist, die auf Objekte dieser Typen wirkt.

wenn zwei Objekte miteinander interagieren müssen, ALLE Interaktionsregeln in der Hash-Tabelle finden und sie ausführen

Dadurch entfallen die Abhängigkeiten zwischen den Klassen, ganz zu schweigen von den hässlichen Switch-Anweisungen in Ihrem ursprünglichen Beispiel

wenn die Leistung ein Problem darstellt und sich die Interaktionsregeln während der Ausführung nicht ändern, die Regelsätze für Typpaare zwischengespeichert werden, sobald sie auftreten, und eine neue MSIL-Methode ausgegeben wird, um sie alle auf einmal auszuführen

1voto

17 of 26 Punkte 26635

Hier sind definitiv drei Objekte im Spiel:

1) Gelände
2) Terrain-Effekte
3) Einheiten

Ich würde nicht empfehlen, eine Karte mit dem Paar des Geländes/der Einheit als Schlüssel zum Nachschlagen der Aktion. Dadurch wird es für Sie schwierig, sicherzustellen, dass Sie alle Kombinationen abgedeckt haben, wenn die Listen der Einheiten und Gelände wachsen.

Tatsächlich scheint es so zu sein, dass jede Gelände-Einheiten-Kombination einen einzigartigen Geländeeffekt hat, so dass es zweifelhaft ist, dass eine gemeinsame Liste von Geländeeffekten überhaupt von Vorteil wäre.

Stattdessen würde ich dafür sorgen, dass jede Einheit ihre eigene Karte mit den Geländeeffekten unterhält. Dann kann das Terrain einfach Unit->AffectUnit(myTerrainType) aufrufen und die Einheit kann den Effekt, den das Terrain auf sie selbst hat, nachschlagen.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X