Il Builder Pattern non risolve il "problema" di molti argomenti. Ma perché molti argomenti sono problematici?
- Indicano che la tua classe potrebbe fare troppo . Tuttavia, ci sono molti tipi che contengono legittimamente molti membri che non possono essere raggruppati in modo ragionevole.
- Testare e comprendere una funzione con molti input diventa esponenzialmente più complicato - letteralmente!
- Quando la lingua non offre parametri denominati, una chiamata di funzione non è auto-documentata . Leggere una chiamata di funzione con molti argomenti è piuttosto difficile perché non hai idea di cosa dovrebbe fare il settimo parametro. Non ti accorgeresti nemmeno se il 5 ° e il 6 ° argomento sono stati scambiati accidentalmente, specialmente se sei in una lingua tipizzata in modo dinamico o tutto sembra essere una stringa, o quando l'ultimo parametro è
true
per qualche motivo.
Falsificazione di parametri denominati
Il Builder Pattern risolve solo uno di questi problemi, vale a dire le preoccupazioni di manutenibilità delle chiamate di funzioni con molti argomenti ∗ . Quindi una chiamata di funzione come
MyClass o = new MyClass(a, b, c, d, e, f, g);
potrebbe diventare
MyClass o = MyClass.builder()
.a(a).b(b).c(c).d(d).e(e).f(f).g(g)
.build();
∗ Il modello Builder era originariamente inteso come un approccio agnostico di rappresentazione per assemblare oggetti compositi, che è un'aspirazione di gran lunga maggiore rispetto agli argomenti nominati per i parametri. In particolare, il modello del generatore non richiede un'interfaccia fluida.
Questo offre un po 'di sicurezza in più poiché esploderà se invochi un metodo builder che non esiste, ma per il resto non ti porta nulla che un commento nella chiamata del costruttore non avrebbe. Inoltre, la creazione manuale di un builder richiede codice e più codice può sempre contenere più bug.
Nelle lingue in cui è facile definire un nuovo tipo di valore, ho scoperto che è meglio usare i tipi microtyping / tiny per simulare argomenti con nome. Si chiama così perché i tipi sono davvero piccoli, ma finisci per scrivere molto di più ;-)
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));
Ovviamente, i nomi di tipo A
, B
, C
, ... dovrebbero essere i nomi di auto-documentazione che illustrano il significato del parametro, spesso lo stesso nome da darle la variabile parametro. Rispetto al linguaggio del builder-for-named -argomento, l'implementazione richiesta è molto più semplice e quindi meno probabile che contenga bug. Ad esempio (con sintassi Java-ish):
class MyClass {
...
public static class A {
public final int value;
public A(int a) { value = a; }
}
...
}
Il compilatore ti aiuta a garantire che tutti gli argomenti siano stati forniti; con un Builder dovresti verificare manualmente la presenza di argomenti mancanti o codificare una macchina a stati nel sistema di tipi di lingua host, entrambi conterrebbero probabilmente dei bug.
Esiste un altro approccio comune per simulare argomenti denominati: un singolo oggetto parametro astratto che utilizza una sintassi della classe inline per inizializzare tutti i campi. 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;
...
}
}
Tuttavia, è possibile dimenticare i campi, e questa è una soluzione abbastanza specifica per la lingua (ho visto usi in JavaScript, C # e C).
Fortunatamente, il costruttore può ancora convalidare tutti gli argomenti, il che non è il caso in cui gli oggetti vengono creati in uno stato parzialmente costruito e richiedere all'utente di fornire ulteriori argomenti tramite setter o un init()
metodo: questi richiedono il minimo sforzo di codifica, ma rendono è più difficile scrivere programmi corretti .
Quindi, mentre ci sono molti approcci per affrontare i "molti parametri senza nome rendono difficile il codice per mantenere il problema", rimangono altri problemi.
Approccio al problema alla radice
Ad esempio il problema della testabilità. Quando scrivo unit test, ho bisogno della possibilità di iniettare dati di test e di fornire implementazioni di test per deridere dipendenze e operazioni che hanno effetti collaterali esterni. Non posso farlo quando crei un'istanza di classi all'interno del tuo costruttore. A meno che la responsabilità della tua classe non sia la creazione di altri oggetti, non dovrebbe creare un'istanza di classi non banali. Questo va di pari passo con il problema della responsabilità singola. Più è focalizzata la responsabilità di una classe, più facile è testarla (e spesso più facile da usare).
L'approccio più semplice e spesso migliore è che il costruttore consideri le dipendenze completamente costruite come parametro , sebbene ciò spinga la responsabilità di gestire le dipendenze per il chiamante - neanche l'ideale, a meno che le dipendenze non siano entità indipendenti nel modello di dominio.
A volte vengono utilizzate fabbriche (astratte) o strutture di iniezione a piena dipendenza , anche se potrebbero essere eccessive nella maggior parte dei casi d'uso. In particolare, questi riducono il numero di argomenti solo se molti di questi argomenti sono oggetti quasi globali o valori di configurazione che non cambiano tra le istanze degli oggetti. Ad esempio, se i parametri a
e d
erano globale-ish, ci saremmo
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);
}
}
A seconda dell'applicazione, questo potrebbe essere un punto di svolta in cui i metodi di fabbrica finiscono per non avere quasi argomenti perché tutto può essere fornito dal gestore delle dipendenze, oppure potrebbe essere una grande quantità di codice che complica l'istanza senza alcun vantaggio apparente. Tali fabbriche sono molto più utili per mappare le interfacce su tipi concreti di quanto non lo siano per la gestione dei parametri. Tuttavia, questo approccio cerca di affrontare il problema alla radice di troppi parametri piuttosto che nasconderlo con un'interfaccia piuttosto fluida.