Konstruktor mit tonnenweise Parametern gegen Builder-Muster

21

Wenn Ihre Klasse über einen Konstruktor mit vielen Parametern verfügt, z. B. mehr als 4, handelt es sich höchstwahrscheinlich um einen Codegeruch . Sie müssen erneut prüfen, ob die Klasse SRP erfüllt .

Aber was ist, wenn wir ein Objekt erstellen und ablegen, das von 10 oder mehr Parametern abhängt, und am Ende alle diese Parameter über das Builder-Muster festlegen? Stellen Sie sich vor, Sie erstellen ein PersonTyp-Objekt mit seinen persönlichen Daten, Arbeitsdaten, Freunde-Daten, Interessen-Daten, Bildungsdaten und so weiter. Das ist schon gut, aber du hast irgendwie gleich mehr als 4 Parameter eingestellt, oder? Warum werden diese beiden Fälle nicht als gleich angesehen?

Narek
quelle

Antworten:

24

Das Builder-Muster löst nicht das „Problem“ vieler Argumente. Aber warum sind viele Argumente problematisch?

  • Sie weisen darauf hin, dass Ihre Klasse möglicherweise zu viel tut . Es gibt jedoch viele Typen, die legitimerweise viele Mitglieder enthalten, die nicht sinnvoll gruppiert werden können.
  • Das Testen und Verstehen einer Funktion mit vielen Eingaben wird exponentiell komplizierter - im wahrsten Sinne des Wortes!
  • Wenn die Sprache keine benannten Parameter anbietet, ist ein Funktionsaufruf nicht selbstdokumentierend . Das Lesen eines Funktionsaufrufs mit vielen Argumenten ist ziemlich schwierig, da Sie keine Ahnung haben, was der 7. Parameter tun soll. Sie würden es nicht einmal bemerken, wenn das fünfte und sechste Argument versehentlich vertauscht würden, insbesondere wenn Sie eine dynamisch getippte Sprache verwenden oder wenn alles zufällig eine Zeichenfolge ist oder wenn der letzte Parameter trueaus irgendeinem Grund angegeben ist.

Fälschung benannter Parameter

Die Pattern - Generator - Adressen nur eines dieser Probleme, nämlich die Wartbarkeit Anliegen der Funktionsaufrufe mit vielen Argumenten * . Also ein Funktionsaufruf wie

MyClass o = new MyClass(a, b, c, d, e, f, g);

könnte werden

MyClass o = MyClass.builder()
  .a(a).b(b).c(c).d(d).e(e).f(f).g(g)
  .build();

∗ Das Builder-Muster war ursprünglich als repräsentationsunabhängiger Ansatz zum Zusammensetzen von zusammengesetzten Objekten gedacht. Dies ist ein weitaus größerer Anspruch als nur die genannten Argumente für Parameter. Insbesondere erfordert das Buildermuster keine fließende Schnittstelle.

Dies bietet zusätzliche Sicherheit, da es explodiert, wenn Sie eine Buildermethode aufrufen, die nicht vorhanden ist, andernfalls jedoch nichts bringt, was ein Kommentar im Konstruktoraufruf nicht hätte. Das manuelle Erstellen eines Builders erfordert außerdem Code, und mehr Code kann immer mehr Fehler enthalten.

In Sprachen, in denen es einfach ist, einen neuen Werttyp zu definieren, ist es meiner Meinung nach viel besser, Mikrotypen / kleine Typen zu verwenden, um benannte Argumente zu simulieren. Der Name ist so, weil die Typen sehr klein sind, aber am Ende tippt man viel mehr ;-)

MyClass o = new MyClass(
  new MyClass.A(a), new MyClass.B(b), new MyClass.C(c),
  new MyClass.D(d), new MyClass.E(e), new MyClass.F(f),
  new MyClass.G(g));

Offensichtlich sind die Typnamen A, B, C, ... sollten selbsterklärend Namen sein , die die Bedeutung der den Parameter, die oft die gleichen Namen zeigen , wie Sie den Parameter - Variable geben würden. Verglichen mit dem Builder-for-Named-Arguments-Idiom ist die erforderliche Implementierung viel einfacher und daher weniger fehleranfällig. Zum Beispiel (mit Java-ish-Syntax):

class MyClass {
  ...
  public static class A {
    public final int value;
    public A(int a) { value = a; }
  }
  ...
}

Mit dem Compiler können Sie sicherstellen, dass alle Argumente angegeben wurden. Mit einem Builder müssten Sie manuell nach fehlenden Argumenten suchen oder einen Zustandsautomaten in das System des Host-Sprachtyps codieren - beide enthalten wahrscheinlich Fehler.

Es gibt einen anderen gängigen Ansatz zum Simulieren benannter Argumente: Ein einzelnes abstraktes Parameterobjekt , das eine Inline-Klassensyntax verwendet, um alle Felder zu initialisieren. In Java:

MyClass o = new MyClass(new MyClass.Arguments(){{ argA = a; argB = b; argC = c; ... }});

class MyClass {
  ...
  public static abstract class Arguments {
    public int argA;
    public String ArgB;
    ...
  }
}

Es ist jedoch möglich, Felder zu vergessen, und dies ist eine recht sprachspezifische Lösung (die ich in JavaScript, C # und C gesehen habe).

Glücklicherweise kann der Konstruktor immer noch alle Argumente validieren, was nicht der Fall ist, wenn Ihre Objekte in einem teilweise konstruierten Zustand erstellt werden, und den Benutzer dazu auffordern, weitere Argumente über Setter oder eine init()Methode bereitzustellen - diese erfordern nur den geringsten Programmieraufwand es ist schwieriger, korrekte Programme zu schreiben .

Während es also viele Ansätze gibt, um die „vielen unbenannten Parameter machen es schwierig, den Code zu warten“, bleiben andere Probleme bestehen.

Annäherung an das Root-Problem

Zum Beispiel das Testbarkeitsproblem. Wenn ich Komponententests schreibe, muss ich in der Lage sein, Testdaten einzuspeisen und Testimplementierungen bereitzustellen, um Abhängigkeiten und Vorgänge mit externen Nebenwirkungen zu verschleiern. Ich kann das nicht tun, wenn Sie Klassen in Ihrem Konstruktor instanziieren. Sofern Ihre Klasse nicht für die Erstellung anderer Objekte verantwortlich ist, sollte sie keine nicht trivialen Klassen instanziieren. Dies geht einher mit dem Problem der einmaligen Verantwortung. Je fokussierter die Verantwortung einer Klasse ist, desto einfacher ist es zu testen (und oft einfacher zu bedienen).

Der einfachste und häufig beste Ansatz besteht darin, dass der Konstruktor vollständig konstruierte Abhängigkeiten als Parameter verwendet . Dadurch wird jedoch die Verantwortung für die Verwaltung von Abhängigkeiten auf den Aufrufer verlagert. Dies ist auch nicht ideal, es sei denn, die Abhängigkeiten sind unabhängige Entitäten in Ihrem Domänenmodell.

Manchmal werden stattdessen (abstrakte) Fabriken oder Frameworks mit vollständiger Abhängigkeitsinjektion verwendet, obwohl dies in den meisten Anwendungsfällen zu viel des Guten sein kann. Diese reduzieren insbesondere nur die Anzahl der Argumente, wenn es sich bei vielen dieser Argumente um quasi-globale Objekte oder Konfigurationswerte handelt, die sich zwischen der Objektinstanziierung nicht ändern. Wenn zum Beispiel Parameter aund dglobal wären , würden wir bekommen

Dependencies deps = new Dependencies(a, d);
...
MyClass o = deps.newMyClass(b, c, e, f, g);

class MyClass {
  MyClass(Dependencies deps, B b, C c, E e, F f, G g) {
    this.depA = deps.newDepA(b, c);
    this.depB = deps.newDepB(e, f);
    this.g = g;
  }
  ...
}

class Dependencies {
  private A a;
  private D d;
  public Dependencies(A a, D d) { this.a = a; this.d = d; }
  public DepA newDepA(B b, C c) { return new DepA(a, b, c); }
  public DepB newDepB(E e, F f) { return new DepB(d, e, f); }
  public MyClass newMyClass(B b, C c, E e, F f, G g) {
    return new MyClass(deps, b, c, e, f, g);
  }
}

Abhängig von der Anwendung kann dies ein grundlegender Unterschied sein, bei dem die Factory-Methoden fast keine Argumente haben, da alle vom Abhängigkeits-Manager bereitgestellt werden können, oder es kann sich um eine große Menge Code handeln, die die Instanziierung erschwert, ohne dass ein offensichtlicher Vorteil erkennbar ist. Solche Fabriken sind für die Zuordnung von Schnittstellen zu konkreten Typen weitaus nützlicher als für die Verwaltung von Parametern. Dieser Ansatz versucht jedoch, das Grundproblem zu vieler Parameter zu lösen, anstatt es nur mit einer ziemlich fließenden Oberfläche zu verbergen.

amon
quelle
Ich würde wirklich gegen einen selbstdokumentierenden Teil argumentieren. Wenn Sie eine anständige IDE + anständige Kommentare haben, ist die Konstruktor- und Parameterdefinition die meiste Zeit einen Tastendruck entfernt.
JavierIEH
In Android Studio 3.0 wird der Parametername im Konstruktor neben dem Wert angezeigt, der in einem Konstruktoraufruf übergeben wird. zB: neues A (operand1: 34, operand2: 56); operand1 und operand2 sind die Parameternamen im Konstruktor. Sie werden von der IDE angezeigt, um den Code besser lesbar zu machen. Sie müssen also nicht zur Definition gehen, um herauszufinden, was der Parameter ist.
Granat
9

Das Builder-Muster löst nichts für Sie und behebt keine Entwurfsfehler.

Wenn Sie eine Klasse haben, die 10 Parameter benötigt, um konstruiert zu werden, wird Ihr Entwurf nicht plötzlich besser, wenn Sie einen Builder dazu bringen, sie zu konstruieren. Sie sollten sich für die Umgestaltung der betreffenden Klasse entscheiden.

Wenn Sie dagegen eine Klasse haben, beispielsweise ein einfaches DTO, bei dem bestimmte Attribute der Klasse optional sind, kann das Builder-Muster die Konstruktion des Objekts vereinfachen.

Andy
quelle
1

Jeder Teil, z. B. persönliche Informationen, Arbeitsinformationen (jede Beschäftigung "Stint"), jeder Freund usw., sollte in ihre eigenen Objekte umgewandelt werden.

Überlegen Sie, wie Sie eine Update-Funktionalität implementieren würden. Der Benutzer möchte einen neuen Arbeitsverlauf hinzufügen . Der Benutzer möchte weder andere Teile der Informationen noch den bisherigen Arbeitsverlauf ändern. Behalten Sie diese einfach bei.

Wenn es viele Informationen gibt, ist es unvermeidlich, die Informationen Stück für Stück aufzubauen. Sie können diese Teile logisch gruppieren - basierend auf der menschlichen Intuition (was die Verwendung für einen Programmierer erleichtert) oder basierend auf dem Verwendungsmuster (welche Informationen normalerweise gleichzeitig aktualisiert werden).

Das Builder-Muster ist nur eine Möglichkeit, eine Liste (geordnet oder ungeordnet) von typisierten Argumenten zu erfassen, die dann an den tatsächlichen Konstruktor übergeben werden können (oder der Konstruktor kann die erfassten Argumente des Builders lesen).

Die Richtlinie 4 ist nur eine Faustregel. Es gibt Situationen, in denen mehr als 4 Argumente erforderlich sind und in denen es keinen vernünftigen oder logischen Weg gibt, sie zu gruppieren. In diesen Fällen kann es sinnvoll sein, eine zu erstellen, structdie direkt oder mit Eigenschaftssetzern gefüllt werden kann. Beachten Sie, dass das structund der Builder in diesem Fall einen sehr ähnlichen Zweck erfüllen.

In Ihrem Beispiel haben Sie jedoch bereits beschrieben, wie Sie sie logisch gruppieren können. Wenn Sie mit einer Situation konfrontiert sind, in der dies nicht zutrifft, können Sie dies möglicherweise anhand eines anderen Beispiels veranschaulichen.

rwong
quelle