Perché un tipo dovrebbe essere accoppiato al suo costruttore?


20

Di recente ho eliminato una mia risposta su Code Review , che è iniziata in questo modo:

private Person(PersonBuilder builder) {

Fermare. Bandiera rossa. Un PersonBuilder costruirà una persona; sa di una persona. La classe Person non dovrebbe sapere nulla di PersonBuilder: è solo un tipo immutabile. Hai creato un accoppiamento circolare qui, dove A dipende da B che dipende da A.

La persona dovrebbe semplicemente assumere i suoi parametri; un cliente disposto a creare una Persona senza costruirla dovrebbe essere in grado di farlo.

Sono stato schiaffeggiato con un voto negativo e mi è stato detto che (citando) la bandiera rossa, perché? L'implementazione qui ha la stessa forma di quella dimostrata da Joshua Bloch nel suo libro "Effective Java" (oggetto n. 2).

Quindi, sembra che il modo giusto di implementare un Builder Pattern in Java sia di rendere il builder un tipo nidificato (non è di questo che si tratta questa domanda), e quindi rendere il prodotto (la classe dell'oggetto che si sta costruendo ) prendere una dipendenza dal costruttore , in questo modo:

private StreetMap(Builder builder) {
    // Required parameters
    origin      = builder.origin;
    destination = builder.destination;

    // Optional parameters
    waterColor         = builder.waterColor;
    landColor          = builder.landColor;
    highTrafficColor   = builder.highTrafficColor;
    mediumTrafficColor = builder.mediumTrafficColor;
    lowTrafficColor    = builder.lowTrafficColor;
}

https://en.wikipedia.org/wiki/Builder_pattern#Java_example

La stessa pagina di Wikipedia per lo stesso modello Builder ha un'implementazione molto diversa (e molto più flessibile) per C #:

//Represents a product created by the builder
public class Car
{
    public Car()
    {
    }

    public int Wheels { get; set; }

    public string Colour { get; set; }
}

Come puoi vedere, il prodotto qui non sa nulla di una Builderclasse, e per quanto gli interessi potrebbe essere istanziato da una chiamata diretta del costruttore, una fabbrica astratta, ... o un costruttore - per quanto ne capisco, il il prodotto di un modello creazionale non ha mai bisogno di sapere nulla di ciò che lo crea.

Mi è stato offerto il controargomentazione (che apparentemente è esplicitamente difeso nel libro di Bloch) secondo cui un modello di costruttore potrebbe essere usato per rielaborare un tipo che avrebbe un costruttore gonfio con dozzine di argomenti opzionali. Quindi, invece di attenermi a ciò che pensavo di sapere, ho studiato un po 'su questo sito e ho scoperto che, come sospettavo, questo argomento non regge .

Quindi qual è il problema? Perché trovare soluzioni troppo ingegnerizzate a un problema che non dovrebbe nemmeno esistere in primo luogo? Se togliiamo Joshua Bloch dal suo piedistallo per un minuto, possiamo trovare un unico buon motivo valido per accoppiare due tipi concreti e definirlo una buona pratica?

Tutto questo per me puzza di programmazione di culto delle merci .


12
E questa "domanda" puzza di essere solo un furfante ...
Philip Kendall

2
@PhilipKendall forse un po '. Ma sono sinceramente interessato a capire perché ogni implementazione di un builder Java abbia questo stretto accoppiamento.
Mathieu Guindon,

19
@PhilipKendall Puzza di curiosità per me.
Albero

8
@PhilipKendall Sembra un rant, sì, ma al centro c'è una valida domanda sull'argomento. Forse il rant potrebbe essere modificato?
Andres F.

Non sono impressionato dall'argomento "troppi argomenti del costruttore". Ma sono ancora meno impressionato da entrambi questi esempi di costruttori. Forse perché gli esempi sono troppo semplici per dimostrare un comportamento non banale, ma l'esempio Java si presenta come un'interfaccia fluida contorta , e l'esempio C # è solo un modo per implementare le proprietà. Nessuno dei due esempi ha alcuna forma di convalida dei parametri; un costruttore convaliderebbe almeno il tipo e il numero di argomenti forniti, il che significa che vince il costruttore con troppi argomenti.
Robert Harvey,

Risposte:


22

Non sono d'accordo con la tua affermazione che si tratta di una bandiera rossa. Non sono inoltre d'accordo con la tua rappresentazione del contrappunto, secondo cui esiste un modo giusto di rappresentare un modello di costruttore.

Per me c'è una domanda: il Builder è una parte necessaria dell'API del tipo? Qui, PersonBuilder è una parte necessaria dell'API Person?

È del tutto ragionevole che una classe Person verificata per la validità, immutabile e / o strettamente incapsulata sarà necessariamente creata attraverso un Builder che fornisce (indipendentemente dal fatto che quel builder sia nidificato o adiacente). In tal modo, la Persona può mantenere tutti i suoi campi privati ​​o pacchetto-privati ​​e finali e anche lasciare la classe aperta per modifiche se si tratta di un lavoro in corso. (Potrebbe finire chiuso per modifica e aperto per estensione, ma non è importante in questo momento, ed è particolarmente controverso per oggetti di dati immutabili.)

Se una Persona viene necessariamente creata tramite un PersonBuilder fornito come parte di un singolo progetto API a livello di pacchetto, un accoppiamento circolare va bene e una bandiera rossa non è garantita qui. In particolare, l'hai dichiarato un fatto incontrovertibile e non un'opinione o una scelta di progettazione dell'API, quindi ciò potrebbe aver contribuito a una cattiva risposta. Non ti avrei sottovalutato, ma non do la colpa a qualcun altro per averlo fatto, e lascerò il resto della discussione su "ciò che merita un downvote" al centro assistenza e alla meta di Code Review . I downvotes si verificano; non è uno "schiaffo".

Ovviamente, se vuoi lasciare molti modi aperti per costruire un oggetto Person, allora PersonBuilder potrebbe diventare un consumatore o un'utilitàdella classe Person, da cui la persona non dovrebbe dipendere direttamente. Questa scelta sembra più flessibile - chi non vorrebbe più opzioni di creazione di oggetti? - ma per mantenere le garanzie (come l'immutabilità o la serializzabilità) all'improvviso la Persona deve esporre un diverso tipo di tecnica di creazione pubblica, come un costruttore molto lungo . Se il Builder ha lo scopo di rappresentare o sostituire un costruttore con molti campi opzionali o un costruttore aperto alla modifica, allora il costruttore lungo della classe potrebbe essere un dettaglio di implementazione che è meglio lasciare nascosto. (Ricorda anche che un costruttore ufficiale e necessario non ti impedisce di scrivere la tua utility di costruzione, solo che la tua utility di costruzione può consumare il Builder come API come nella mia domanda originale sopra.)


ps: noto che l'esempio di revisione del codice che hai elencato ha una persona immutabile, ma il controesempio di Wikipedia elenca un'auto mutabile con getter e setter. Potrebbe essere più facile vedere i macchinari necessari omessi dall'auto se si mantengono coerenti la lingua e gli invarianti.


Penso di vedere il tuo punto di vista sul trattare il costruttore come un dettaglio di implementazione. Non sembra che Effective Java trasmetta correttamente questo messaggio quindi (non ho / non ho letto quel libro), lasciando un sacco di sviluppatori Java junior (?) Supponendo che "sia così" indipendentemente dal buon senso . Concordo sul fatto che gli esempi di Wikipedia sono negativi per il confronto .. ma hey è Wikipedia .. e FWIW in realtà non mi interessa il downvote; la risposta eliminata si trova comunque con un punteggio netto positivo (e c'è la mia possibilità di segnare finalmente un [distintivo: pressione dei pari]).
Mathieu Guindon,

1
Tuttavia, non riesco a scrollarmi di dosso l'idea che un tipo con troppi argomenti costruttivi sia un problema di progettazione (odori di rottura di SRP), che un modello di costruttore si inserisce rapidamente sotto un tappeto.
Mathieu Guindon,

3
@ Mat'sMug Il mio post qui sopra dà per scontato che la classe ha bisogno di tanti argomenti di costruzione . Allo stesso modo lo tratterei come un odore di design se stessimo parlando di un componente di logica aziendale arbitraria, ma per un oggetto dati, non è una questione per un tipo avere dieci o venti attributi diretti. Ad esempio, non è una violazione di SRP che un oggetto rappresenti un singolo record fortemente tipizzato in un registro .
Jeff Bowman supporta Monica

1
Giusto. String(StringBuilder)Tuttavia non ho ancora il costruttore, che sembra obbedire a quel "principio" in cui è normale che un tipo abbia una dipendenza dal suo costruttore. Ero sbalordito quando ho visto quel sovraccarico del costruttore. Non è che un Stringabbia 42 parametri del costruttore ...
Mathieu Guindon,

3
@Luaan Odd. Non l'ho mai visto letteralmente usato e non sapevo che esistesse; toString()è universale.
Chrylis

7

Capisco quanto possa essere fastidioso quando in un dibattito si usa "appello all'autorità". Gli argomenti dovrebbero valere sulla propria IMO e sebbene non sia sbagliato indicare il consiglio di una persona così rispettata, in realtà non può essere considerato un argomento completo in sé poiché sappiamo che il Sole viaggia intorno alla terra perché Aristotele lo ha detto .

Detto questo, penso che la premessa del tuo argomento sia la stessa. Non dovremmo mai accoppiare due tipi concreti e chiamarlo una buona pratica perché ... Qualcuno l'ha detto?

Affinché l'argomento che l'accoppiamento sia problematico sia valido, devono esserci problemi specifici che crea. Se questo approccio non è soggetto a tali problemi, non è possibile argomentare logicamente che questa forma di accoppiamento sia problematica. Quindi, se vuoi esprimere il tuo punto qui, penso che devi mostrare come questo approccio sia soggetto a questi problemi e / o in che modo il disaccoppiamento del costruttore migliorerà un design.

Di persona, faccio fatica a vedere come l'approccio accoppiato creerà problemi. Le classi sono completamente accoppiate, ma il builder esiste solo per creare istanze della classe. Un altro modo di vederlo sarebbe come un'estensione del costruttore.


Quindi ... maggiore accoppiamento, minore coesione => lieto fine? hmm ... vedo il punto sul costruttore "estendere il costruttore", ma per far apparire un altro esempio, non riesco a vedere cosa Stringsta facendo un sovraccarico del costruttore che accetta un StringBuilderparametro (anche se è discutibile se a StringBuildersia un costruttore , ma quello è un'altra storia).
Mathieu Guindon,

4
@ Mat'sMug Per essere chiari, non ho davvero una forte sensazione in questo senso. Non tendo ad usare il modello builder perché non creo davvero classi che hanno un sacco di input di stato. Generalmente decomponerei una tale classe per motivi a cui alludi altrove. Tutto quello che sto dicendo è che se puoi mostrare come questo approccio porti a un problema, non importa se Dio è sceso e ha consegnato quel modello di costruttore a Mosè su una tavoletta di pietra. Hai vinto'. D'altra parte se non puoi ...
JimmyJames

Un uso legittimo del modello di builder che ho nella mia base di codice è quello di deridere l'API VBIDE; in pratica fornisci il contenuto della stringa di un modulo di codice e il builder ti dà una VBEfinta, completa di finestre, riquadro di codice attivo, un progetto e un componente con un modulo che contiene la stringa che hai fornito. Senza di essa, non so come sarei in grado di scrivere l'80% dei test su Rubberduck , almeno senza pugnalarmi negli occhi ogni volta che li guardo.
Mathieu Guindon,

@ Mat'sMug Può essere davvero utile per smascherare i dati in oggetti immutabili. Questi tipi di oggetti tendono ad essere sacchi di proprietà, cioè non proprio OO. L'intero spazio del problema è una PITA tale che qualsiasi cosa che aiuti a fare verrà utilizzata, indipendentemente dal fatto che seguano o meno le buone pratiche.
JimmyJames,

4

Per rispondere al motivo per cui un tipo dovrebbe essere accoppiato con un costruttore, è necessario capire perché qualcuno dovrebbe usare un costruttore in primo luogo. In particolare: Bloch consiglia di utilizzare un builder quando si dispone di un gran numero di parametri del costruttore. Questo non è l'unico motivo per usare un costruttore, ma suppongo che sia il più comune. In breve, il builder è un sostituto di quei parametri del costruttore e spesso il builder viene dichiarato nella stessa classe che sta costruendo. Pertanto, la classe che racchiude già conosce il builder e passarlo come argomento del costruttore non cambia questo. Ecco perché un tipo dovrebbe essere accoppiato con il suo builder.

Perché avere un gran numero di parametri implica che dovresti usare un builder? Bene, avere un gran numero di parametri non è raro nel mondo reale, né viola il principio della singola responsabilità. Fa anche schifo quando hai dieci parametri String e stai cercando di ricordare quali sono e quali sono la quinta o la sesta stringa che dovrebbero essere nulle. Un builder semplifica la gestione dei parametri e può anche fare altre cose interessanti che dovresti leggere sul libro per scoprire.

Quando usi un costruttore che non è associato al suo tipo? Generalmente quando i componenti di un oggetto non sono immediatamente disponibili e gli oggetti che hanno quei pezzi non hanno bisogno di conoscere il tipo che stai cercando di costruire o stai aggiungendo un builder per una classe su cui non hai il controllo .


Stranamente, le uniche volte in cui avevo bisogno di un builder erano quando avevo un tipo che non aveva una forma definitiva, come questo MockVbeBuilder , che costruisce una simulazione dell'API di un IDE; un test potrebbe solo bisogno di un semplice modulo di codice, un altro test potrebbe avere bisogno di due moduli e un modulo di classe - IMO che è ciò che builder di GoF è per. Detto questo, potrei iniziare a usarlo per un'altra classe con un costruttore pazzo ... ma l'accoppiamento non sembra proprio giusto.
Mathieu Guindon,

1
@ Mat's Mug La classe che hai presentato sembra più rappresentativa del modello di progettazione Builder (da Design Patterns di Gamma, et al.), Non necessariamente una semplice classe di costruzione. Entrambi costruiscono cose, ma non sono la stessa cosa. Inoltre, non hai mai "bisogno" di un costruttore, semplifica semplicemente altre cose.
Old Fat Ned

1

il prodotto di un modello creazionale non ha mai bisogno di sapere nulla di ciò che lo crea.

La Personclasse non sa cosa la sta creando. Ha solo un costruttore con un parametro, e allora? Il nome del tipo di parametro termina con ... Builder che non forza davvero nulla. Dopotutto è solo un nome.

Il builder è un oggetto di configurazione glorificato 1 . E un oggetto di configurazione è in realtà solo un raggruppamento di proprietà che appartengono l'una all'altra. Se si appartengono solo allo scopo di essere passati al costruttore di una classe, così sia! Sia origine destinationdel StreetMapesempio sono di tipo Point. Sarebbe possibile passare le singole coordinate di ciascun punto alla mappa? Certo, ma le proprietà di un Pointappartengono insieme, inoltre puoi avere tutti i tipi di metodi che aiutano nella loro costruzione:

1 La differenza è che il builder non è solo un semplice oggetto dati, ma consente queste chiamate setter concatenate. Si Builderpreoccupa principalmente di come costruirsi.

Ora aggiungiamo un po 'di pattern di comando al mix, perché se guardi da vicino, il builder è in realtà un comando:

  1. Incapsula un'azione: chiamare un metodo di un'altra classe
  2. Contiene le informazioni necessarie per eseguire tale azione: i valori per i parametri del metodo

La parte speciale per il costruttore è che:

  1. Quel metodo da chiamare è in realtà il costruttore di una classe. Meglio chiamarlo un modello di creazione ora.
  2. Il valore per il parametro è lo stesso builder

Ciò che viene passato al Personcostruttore può costruirlo, ma non è necessario. Potrebbe essere un semplice oggetto di configurazione vecchio o un builder.


0

No, hanno completamente torto e hai assolutamente ragione. Quel modello di costruttore è semplicemente stupido. Non sono affari della Persona da dove provengono gli argomenti del costruttore. Il costruttore è solo una comodità per gli utenti e niente di più.


4
Grazie ... Sono sicuro che questa risposta raccoglierebbe più voti se si espandesse un po 'di più, sembra una risposta incompiuta =)
Mathieu Guindon

Sì, farei +1, un approccio alternativo al costruttore che fornisce le stesse protezioni e garanzie senza l'accoppiamento
matt freake

3
Per un contrappunto a questo, vedi la risposta accettata , che menziona i casi in cui PersonBuilderin realtà è una parte essenziale Persondell'API. Allo stato attuale, questa risposta non discute il suo caso.
Andres F.
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.