Wie kann man einen Konstruktor zerlegen?

21

Nehmen wir an, ich habe eine Enemy-Klasse und der Konstruktor würde ungefähr so ​​aussehen:

public Enemy(String name, float width, float height, Vector2 position, 
             float speed, int maxHp, int attackDamage, int defense... etc.){}

Das sieht schlecht aus, weil der Konstruktor so viele Parameter hat, aber wenn ich eine Enemy-Instanz erstelle, muss ich all diese Dinge angeben. Ich möchte diese Attribute auch in der Enemy-Klasse haben, damit ich eine Liste von ihnen durchlaufen und diese Parameter abrufen / festlegen kann. Ich dachte, ich könnte Enemy in EnemyB, EnemyA unterteilen, während ich deren maxHp und andere spezifische Attribute fest codiere, aber dann würde ich den Zugriff auf ihre fest codierten Attribute verlieren, wenn ich eine Liste von Enemy (bestehend aus EnemyA's, EnemyB's und EnemyA's) durchlaufen wollte EnemyC's).

Ich versuche nur zu lernen, wie man sauber codiert. Wenn es einen Unterschied macht, arbeite ich in Java / C ++ / C #. Jeder Punkt in die richtige Richtung wird geschätzt.

Travis
quelle
5
Es ist nicht schlecht, einen Konstruktor zu haben, der alle Attribute bindet. In einigen Persistenzumgebungen ist dies sogar erforderlich. Nichts sagt aus, dass Sie nicht mehrere Konstruktoren haben können, möglicherweise mit einer Gültigkeitsprüfungsmethode, die nach der stückweisen Konstruktion aufgerufen wird.
BobDalgleish
1
Ich würde fragen müssen, ob Sie jemals beabsichtigen, Enemy-Objekte in Code mit Literalen zu konstruieren. Wenn Sie dies nicht tun, und ich verstehe nicht, warum Sie dies tun würden, erstellen Sie Konstruktoren, die die Daten von einer Datenbankschnittstelle oder einer Serialisierungszeichenfolge abrufen, oder ...
Zan Lynx
Named Parameter idiom
Mooing Duck

Antworten:

58

Die Lösung besteht darin, die Parameter in zusammengesetzte Typen zu bündeln. Breite und Höhe hängen konzeptionell zusammen - sie geben die Dimensionen des Feindes an und werden normalerweise zusammen benötigt. Sie könnten durch einen DimensionsTyp ersetzt werden, oder vielleicht durch einen RectangleTyp, der auch die Position enthält. Auf der anderen Seite könnte es sinnvoller , zu einer Gruppe machen positionund speedin eine MovementDataArt, vor allem , wenn die Beschleunigung später das Bild eintritt. Von Kontext Ich gehe davon aus maxHp, attackDamage, defenseusw. auch in einer zusammengehören StatsArt. Eine überarbeitete Signatur könnte also ungefähr so ​​aussehen:

public Enemy(String name, Dimensions dimensions, MovementData movementData, Stats stats)

Die genauen Details zum Zeichnen der Linien hängen vom Rest Ihres Codes und den gemeinsam verwendeten Daten ab.

Doval
quelle
21
Ich möchte auch hinzufügen, dass so viele Werte auf einen Verstoß gegen das Prinzip der Einzelverantwortung hindeuten können. Das Gruppieren von Werten in bestimmte Objekte ist der erste Schritt zur Trennung dieser Verantwortlichkeiten.
Euphoric
2
Ich denke nicht, dass die Liste der Werte ein SRP-Problem ist; Die meisten von ihnen sind wahrscheinlich für Konstruktoren der Basisklasse gedacht. Jede Klasse in der Hierarchie kann eine einzelne Verantwortung haben. Enemyist nur die Klasse, die die angreift Player, aber ihre gemeinsame Basisklasse Combatantbenötigt die Kampfstatistik.
MSalters
@MSalters Dies weist nicht unbedingt auf ein SRP-Problem hin, könnte es aber. Wenn er genügend Zahlen eingeben muss, könnten diese Funktionen in die Enemy-Klasse gelangen, wenn es sich entweder um statische / freie Funktionen (wenn er Dimensions/ MovementDataals einfache alte Datencontainer verwendet) oder um Methoden (wenn er sie in abstrakte Daten umwandelt) handeln soll Typen / Objekte). Wenn er beispielsweise noch keinen Vector2Typ erstellt hätte, wäre er möglicherweise in Vektormathematik gelandet Enemy.
Doval
24

Vielleicht möchten Sie sich das Builder-Muster ansehen . Aus dem Link (mit Beispielen für Muster und Alternativen):

Das Builder-Muster ist eine gute Wahl beim Entwerfen von Klassen, deren Konstruktoren oder statische Fabriken mehr als eine Handvoll Parameter haben würden, insbesondere wenn die meisten dieser Parameter optional sind. Client-Code ist mit Buildern viel einfacher zu lesen und zu schreiben als mit dem herkömmlichen Teleskop-Konstruktormuster, und Builder sind viel sicherer als JavaBeans.

Rory Hunter
quelle
4
Ein kurzer Codeausschnitt wäre hilfreich. Dies ist ein großartiges Muster zum Erstellen komplizierter Objekte oder Strukturen mit verschiedenen Eingaben. Sie können auch Builder wie EnemyABuilder, EnemyBBuilder usw. spezialisieren, die die verschiedenen gemeinsamen Eigenschaften einschließen. Dies ist eine Art Kehrseite des Factory-Musters (wie unten beantwortet), aber ich persönlich bevorzuge Builder.
Rob
1
Danke, sowohl das Builder-Muster als auch das Factory-Muster sehen so aus, als würden sie gut mit dem zusammenarbeiten, was ich versuche, insgesamt zu tun. Ich denke, eine Kombination aus Builder / Factory und Dovals Vorschlag könnte das sein, wonach ich suche. Bearbeiten: Ich denke, ich kann nur eine Antwort markieren; Ich werde es Doval geben, da es die Themenfrage beantwortet, aber die anderen sind für mein spezifisches Problem ebenso hilfreich. Danke euch allen.
Travis
Ich denke, es ist erwähnenswert, dass, wenn Ihre Sprache Phantomtypen unterstützt, Sie ein Builder-Muster schreiben können, das erzwingt, dass einige / alle SetX-Funktionen aufgerufen werden. Es erlaubt einem auch sicherzustellen, dass sie nur einmal angerufen werden (falls gewünscht).
Thomas Eding
1
@ Mark16 Wie im Link erwähnt, > simuliert das Builder-Muster benannte optionale Parameter, wie sie in Ada und Python zu finden sind. Sie haben erwähnt, dass Sie in der Frage auch C # verwenden, und diese Sprache unterstützt benannte / optionale Argumente (ab C # 4.0), sodass dies möglicherweise eine andere Option ist.
Bob
5

Das Verwenden von Unterklassen zum Voreinstellen einiger Werte ist nicht wünschenswert. Nur Unterklasse, wenn ein neuer Feindtyp ein anderes Verhalten oder neue Attribute hat.

Das Factory-Muster wird normalerweise verwendet, um die genaue verwendete Klasse zu abstrahieren. Es kann jedoch auch verwendet werden, um Vorlagen für die Objekterstellung bereitzustellen:

class EnemyFactory {

    // each of these methods is essentially a template for a kind of enemy

    Enemy enemyA(String name, ...) {
        return new Enemy(name, ..., presetValue, ...);
    }

    Enemy enemyB(String name, ...) {
        return new Enemy(name, ..., otherValue, ...);
    }

    Enemy enemyC(String name, ...) {
        return new EnemySubclass(name, ..., otherValue, ...);
    }

    ...
}

EnemyFactory factory = new EnemyFactory();
Enemy a = factory.enemyA("fred", ...);
Enemy b = factory.enemyB("willy", ...);
amon
quelle
0

Ich würde Unterklassen für Klassen reservieren, die Objekte darstellen, die Sie möglicherweise unabhängig verwenden möchten, z. B. Charakterklasse, in der alle Charaktere, nicht nur Feinde, Namen, Geschwindigkeit, maxHp oder eine Klasse zur Darstellung von Sprites mit einer Präsenz auf dem Bildschirm mit Breite haben. Höhe, Position.

Ich sehe an einem Konstruktor mit vielen Eingabeparametern nichts, aber wenn Sie ihn ein wenig aufteilen möchten, könnten Sie einen Konstruktor haben, der die meisten Parameter festlegt, und einen anderen (überladenen) Konstruktor, der verwendet werden kann um bestimmte und andere auf Standardwerte zu setzen.

Abhängig davon, welche Sprache Sie verwenden, können einige Standardwerte für die Eingabeparameter Ihres Konstruktors festlegen, z.

Enemy(float height = 42, float width = 42);
Encaitar
quelle
0

Ein Codebeispiel zur Antwort von Rory Hunter (in Java):

public class Enemy{
   private String name;
   private float width;
   ...

   public static class Builder{
       private Enemy instance;

       public Builder(){
           this.instance = new Enemy();
       }


       public Builder withName(String name){
           instance.name = name;
           return this;
       }

       ...

       public Enemy build(){
           return instance;
       }
   }
}

Jetzt können Sie neue Instanzen von Enemy wie folgt erstellen:

Enemy myEnemy = new Enemy.Builder().withName("John").withX(x).build();
Toon Borgers
quelle
1
Programmierer ist Tour konzeptionelle Fragen und Antworten werden erwartet, um Dinge zu erklären . Das Werfen von Code-Dumps anstelle von Erklärungen ist wie das Kopieren von Code von IDE auf Whiteboard: Es mag vertraut und manchmal sogar verständlich erscheinen, aber es fühlt sich seltsam an ... einfach seltsam. Whiteboard hat keinen Compiler
Mücke