Frage

Nehmen wir an, Sie haben eine Klasse namens „Customer“, die die folgenden Felder enthält:

  • Nutzername
  • Email
  • Vorname
  • Familienname, Nachname

Nehmen wir außerdem an, dass gemäß Ihrer Geschäftslogik für alle Kundenobjekte diese vier Eigenschaften definiert sein müssen.

Nun können wir dies ganz einfach tun, indem wir den Konstruktor zwingen, jede dieser Eigenschaften anzugeben.Es ist jedoch ziemlich leicht zu erkennen, wie dies außer Kontrolle geraten kann, wenn Sie gezwungen sind, dem Kundenobjekt weitere erforderliche Felder hinzuzufügen.

Ich habe Klassen gesehen, die mehr als 20 Argumente in ihren Konstruktor aufnehmen, und es ist einfach mühsam, sie zu verwenden.Wenn Sie diese Felder jedoch nicht benötigen, besteht alternativ das Risiko, dass undefinierte Informationen vorliegen oder, schlimmer noch, Fehler bei der Objektreferenzierung auftreten, wenn Sie sich bei der Angabe dieser Eigenschaften auf den aufrufenden Code verlassen.

Gibt es Alternativen dazu oder müssen Sie nur entscheiden, ob die Anzahl der X-Konstruktorargumente zu groß ist, als dass Sie damit leben könnten?

War es hilfreich?

Lösung

Zwei zu berücksichtigende Designansätze

Der Wesen Muster

Der flüssige Schnittstelle Muster

Beide haben eine ähnliche Absicht, da wir langsam ein Zwischenobjekt aufbauen und dann in einem einzigen Schritt unser Zielobjekt erstellen.

Ein Beispiel für die Fluent-Schnittstelle in Aktion wäre:

public class CustomerBuilder {
    String surname;
    String firstName;
    String ssn;
    public static CustomerBuilder customer() {
        return new CustomerBuilder();
    }
    public CustomerBuilder withSurname(String surname) {
        this.surname = surname; 
        return this; 
    }
    public CustomerBuilder withFirstName(String firstName) {
        this.firstName = firstName;
        return this; 
    }
    public CustomerBuilder withSsn(String ssn) {
        this.ssn = ssn; 
        return this; 
    }
    // client doesn't get to instantiate Customer directly
    public Customer build() {
        return new Customer(this);            
    }
}

public class Customer {
    private final String firstName;
    private final String surname;
    private final String ssn;

    Customer(CustomerBuilder builder) {
        if (builder.firstName == null) throw new NullPointerException("firstName");
        if (builder.surname == null) throw new NullPointerException("surname");
        if (builder.ssn == null) throw new NullPointerException("ssn");
        this.firstName = builder.firstName;
        this.surname = builder.surname;
        this.ssn = builder.ssn;
    }

    public String getFirstName() { return firstName;  }
    public String getSurname() { return surname; }
    public String getSsn() { return ssn; }    
}
import static com.acme.CustomerBuilder.customer;

public class Client {
    public void doSomething() {
        Customer customer = customer()
            .withSurname("Smith")
            .withFirstName("Fred")
            .withSsn("123XS1")
            .build();
    }
}

Andere Tipps

Ich sehe, dass einige Leute sieben als Obergrenze empfehlen.Anscheinend stimmt es nicht, dass Menschen sieben Dinge gleichzeitig im Kopf haben können;sie können sich nur an vier erinnern (Susan Weinschenk, 100 Dinge, die jeder Designer über Menschen wissen muss, 48).Trotzdem halte ich vier für eine Art hohe Erdumlaufbahn.Aber das liegt daran, dass Bob Martin mein Denken verändert hat.

In Sauberer Code, Onkel Bob plädiert für drei als allgemeine Obergrenze für die Anzahl der Parameter.Er stellt die radikale Behauptung auf (40):

Die ideale Anzahl von Argumenten für eine Funktion ist Null (niladisch).Als nächstes kommt eins (monadisch), dicht gefolgt von zwei (dyadisch).Drei Argumente (triadisch) sollten nach Möglichkeit vermieden werden.Mehr als drei (polyadisch) erfordern eine ganz besondere Begründung – und sollten dann sowieso nicht verwendet werden.

Er sagt dies aus Gründen der Lesbarkeit;sondern auch wegen der Testbarkeit:

Stellen Sie sich vor, wie schwierig es ist, alle Testfälle zu schreiben, um sicherzustellen, dass alle verschiedenen Argumentkombinationen ordnungsgemäß funktionieren.

Ich ermutige Sie, sich ein Exemplar seines Buches zu besorgen und seine vollständige Diskussion der Funktionsargumente (40-43) zu lesen.

Ich stimme denen zu, die das Prinzip der Einzelverantwortung erwähnt haben.Es fällt mir schwer zu glauben, dass eine Klasse, die mehr als zwei oder drei Werte/Objekte ohne vernünftige Standardwerte benötigt, wirklich nur eine Verantwortung hat und nicht besser dran wäre, wenn eine andere Klasse extrahiert würde.

Wenn Sie nun Ihre Abhängigkeiten über den Konstruktor einfügen, treffen Bob Martins Argumente darüber, wie einfach es ist, den Konstruktor aufzurufen, nicht so sehr zu (denn dann gibt es normalerweise nur einen Punkt in Ihrer Anwendung, an dem Sie das verknüpfen, oder Sie sogar). haben Sie ein Framework, das das für Sie erledigt).Das Prinzip der Einzelverantwortung ist jedoch weiterhin relevant:Sobald eine Klasse vier Abhängigkeiten hat, riecht es meiner Meinung nach, dass sie viel Arbeit leistet.

Wie bei allen Dingen in der Informatik gibt es jedoch zweifellos gültige Fälle für eine große Anzahl von Konstruktorparametern.Verzerren Sie Ihren Code nicht, um die Verwendung einer großen Anzahl von Parametern zu vermeiden.Wenn Sie jedoch eine große Anzahl von Parametern verwenden, sollten Sie innehalten und darüber nachdenken, da dies bedeuten kann, dass Ihr Code bereits verzerrt ist.

Bleiben Sie in Ihrem Fall beim Konstruktor.Die Informationen gehören in den Bereich „Kunde“ und 4 Felder sind in Ordnung.

Wenn Sie viele erforderliche und optionale Felder haben, ist der Konstruktor nicht die beste Lösung.Wie @boojiboy sagte, ist es schwer zu lesen und es ist auch schwierig, Client-Code zu schreiben.

@contagious schlug die Verwendung des Standardmusters und der Setter für optionale Attribute vor.Das erfordert, dass die Felder veränderbar sind, aber das ist ein kleines Problem.

Joshua Block über Effective Java 2 sagt, dass Sie in diesem Fall einen Builder in Betracht ziehen sollten.Ein Beispiel aus dem Buch:

 public class NutritionFacts {  
   private final int servingSize;  
   private final int servings;  
   private final int calories;  
   private final int fat;  
   private final int sodium;  
   private final int carbohydrate;  

   public static class Builder {  
     // required parameters  
     private final int servingSize;  
     private final int servings;  

     // optional parameters  
     private int calories         = 0;  
     private int fat              = 0;  
     private int carbohydrate     = 0;  
     private int sodium           = 0;  

     public Builder(int servingSize, int servings) {  
      this.servingSize = servingSize;  
       this.servings = servings;  
    }  

     public Builder calories(int val)  
       { calories = val;       return this; }  
     public Builder fat(int val)  
       { fat = val;            return this; }  
     public Builder carbohydrate(int val)  
       { carbohydrate = val;   return this; }  
     public Builder sodium(int val)  
       { sodium = val;         return this; }  

     public NutritionFacts build() {  
       return new NutritionFacts(this);  
     }  
   }  

   private NutritionFacts(Builder builder) {  
     servingSize       = builder.servingSize;  
     servings          = builder.servings;  
     calories          = builder.calories;  
     fat               = builder.fat;  
     soduim            = builder.sodium;  
     carbohydrate      = builder.carbohydrate;  
   }  
}  

Und dann verwenden Sie es so:

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).
      calories(100).sodium(35).carbohydrate(27).build();

Das obige Beispiel stammt aus Effektives Java 2

Und das gilt nicht nur für den Konstruktor.Unter Berufung auf Kent Beck in Implementierungsmuster:

setOuterBounds(x, y, width, height);
setInnerBounds(x + 2, y + 2, width - 4, height - 4);

Wenn Sie das Rechteck explizit als Objekt verwenden, wird der Code besser erklärt:

setOuterBounds(bounds);
setInnerBounds(bounds.expand(-2));

Ich denke, die „reine OOP“-Antwort lautet: Wenn Operationen an der Klasse ungültig sind, wenn bestimmte Mitglieder nicht initialisiert sind, müssen diese Mitglieder vom Konstruktor festgelegt werden.Es gibt immer den Fall, dass Standardwerte verwendet werden können, aber ich gehe davon aus, dass wir diesen Fall nicht berücksichtigen.Dies ist ein guter Ansatz, wenn die API repariert ist, da das Ändern des einzelnen zulässigen Konstruktors nach der Veröffentlichung der API für Sie und alle Benutzer Ihres Codes ein Albtraum sein wird.

Was ich in C# über die Designrichtlinien verstehe, ist, dass dies nicht unbedingt die einzige Möglichkeit ist, mit der Situation umzugehen.Insbesondere bei WPF-Objekten werden Sie feststellen, dass .NET-Klassen tendenziell Parameterlose Konstruktoren bevorzugen und Ausnahmen auslösen, wenn die Daten vor dem Aufruf der Methode nicht in einen gewünschten Zustand initialisiert wurden.Dies ist jedoch wahrscheinlich hauptsächlich spezifisch für komponentenbasiertes Design;Ich kann mir kein konkretes Beispiel für eine .NET-Klasse vorstellen, die sich auf diese Weise verhält.In Ihrem Fall würde es definitiv zu einem erhöhten Testaufwand führen, um sicherzustellen, dass die Klasse niemals im Datenspeicher gespeichert wird, es sei denn, die Eigenschaften wurden validiert.Ehrlich gesagt würde ich aus diesem Grund den Ansatz „Der Konstruktor legt die erforderlichen Eigenschaften fest“ bevorzugen, wenn Ihre API entweder in Stein gemeißelt oder nicht öffentlich ist.

Das Einzige, was ich Bin Sicher ist, dass es wahrscheinlich unzählige Methoden gibt, die dieses Problem lösen können, und jede von ihnen bringt ihre eigenen Probleme mit sich.Das Beste, was Sie tun können, ist, so viele Muster wie möglich zu lernen und das beste für den Job auszuwählen.(Ist das nicht so eine Ausrede einer Antwort?)

Ich denke, bei Ihrer Frage geht es mehr um das Design Ihrer Klassen als um die Anzahl der Argumente im Konstruktor.Wenn ich 20 Daten (Argumente) benötigen würde, um ein Objekt erfolgreich zu initialisieren, würde ich wahrscheinlich darüber nachdenken, die Klasse aufzulösen.

Steve McConnell schreibt in Code Complete, dass es den Leuten schwerfällt, mehr als 7 Dinge gleichzeitig im Kopf zu behalten, also ist das die Zahl, unter der ich zu bleiben versuche.

Wenn Sie unappetitlich viele Argumente haben, packen Sie sie einfach in Strukturen/POD-Klassen zusammen, die vorzugsweise als innere Klassen der Klasse deklariert werden, die Sie erstellen.Auf diese Weise können Sie die Felder weiterhin benötigen und gleichzeitig den Code, der den Konstruktor aufruft, einigermaßen lesbar machen.

Ich denke, es hängt alles von der Situation ab.Für so etwas wie Ihr Beispiel, eine Kundenklasse, würde ich nicht das Risiko eingehen, dass diese Daten bei Bedarf undefiniert sind.Auf der anderen Seite würde die Übergabe einer Struktur die Argumentliste aufräumen, aber Sie müssten in der Struktur immer noch viele Dinge definieren.

Ich denke, der einfachste Weg wäre, für jeden Wert einen akzeptablen Standardwert zu finden.In diesem Fall sieht es so aus, als müsste jedes Feld erstellt werden. Überladen Sie also möglicherweise den Funktionsaufruf, damit, wenn etwas im Aufruf nicht definiert ist, es auf einen Standardwert gesetzt wird.

Erstellen Sie dann Getter- und Setter-Funktionen für jede Eigenschaft, damit die Standardwerte geändert werden können.

Java-Implementierung:

public static void setEmail(String newEmail){
    this.email = newEmail;
}

public static String getEmail(){
    return this.email;
}

Dies ist auch eine gute Vorgehensweise, um die Sicherheit Ihrer globalen Variablen zu gewährleisten.

Der Stil spielt eine große Rolle, und meiner Meinung nach sollte das Design geändert werden, wenn es einen Konstruktor mit mehr als 20 Argumenten gibt.Stellen Sie angemessene Standardvorgaben bereit.

Ich stimme der von Boojiboy erwähnten Beschränkung auf 7 Artikel zu.Darüber hinaus kann es sich lohnen, sich anonyme (oder spezialisierte) Typen, IDictionary oder die Indirektion über einen Primärschlüssel zu einer anderen Datenquelle anzusehen.

Ich würde ähnliche Felder in ein eigenes Objekt mit eigener Konstruktions-/Validierungslogik einkapseln.

Sagen Sie zum Beispiel, wenn Sie haben

  • Geschäftstelefon
  • Geschäftsadresse
  • Festnetztelefon
  • Heimatadresse

Ich würde eine Klasse erstellen, die Telefonnummer und Adresse zusammen mit einem Tag speichert, das angibt, ob es sich um eine „private“ oder eine „geschäftliche“ Telefonnummer/Adresse handelt.Und dann reduzieren Sie die 4 Felder auf ein bloßes Array.

ContactInfo cinfos = new ContactInfo[] {
    new ContactInfo("home", "+123456789", "123 ABC Avenue"),
    new ContactInfo("biz", "+987654321", "789 ZYX Avenue")
};

Customer c = new Customer("john", "doe", cinfos);

Dadurch sollte es weniger wie Spaghetti aussehen.

Wenn Sie viele Felder haben, muss es sicherlich ein Muster geben, das Sie extrahieren können und das eine schöne eigene Funktionseinheit ergibt.Und sorgen Sie auch für besser lesbaren Code.

Und die folgenden sind auch mögliche Lösungen:

  • Verteilen Sie die Validierungslogik, anstatt sie in einer einzigen Klasse zu speichern.Validieren Sie, wenn der Benutzer sie eingibt, und validieren Sie sie dann erneut auf Datenbankebene usw.
  • Mach ein CustomerFactory Klasse, die mir beim Konstruieren helfen würde CustomerS
  • Die Lösung von @marcio ist auch interessant ...

Verwenden Sie einfach Standardargumente.In einer Sprache, die Standardmethodenargumente unterstützt (z. B. PHP), könnten Sie dies in der Methodensignatur tun:

public function doSomethingWith($this = val1, $this = val2, $this = val3)

Es gibt andere Möglichkeiten, Standardwerte zu erstellen, beispielsweise in Sprachen, die das Überladen von Methoden unterstützen.

Natürlich können Sie bei der Deklaration der Felder auch Standardwerte festlegen, wenn Sie dies für angemessen halten.

Es kommt wirklich nur darauf an, ob es für Sie angemessen ist, diese Standardwerte festzulegen, oder ob Ihre Objekte beim Bau ständig festgelegt werden sollten.Das ist wirklich eine Entscheidung, die nur Sie treffen können.

Sofern es sich nicht um mehr als ein Argument handelt, verwende ich immer Arrays oder Objekte als Konstruktorparameter und verlasse mich auf die Fehlerprüfung, um sicherzustellen, dass die erforderlichen Parameter vorhanden sind.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top