Perché un tipo dovrebbe essere accoppiato con il suo costruttore?

21

Recentemente ho eliminato una risposta mia su Analisi del codice , che è iniziata così:

private Person(PersonBuilder builder) {

Stop. Red flag. A PersonBuilder would build a Person; it knows about a Person. The Person class shouldn't know anything about a PersonBuilder - it's just an immutable type. You've created a circular coupling here, where A depends on B which depends on A.

The Person should just intake its parameters; a client that's willing to create a Person without building it should be able to do that.

Sono stato schiaffeggiato con un downvote, e ho detto che (citando) Bandiera rossa, perché? L'implementazione qui ha la stessa forma mostrata da Joshua Bloch nel suo libro "Effective Java" (articolo # 2).

Quindi, sembra che il modo One Right di implementare un Pattern Builder in Java sia rendi il builder un tipo annidato (non è questa la domanda però) e poi fai product (la classe dell'oggetto che viene creato) prendi una dipendenza sul builder , come questo:

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 di 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 classe Builder , e per quanto ne pensi potrebbe essere istanziata da una chiamata diretta al costruttore, una fabbrica astratta, .. .o un costruttore: per quanto ho capito, il prodotto di un modello creativo mai deve sapere qualcosa su cosa lo sta creando.

Mi è stato servito il contro-argomento (che apparentemente è esplicitamente difeso nel libro di Bloch) che un modello di builder potrebbe essere usato per rielaborare un tipo che avrebbe un generatore gonfiato con dozzine di argomenti opzionali. Quindi, invece di attenermi a ciò che pensavo , sapevo di aver fatto ricerche un po 'su questo sito e ho scoperto che, come sospettavo, questo argomento non contiene acqua .

Quindi qual è il problema? Perché proporre soluzioni troppo ingegnerizzate a un problema che non dovrebbe nemmeno esistere in primo luogo? Se prendiamo Joshua Bloch dal suo piedistallo per un minuto, possiamo trovare una sola buona, valida ragione per accoppiare due tipi concreti e definirla una best practice?

Questo mi fa impazzire programmazione settoriale cargo .

    
posta Mathieu Guindon 04.05.2016 - 18:53
fonte

5 risposte

22

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

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

È del tutto ragionevole che una classe Persona verificata dalla validità, immutabile e / o strettamente incapsulata necessariamente venga creata attraverso un Generatore che fornisce (indipendentemente dal fatto che quel costruttore sia annidato o adiacente) . In tal modo, la Persona può mantenere tutti i suoi campi privati o pacchetti-privati e finali, e anche lasciare la classe aperta per le modifiche se si tratta di un lavoro in corso. (Potrebbe finire chiuso per la modifica e aperto per l'estensione, ma al momento non è importante ed è particolarmente controverso per gli oggetti dati immutabili.)

Se una persona viene necessariamente creata attraverso un PersonalBuilder fornito come parte di un singolo progetto API a livello di pacchetto, allora un accoppiamento circolare va bene e una bandiera rossa non è garantita qui. In particolare, l'hai dichiarato come un fatto incontrovertibile e non un'opinione o una scelta di progettazione dell'API, quindi potrebbe aver contribuito a una cattiva risposta. Non ti avrei downvoted, ma non incolpare qualcun altro per averlo fatto, e lascerò il resto della discussione su "cosa garantisce un downvote" al Centro assistenza per la revisione dei codici e meta . I downvotes accadono; non è uno "schiaffo".

Naturalmente, se vuoi lasciare aperti molti modi per creare un oggetto Person, il PersonBuilder potrebbe diventare un consumatore o utilità della classe Person, su 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à) improvvisamente 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 facoltativi o un costruttore aperto alla modifica, il costruttore lungo della classe potrebbe essere un dettaglio di implementazione meglio nascosto. (Ricorda inoltre che un Builder ufficiale e necessario non ti preclude di scrivere la tua utility di costruzione, solo che la tua utility di costruzione può utilizzare Builder come API come nella mia domanda originale sopra.)

p.s .: Prendo atto che l'esempio di revisione del codice che hai elencato ha una Persona immutabile, ma il controesempio da Wikipedia elenca un'automobile mutevole con getter e setter. Potrebbe essere più facile vedere la macchina necessaria omessa dalla macchina se si mantiene la lingua e gli invarianti coerenti tra loro.

    
risposta data 04.05.2016 - 19:37
fonte
7

Capisco quanto possa essere fastidioso quando si usa "l'appello all'autorità" in un dibattito. Le argomentazioni dovrebbero stare sul loro IMO e anche se non è sbagliato indicare il consiglio di una persona così rispettata, in realtà non può essere considerata una discussione completa in sé, come sappiamo che il Sole viaggia intorno alla terra perché così disse Aristotele .

Detto questo, penso che la premessa del tuo argomento sia la stessa. Non dovremmo mai unire due tipi concreti e chiamarlo una buona pratica perché ... qualcuno ha detto così?

Affinché l'argomento che l'accoppiamento sia problematico sia valido, è necessario che ci siano problemi specifici che crea. Se questo approccio non è soggetto a questi problemi, allora non si può logicamente sostenere che questa forma di accoppiamento sia problematica. Quindi, se vuoi farti notare qui, penso che devi mostrare come questo approccio è soggetto a questi problemi e / o come il disaccoppiamento del costruttore migliorerà un progetto.

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

    
risposta data 04.05.2016 - 20:29
fonte
4

Per rispondere perché 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 numero elevato di parametri del costruttore. Questa non è l'unica ragione per usare un costruttore, ma suppongo che sia il più comune. In breve, il builder sostituisce quei parametri del costruttore e spesso il costruttore viene dichiarato nella stessa classe che sta costruendo. Quindi, la classe che racchiude ha già conoscenza del costruttore e passarla come argomento del costruttore non lo cambia. Ecco perché un tipo sarebbe accoppiato con il suo costruttore.

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

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

    
risposta data 05.05.2016 - 02:30
fonte
1

the product of a creational pattern never needs to know anything about what's creating it.

La classe Person non sa cosa lo sta creando. Ha solo un costruttore con un parametro, quindi cosa? Il nome del tipo di parametro termina con ... Builder che non impone nulla. Dopo tutto, è solo un nome.

Il builder è un oggetto di configurazione 1 glorificato E un oggetto di configurazione è in realtà solo un raggruppamento di proprietà che appartengono l'una all'altra. Se appartengono solo l'uno all'altro allo scopo di essere passati al costruttore di una classe, così sia! Sia origin che destination dell'esempio StreetMap sono di tipo Point . Sarebbe possibile passare le singole coordinate di ciascun punto alla mappa? Certo, ma le proprietà di Point appartengono insieme, in più puoi avere tutti i tipi di metodi su di loro che aiutano con la loro costruzione:

1 La differenza è che il builder non è solo un semplice oggetto di dati, ma consente queste chiamate setter concatenate. Il Builder riguarda principalmente come si costruisce da sé.

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

  1. Incapsula un'azione: chiama un metodo di un'altra classe
  2. Conserva le informazioni necessarie per eseguire quell'azione: i valori per i parametri del metodo

La parte speciale per il builder è quella:

  1. Quel metodo da chiamare è in realtà il costruttore di una classe. Meglio chiamalo modello creativo adesso.
  2. Il valore per il parametro è lo stesso builder

Ciò che viene passato al costruttore di Person può crearlo, ma non necessariamente. Potrebbe essere un semplice vecchio oggetto di configurazione o un costruttore.

    
risposta data 05.05.2016 - 03:06
fonte
0

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

    
risposta data 04.05.2016 - 20:14
fonte

Leggi altre domande sui tag