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
true
aus 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 a
und d
global 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.