Laden...

Nach welchen Kriterien Konstruktoren definieren?

Erstellt von wild-card vor 16 Jahren Letzter Beitrag vor 16 Jahren 6.175 Views
W
wild-card Themenstarter:in
10 Beiträge seit 2007
vor 16 Jahren
Nach welchen Kriterien Konstruktoren definieren?

Hi!

Hätte mal eine Frage, wie ihr entscheidet, welche Konstruktoren ihr einem Benutzer zur Verfügung stellt.

Es geht darum, dass ich teilweise nicht so recht weiß, nach welchen Kriterien man Konstruktoren überladen sollte. Sollte man jede Eigenschaft verfügbar machen? Oder sollte man lieber nur die Eigenschaften bereitstellen, die auch Sinn machen?

Ein Beispiel zur Verdeutlichung:


public struct Location {
    public short X;
    public short Y;

     public Location(short x, short y) {
        X = x;
        Y = y;
    }
}

public class Basement {

    // *** Eigenschaften

    protected Location lctn = new Location();
    protected string type = "";

    // *** Konstruktoren

    public Basement() { }

    private Basement(string newType) {
        this.type = newType;
    }

    public Basement(short X, short Y, string newType)
       : this(newType) {
        this.lctn.X = X;
        this.lctn.Y = Y;
    }
    public Basement(Location newLctn, string newType)
        : this(newType) {
        this.lctn = newLctn;
    }

    // *** Eigenschaftsmethoden

    public short XCoordinate {
        get { return this.lctn.X; }
        set { this.lctn.X = value; }
    }
    public short YCoordinate {
        get { return this.lctn.Y; }
        set { this.lctn.Y = value; }
    }

    public string Type {
        get { return this.type; }
        set { this.type = value; }
    }
} 

Würde es in diesem Beispiel überhaupt Sinn machen, einem Benutzer die Möglichkeit zu geben auch x,y Werte separat bei der Erstellung mit anzugeben, oder würdet ihr sagen, wenn der Benutzer über die Struktur newLctn die Koordinaten mitgibt, z.B.:


Basement bsmnt = new Basement(new Location(5, 16), "Type");

...ist das ausreichend?

Habe da noch nicht sehr viel Erfahrung, was das angeht. 😦

MfG

B
1.529 Beiträge seit 2006
vor 16 Jahren

Nun ja, das ist eine Entscheidungsfrage, die einfach auch davon abhängt, wie oft welche Art der Erzeugung benötigt wird.

Generell ist es aber empfehlenswert, die Reihenfolge der Parameter soweit möglich beizubehalten.

Also:

public Basement() : this(String.Empty, new Location(0,0)) { }

public Basement(string newType) : this(newType, new Location(0,0)) { }

public Basement(string newType, short X, short Y ) : this(newType, new Location(X,Y)) {}

public Basement(string newType, Location newLctn )
{
   this.type = newType;
   this.lctn = newLctn;
}
S
8.746 Beiträge seit 2005
vor 16 Jahren

Original von wild-card
Hätte mal eine Frage, wie ihr entscheidet, welche Konstruktoren ihr einem Benutzer zur Verfügung stellt.

Es gibt eigentlich nur eine Regel: Der Konstruktor-Aufruf liefert ein vollständig initialisiertes und benutzbares Objekt.

Dein Default-Konstruktur setzt z.B. keinen Default-Type, Regel damit nicht erfüllt.

49.485 Beiträge seit 2005
vor 16 Jahren

Hallo wild-card,

die reine Lehre sagt, dass ein Objekt durch den Konstruktor sofort in einen (inhaltlich) gültigen Zustand versetzt werden soll. In der Regel ist das nur der Fall, wenn man dem Konstruktor den gesamten benötigten initialen Zustand per Parameter übergibt.

Das ist aber sehr unpraktisch, weil man dann lange Parameterlisten hat und der Sinn der Parameter sich nur aus Reihenfolge (und vielleicht noch dem Typ) ergibt. Wenn man stattdessen Properties zum Setzen des (initialen) Zustands verwendet, sieht man immer sofort den Namen der Property, für die der Wert gedacht ist.

Deshalb macht man es so, dass man oft parameterlose Konstruktoren verwendet und dass man - auch für eigentlich unveränderliche Eigenschaften - bei den Properties Setter zur Verfügung stellt.

Für welche Properties es Getter gibt oder geben sollte, ist von der Frage des Konstruktor unabhängig.

herbivore

W
wild-card Themenstarter:in
10 Beiträge seit 2007
vor 16 Jahren

Okay, die Vorschläge werd ich berücksichtigen und die Konstruktoren umbauen.

Wo wir gerade dabei sind, hab ich nochmal eine Frage zur Vererbung von Konstruktoren. Die Klasse Building erbt nun von Basement, erweitert diese aber um ein paar Eigenschaften.
Ich brauche also auch neue Konstruktoren, die ich bisher wie unten zu sehen realisiert habe. Allerdings gefällt mir das noch nicht so gut, da ich bei den letzten beiden Code doppelt schreiben musste <- doof.

Gibt es denn eine Möglichkeit auf 2 verschiedene Konstruktoren zu verweisen?

Anm.: Ich hab hier Eure Vorschläge noch nicht umgesetzt.

    
    public class Building : Basement {

        // *** Eigenschaften

        protected Occupant ocpnt = new Occupant();
        protected decimal value = 0;

        // *** Konstruktoren

        public Building() { }

        public Building(short X, short Y, string newType) : base(X, Y, newType) { }
        public Building(Location newLctn, string newType) : base(newLctn, newType) { }

        public Building(short X, short Y, string newType, decimal newValue)
            : this(X, Y, newType) {
            this.value = newValue;
        }
        public Building(Location newLctn, string newType, decimal newValue)
            : this(newLctn, newType) {
            this.value = newValue;
        }
        public Building(short X, short Y, Occupant newOcpnt, string newType, decimal newValue)
            : this(X, Y, newType, newValue) {
            this.ocpnt = newOcpnt;
        }
        public Building(Location newLctn, Occupant newOcpnt, string newType, decimal newValue)
            : this(newLctn, newType, newValue) {
            this.ocpnt = newOcpnt;
        }

        ...

PS.: Wie Du schon sagtest herbivore ist das alles etwas unübersichtlich geworden. Könntest Du vllt ein kurzes Beispiel geben, wie Du das mit den "Properties des gesamten initialen Zustandes" meinst?

B
1.529 Beiträge seit 2006
vor 16 Jahren

Du musst keinen Code doppelt schreiben. Aber berücksichtige doch erstmal die obigen Hinweise, bevor du dich an die Vererbung machst. Dann sollten sich die meisten deiner Probleme auflösen.

S
8.746 Beiträge seit 2005
vor 16 Jahren

Original von herbivore
Das ist aber sehr unpraktisch, weil man dann lange Parameterlisten hat und der Sinn der Parameter sich nur aus Reihenfolge (und vielleicht noch dem Typ) ergibt.

Wieso unpraktisch? Du brauchst mit Properties mehr Code als mit dem Konstruktor. Dank Intellisense sollten Parameterlisten auch überschaubar bleiben.

Deshalb macht man es so, dass man oft parameterlose Konstruktoren verwendet

Ich mache das nie. Ausnahmen sind technischer Natur, z.B. einen Default-Konstruktor für die Serialisierung. Hier wird aber die Regel nicht gebrochen, weil Datenobjekt i.d.R. zustandsfrei sind.

49.485 Beiträge seit 2005
vor 16 Jahren

Hallo wild-card,

Konstruktoren werden nicht vererbt (jedenfalls nicht wie normale Methoden). Das bedeutet, wenn es in der Oberklasse einen Konstruktor mit dem Parameter bla gibt, dann muss man wenn man in der Unterklasse auch einen Konstruktor mit dem Parameter bla haben will, diesen neu definieren. Daran führt kein Weg dran vorbei. Du kannst allerdings durch Angabe von

: base (bla)

erzwingen, dass vor dem neuen Konstruktor der Oberklassen-Konstruktor mit dem Parameter bla aufgerufen wird. Dadurch kann man vermeiden, dass man die Anweisungen aus dem Rumpf des Konstruktors duplizieren muss.

"Properties des gesamten initialen Zustandes"

Da hast du mich aber falsch zitiert. 🙂 Lies mal bitte nochmal, was ich geschrieben habe. Ich denke, dann müsste klar sein, was ich meine. Wenn nicht, frag bitte nochmal nach.

Hallo svenson,

Wieso unpraktisch?

weil Parameterlisten von 20 und mehr Parametern auch mit Intellisense unübersichtlich sind. 🙂 Und weil es in C# keine benannten Parameter gibt. Das wäre hierbei eine echte Alternative.

herbivore

B
1.529 Beiträge seit 2006
vor 16 Jahren

Dank Intellisense sollten Parameterlisten auch überschaubar bleiben.

Mittlerweile ist doch bekannt, das herbivore ein Hardcore mit-dem-Editor-programmier-und-ber-die-Kommandozeile-kompilier Programmierer ist. Da ist nichts mit IntelliSense.

Ich bin aber auch der Meinung, dass Eigenschaften, die für die Verwendung der Instanz wichtig sind, bereits bei deren Erzeugung festgelegt werden sollen.

Ansonsten dürfte man nicht mal eben Foo( new Point(5,10) ) schreiben, sondern immer Point p = new Point; p.X = 5; p.Y = 10; Foo(p);.

49.485 Beiträge seit 2005
vor 16 Jahren

Hallo Borg,

ich habe nicht gesagt, dass man Konstruktoren mit Parametern nicht verwenden soll oder gar nicht verwenden darf. Mir ging es um Übersichtlichkeit. Wenn diese - wie in deinem Beispiel - durch Konstruktoren mit Parametern besser ist, dann sollte man diese natürlich verwenden.

herbivore

S
8.746 Beiträge seit 2005
vor 16 Jahren

Original von herbivore
weil Parameterlisten von 20 und mehr Parametern auch mit Intellisense unübersichtlich sind.

Gegenfrage: Wie stellst du sicher, dass du bei 20 Property-Aufrufen nicht einen vergessen hast?

Zudem ist das Beispiel reichlich konstruiert. Du wirst im gesamten .NET-Framework wohl keine einzige Klassen finden, dessen kleinster Konstruktor mehr als 4 Parameter umfaßt. Die Regel besagt ja auch nur, dass der Konstruktor ein Objekt in einem gültigen Zustand abliefern muss. Man muss nicht jeden Zustand mit einem Konstruktoraufruf erreichen können. In vielen Fällen kann das bedeuten, dass man wirklich nur einen Default-Konstruktor hat und den Rest per Properties macht.

Das hier gemachte Beispiel ist aber ganz gut: Hier hat jedes Objekt offenbar einen verpflichtenden Type-Member. Jeder Konstruktur muss daher diesen Member setzen. Macht einer das nicht, verstößt das Design gegen das Gebot der Robustheit. Da rettet dann auch kein Type-Setter mehr.

W
wild-card Themenstarter:in
10 Beiträge seit 2007
vor 16 Jahren

Eine Frage bzgl. Standardkonstruktor:

Wenn ich diesen schon so genau definiere, dass alle Eigenschaften initialisiert werden, selbst wenn kein einziger Parameter übergeben wird, kann ich doch eine Initialisierung der Eigenschaften auslassen?


protected Location lctn;
protected string type;

anstatt


protected Location lctn = new Location();
protected string type = "";

Oder? =)

49.485 Beiträge seit 2005
vor 16 Jahren

Hallo wild-card,

ja, kann man machen. Ist eine reine Geschmacksfrage.

herbivore

S
8.746 Beiträge seit 2005
vor 16 Jahren

Hat sogar prinzipiell den Vorteil, dass die Initialisierung nur an einer Stelle im Code erfolgt und nicht über x Stellen verteilt. Das kann man aber auch dadurch verhindern, dass man in einem Konstruktor erst den Default-Konstruktor aufruft und dann weitere Initialisierungen vornimmt. Im besten Falle wird ein Member nur in genau einem Konstruktor gesetzt, sonst hat man irgendwo Copy&Paste-Code.

N
4.644 Beiträge seit 2004
vor 16 Jahren

Ich denke solche Fragen werden sich dann mit .NET 3.5 erledigen. -> Object Initializers

W
wild-card Themenstarter:in
10 Beiträge seit 2007
vor 16 Jahren

Also ich hab jetzt die Basement-Klasse erstmal so bearbeitet, wie Du das oben vorgeschlagen hast, Borg.

Jetzt bin ich allerdings total verwirrt, wie das mit der Vererbung gehandhabt werden soll... Ich kann ja entweder nur mit ": this()" oder mit ": base()" erweitern, oder lieg ich da falsch?

Betrachte ich erstmal nur den Standard-Konstruktor, der ein komplett initialisiertes Default-Objekt erstellen soll, so müsste der doch so aussehen?

public Building() : this(new Occupant(""), 0) {  }

Allerdings soll die Klasse Building immernoch Basement erweitern, also müsste ich den Kons. doch wie folgt ändern?


public Building() : base() {
    this.ocpnt = new Occupant();
    this.value = 0;
}

Oder? 🤔

P.S.: Kommen diese Features aus dem obigen Link erst in .NET3.5, dort ist ja die Rede von .NET3.0 und das ist doch schon verfügbar?

49.485 Beiträge seit 2005
vor 16 Jahren

Hallo wild-card,

richtig, man kann nur base oder this angeben. Das ist aber auch kein Problem.

Allerdings soll die Klasse Building immernoch Basement erweitern, also müsste ich den Kons. doch wie folgt ändern?

Nö, musst du nicht ändern. Wenn du base nicht angibt, wird immer automatisch der parameterlose Konstruktor der Basisklasse aufgerufen.

Kommen diese Features aus dem obigen Link erst in .NET3.5, dort ist ja die Rede von .NET3.0 und das ist doch schon verfügbar?

Erst in .NET 3.5. Im Artikel ist die Rede von C# 3.0. .NET 3.0 benutzt C# 2.0.

herbivore

W
wild-card Themenstarter:in
10 Beiträge seit 2007
vor 16 Jahren

Original von herbivore
Nö, musst du nicht ändern. Wenn du base nicht angibt, wird immer automatisch der parameterlose Konstruktor der Basisklasse aufgerufen.

Hm, sowas muss man natürlich erstmal wissen... Vielen Dank!

S
1.047 Beiträge seit 2005
vor 16 Jahren

muß aber sagen das ist mal ein richtig nettes feature was uns da in c# 3.0 erwartet =)

49.485 Beiträge seit 2005
vor 16 Jahren

Hallo wild-card,

Hm, sowas muss man natürlich erstmal wissen... Vielen Dank!

genaugenommen muss es heißen, wenn in keinem Konstruktor, der über die this-Kette aufgerufen wird, base angegeben ist, wird der parameterlose Konstruktor der Basisklasse aufgerufen. Wenn in dieser Kette irgendwann bei einem Konstruktor base steht, wird der Konstruktor der Basisklasse aufgerufen, der zu den in base übergebenen Parametern passt.

herbivore

W
wild-card Themenstarter:in
10 Beiträge seit 2007
vor 16 Jahren

N'abend allerseits.

Ich hab mich nochmal mit den Konstruktoren beschäftigt und meine das Prinzip eurer Heransgehensweise verstanden zu haben. Vielleicht könnt ihr mir sagen, ob mein Denkansatz richtig ist?

Erstens:


//Klasse Basement
public Basement() : this(String.Empty, new Location(0, 0)) { }
public Basement(string newType) : this(newType, new Location(0, 0)) { }
public Basement(string newType, short X, short Y) : this(newType, new Location(X, Y)) { }
// Hauptkonstruktor:
public Basement(string newType, Location newLctn) {
    this.type = newType;
    this.lctn = newLctn;
}

Angenommen ich erstelle nun ein neues Objekt A der Klasse Basement ohne Parameterübergabe, dann wird zunächst der Standardkons. aufgerufen. Dieser stellt fest, dass er eine Weiterleitung auf sich selbst, jedoch mit 2 übergebenen Parametern enthält. Also wird jetzt nach einem überladenen Kons. gesucht, der 2 Parameter aufnehmen und verarbeiten kann und den hab ich hier als "Hauptkons." betitelt. Soweit richtig?

Btw: Ich hätte gerne, dass die Location vorne steht, Muss ich dazu dann ediglich die Parameter umdrehen, oder muss ich auch einen neuen Konstruktor einfügen?

Ich hab zusätzlich überlegt, ob es sinnvoll ist, dem Benutzer die Möglichkeit zu geben, zwar ein Basement an einer bestimmten Stelle zu errichten, dessen Typ aber nicht zu nennen? Eigentlich macht das keinen Sinn, aber man sollte doch alle Möglichkeiten abdecken?

Dann weiter:


//Klasse Building
public Building() : this(0, new Occupant(String.Empty)) { }
public Building(decimal newValue) : this(newValue, new Occupant(String.Empty)) { }
public Building(decimal newValue, Occupant newOcpnt) 
    : this(newValue, newOcpnt, String.Empty, new Location(0, 0)) { }
public Building(decimal newValue, Occupant newOcpnt, string newType) 
    : this(newValue, newOcpnt, newType, new Location(0, 0)) { }
//Hauptkonstruktor:
public Building(decimal newValue, Occupant newOcpnt, string newType, Location newLctn)
    : base(newType, newLctn) {
    this.value = newValue;
    this.ocpnt = newOcpnt;
}

Bei diesem Code-Stück ist das Prinzip der internen Weiterleitung das gleiche, wie oben, allerdings bin ich bezüglich der Parameterwahl noch nicht so ganz glücklich, bzw. bin unsicher, ob diese so optimal ist, denn auch hier hätte ich am liebsten die Location ganz vorne.

Entspricht letzteres eigentlich dem, was Du bzgl. der Parameterreihenfolge vorgeschlagen hattest, Borg?

Ich danke für die Geduld! 👍

B
1.529 Beiträge seit 2006
vor 16 Jahren

Soweit richtig?

Ja.

Ich hätte gerne, dass die Location vorne steht, Muss ich dazu dann lediglich die Parameter umdrehen, oder muss ich auch einen neuen Konstruktor einfügen?

Einfach Reihenfolge der Parameterliste tauschen. Siehe unten.

Eigentlich macht das keinen Sinn, aber man sollte doch alle Möglichkeiten abdecken?

Nein. Wenn es keinen Sinn macht, brauchst du es auch nicht anzubieten.

Dann weiter: ...

  1. Occupant sollte einen Standard-Konstruktor anbieten, so dass du dir das String.Empty sparen kannst.
  2. Allein von den Namen der Klassen her, vermute ich, dass dein Klassendesign nicht ganz sauber ist. Bei dir ist Building ein Kind von Basement. Das würde aber bedeuten, dass jedes Gebäude automatisch ein Untergeschoss ist.
    Gemeint ist aber sicherlich, dass jedes Building ein Basement hat (Aggregation).

denn auch hier hätte ich am liebsten die Location ganz vorne.

Siehe oben und folgendes.

Entspricht letzteres eigentlich dem, was Du bzgl. der Parameterreihenfolge vorgeschlagen hattest, Borg?

Ich würde die Parameter immer nach Priorität anordnen. Und wenn du selbst schreibst, dass ein Basement zwar ohne Location aber nicht ohne Typ sinnvoll angelegt werden kann, dann ist der Typ der wichtigste Parameter und sollte meines Erachtens nach ganz vorn.
Ansonsten halte ich es für wenig intuitiv, wenn ich einen Konstruktor einen Typ übergebe und ihm später noch die Location hinzufügen will, jedoch diese plötzlich vor dem Typ angeben muss.

W
wild-card Themenstarter:in
10 Beiträge seit 2007
vor 16 Jahren

Original von Borg
2. Allein von den Namen der Klassen her, vermute ich, dass dein Klassendesign nicht ganz sauber ist. Bei dir ist Building ein Kind von Basement. Das würde aber bedeuten, dass jedes Gebäude automatisch ein Untergeschoss ist.
Gemeint ist aber sicherlich, dass jedes Building ein Basement hat (Aggregation).

Meine Idee ist folgende: Ich möchte eine 2D-Karte in der Vogelperspektive erstellen, auf der verschiedene Gebäude und Straßen zu sehen sind. Eine Straße sehe ich aber nicht als Gebäude an, sondern als Basement, zumal diese auch keinen Wert und keinen Besitzer braucht. Ich "sage" also: Ein Gebäude ist auch ein Bodenfeld, daher die Ist-ein-Beziehung.
Vielleicht sollte ich das, um solche "Verwirrungen" zu vermeiden, umbenennen, z.B. in Tile...

Original von Borg
Ich würde die Parameter immer nach Priorität anordnen. Und wenn du selbst schreibst, dass ein Basement zwar ohne Location aber nicht ohne Typ sinnvoll angelegt werden kann, dann ist der Typ der wichtigste Parameter und sollte meines Erachtens nach ganz vorn.
Ansonsten halte ich es für wenig intuitiv, wenn ich einen Konstruktor einen Typ übergebe und ihm später noch die Location hinzufügen will, jedoch diese plötzlich vor dem Typ angeben muss.

Das leuchtet ein. Wie handhabt ihr das dann in vererbten Klassen? Werden diese auch zuerst mit den Basisparametern aufgerufen, oder stehen diese dann hinten an?

Beispielsweise:
Building(Typ,Ort,Wert,Besitzer) oder Building(Wert, Besitzer, Typ, Ort)

Danke für die Geduld! 👅 Ich hab jetzt um einiges mehr verstanden. =)

49.485 Beiträge seit 2005
vor 16 Jahren

Hallo wild-card,

was die Regeln für die Parameter(reihenfolge) angeht, würde ich keinen Unterschied machen, ob du über mehrere Konstruktoren in einer Klasse oder über welche in verschiedenen Klassen redest. Das die gleichen Regeln eingehalten werden sollen, ist zwar bei Vererbung vielleicht ein bisschen augenfälliger, aber wenn man die Regeln strikt für alle Klassen der Bibliothek/des Programms einhält, ist es noch besser. Stichworte: Konsistenz, Uniformität, ...

herbivore

B
1.529 Beiträge seit 2006
vor 16 Jahren

Ich "sage" also: Ein Gebäude ist auch ein Bodenfeld, daher die Ist-ein-Beziehung.

Dem kann ich mich zwar nicht anschließen, aber vielleicht fehlen mir auch nur weitere Informationen.
In meiner kleinen Denkwelt habe ich ein Objekt vom Typ Gebäude, welches auf einem Objekt vom Typ Grundstück steht. Dennoch sind Grundstück und Gebäude zwei verschiedene Paar Schuhe. Auf einem Grundstück können sogar mehrere Gebäude stehen. Keinesfalls jedoch ist ein Gebäude automatisch ein Grundstück; es hat (steht auf) ein(em) Grundstück.

W
wild-card Themenstarter:in
10 Beiträge seit 2007
vor 16 Jahren

Okay, Du hast mich überzeugt. 😉