Costruttore con tonnellate di parametri rispetto al modello del costruttore


21

È risaputo che se la tua classe ha un costruttore con molti parametri, diciamo più di 4, probabilmente è un odore di codice . È necessario riconsiderare se la classe soddisfa SRP .

Ma cosa succede se costruiamo e obiettiamo che dipende da 10 o più parametri, e alla fine finiamo con l'impostazione di tutti quei parametri attraverso il modello Builder? Immagina di costruire un Personoggetto tipo con le sue informazioni personali, informazioni di lavoro, informazioni sugli amici, informazioni sugli interessi, informazioni sull'istruzione e così via. Questo è già buono, ma in qualche modo hai impostato lo stesso più di 4 parametri, giusto? Perché questi due casi non sono considerati uguali?

Risposte:


25

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 è trueper 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 ae derano 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.


Discuterei davvero contro la parte autocompensante. Se hai un IDE decente + commenti decenti, il più delle volte la definizione del costruttore e dei parametri è a portata di tasto.
JavierIEH,

In Android Studio 3.0, il nome del parametro nel costruttore viene visualizzato accanto al valore che viene passato in una chiamata del costruttore. ad es .: nuovo A (operando1: 34, operando2: 56); operando1 e operando2 sono i nomi dei parametri nel costruttore. Sono mostrati dall'IDE per rendere il codice più leggibile. Quindi, non è necessario andare alla definizione per scoprire qual è il parametro.
granato

9

Il modello del costruttore non risolve nulla per te e non corregge gli errori di progettazione.

Se hai una classe che necessita di 10 parametri da costruire, fare un builder per costruirlo non renderà improvvisamente migliore il tuo progetto. Dovresti optare per il refactoring della classe in questione.

D'altra parte, se hai una classe, forse un semplice DTO, in cui alcuni attributi della classe sono opzionali, il modello di costruzione potrebbe facilitare la costruzione di detto oggetto.


1

Ogni parte, ad esempio informazioni personali, informazioni sul lavoro (ogni "periodo di lavoro"), ogni amico, ecc., Deve essere convertito nei propri oggetti.

Considera come implementare una funzionalità di aggiornamento. L'utente vuole aggiungere una nuova cronologia di lavoro. L'utente non vuole modificare altre parti delle informazioni, né la cronologia di lavoro precedente, semplicemente mantenerle uguali.

Quando ci sono molte informazioni, costruire le informazioni un pezzo alla volta è inevitabile. Puoi raggruppare quei pezzi logicamente - in base all'intuizione umana (che ne semplifica l'uso da parte di un programmatore), o in base al modello di utilizzo (quali informazioni vengono in genere aggiornate contemporaneamente).

Il modello di generatore è solo un modo per acquisire un elenco (ordinato o non ordinato) di argomenti digitati, che possono quindi essere passati al costruttore effettivo (oppure il costruttore può leggere gli argomenti acquisiti del costruttore).

La linea guida di 4 è solo una regola empirica. Ci sono situazioni che richiedono più di 4 argomenti e che non esiste un modo logico o logico per raggrupparli. In questi casi, può avere senso creare un oggetto structche può essere popolato direttamente o con setter di proprietà. Si noti che structin questo caso e il costruttore ha uno scopo molto simile.

Nel tuo esempio, tuttavia, hai già descritto quale sarebbe un modo logico per raggrupparli. Se stai affrontando una situazione in cui ciò non è vero, forse puoi illustrarlo con un esempio diverso.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.