Legittimo "vero lavoro" in un costruttore?

22

Sto lavorando a un progetto, ma continuo a colpire un posto di blocco. Ho una particolare classe (ModelDef) che è essenzialmente il proprietario di un albero nodo complesso costruito analizzando uno schema XML (penso DOM). Voglio seguire i buoni principi di progettazione (SOLID) e garantire che il sistema risultante sia facilmente verificabile. Ho tutte le intenzioni di usare DI per passare le dipendenze nel costruttore di ModelDef (in modo che possano essere facilmente sostituite, se necessario, durante i test).

Ciò di cui sto combattendo, però, è la creazione dell'albero dei nodi. Questo albero sarà composto interamente da semplici oggetti "di valore" che non dovranno essere testati in modo indipendente. (Tuttavia, potrei ancora passare una Fabbrica astratta in ModelDef per aiutare nella creazione di questi oggetti.)

Ma continuo a leggere che un costruttore non dovrebbe fare alcun vero lavoro (es. Flaw: Constructor esegue Real Work ). Questo ha perfettamente senso per me se "lavoro reale" significa costruire oggetti dipendenti pesanti da pesare che uno potrebbe in seguito voler escludere per i test. (Questi dovrebbero essere passati tramite DI.)

Ma per quanto riguarda gli oggetti di valore leggero come questo albero dei nodi? L'albero deve essere creato da qualche parte, giusto? Perché non tramite il costruttore di ModelDef (usando, ad esempio, un metodo buildNodeTree ())?

Non voglio veramente creare l'albero dei nodi al di fuori di ModelDef e poi passarlo (tramite il DI costruttore del costruttore), perché la creazione dell'albero dei nodi analizzando lo schema richiede una quantità significativa di codice complesso - codice che deve essere accuratamente testato Non voglio relegarlo in codice "incollato" (che dovrebbe essere relativamente banale e probabilmente non verrà testato direttamente).

Ho pensato di inserire il codice per creare l'albero dei nodi in un oggetto separato "builder", ma esitare a chiamarlo un "builder", perché in realtà non corrisponde al Pattern Builder (che sembra essere più interessato con l'eliminazione dei costruttori telescopici). Ma anche se l'ho chiamato qualcosa di diverso (ad esempio NodeTreeConstructor), sembra ancora un po 'un trucco per evitare che il costruttore ModelDef costruisca l'albero dei nodi. Deve essere costruito da qualche parte; perché non nell'oggetto che lo possiederà?

    
posta Gurtz 20.12.2015 - 02:11
fonte

4 risposte

26

E, oltre a quello che ha suggerito Ross Patterson, considera questa posizione esattamente l'opposto:

  1. Prendi le massime come "Non fare alcun vero lavoro nei tuoi costruttori" con un pizzico di sale.

  2. Un costruttore non è, in realtà, nient'altro che un metodo statico. Quindi, strutturalmente, non c'è davvero molta differenza tra:

    a) un semplice costruttore e un mucchio di complessi metodi di factory statici e

    b) un semplice costruttore e un mucchio di costruttori più complessi.

Una parte considerevole del sentimento negativo nei confronti di un vero lavoro nei costruttori deriva da un certo periodo della storia del C ++ in cui si discute su quale stato l'oggetto verrà lasciato se viene lanciata un'eccezione all'interno del costruttore e se il distruttore dovrebbe essere invocato in tale evento. Quella parte della storia di C ++ è finita, e il problema è stato risolto, mentre in linguaggi come Java non c'è mai stato alcun problema di questo tipo.

La mia opinione è che se si evita semplicemente di usare new nel costruttore, (come indica l'intenzione di utilizzare Iniezione di dipendenza), si dovrebbe andare bene. Rido delle affermazioni come "la logica condizionale o ciclica in un costruttore è un segnale di avvertimento di un difetto".

Oltre a tutto ciò, personalmente, prenderei la logica di analisi XML fuori dal costruttore, non perché è male avere una logica complessa in un costruttore, ma perché è bene seguire il principio della "separazione delle preoccupazioni". Quindi, sposterei la logica di analisi XML in una classe separata del tutto, non in alcuni metodi statici che appartengono alla tua classe ModelDef .

Modifica

Suppongo che se hai un metodo al di fuori di ModelDef che crea un ModelDef da XML, dovrai istanziare una struttura di dati albero dinamica temporanea, popolarla analizzando il tuo XML e poi creare la tua nuova ModelDef che passa quella struttura come parametro costruttore. Quindi, questo potrebbe forse essere pensato come un'applicazione del modello "Builder". C'è un'analogia molto stretta tra ciò che si vuole fare e il String & StringBuilder pair. Tuttavia, ho trovato questo Q & A che sembra non essere d'accordo, per ragioni che non mi sono chiare: Stackoverflow - StringBuilder e Builder Pattern . Quindi, per evitare un lungo dibattito qui su se il StringBuilder implementa o meno il modello del "costruttore", direi sentitevi liberi di essere ispirato da come StrungBuilder funziona nella creazione di una soluzione che si adatta alle vostre ha bisogno e posticipa chiamandolo un'applicazione del modello "Builder" fino a quando non saranno stati risolti quei piccoli dettagli.

Vedi questa nuova domanda: Programmatori SE: "StringBuilder" è un'applicazione del modello di costruzione del costruttore?

    
risposta data 20.12.2015 - 02:39
fonte
9

Hai già le migliori ragioni per non farlo funzionare nel costruttore ModelDef :

  1. Non c'è nulla di "leggero" nell'analisi di un documento XML in un albero dei nodi.
  2. Non c'è nulla di ovvio in un ModelDef che dice che può essere creato solo da un documento XML.

Sembra che la tua classe debba avere una varietà di metodi statici come ModelDef.FromXmlString(string xmlDocument) , ModelDef.FromXmlDoc(XmlDoc parsedNodeTree) , etc.

    
risposta data 20.12.2015 - 02:20
fonte
5

Ho sentito che "regola" prima. Nella mia esperienza è sia vero che falso.

In un orientamento all'oggetto più "classico" parliamo di oggetti che incapsulano stato e comportamento. Quindi un costruttore di oggetti dovrebbe assicurarsi che l'oggetto sia inizializzato su uno stato valido (e segnalare un errore se gli argomenti forniti non rendono l'oggetto valido). Garantire che un oggetto sia inizializzato su uno stato valido, suona sicuramente come un vero lavoro per me. E questa idea ha dei meriti, se hai un oggetto che consente solo l'inizializzazione ad uno stato valido tramite il costruttore e l'oggetto lo incapsula correttamente così che ogni metodo che cambia lo stato controlla anche che non lo faccia t cambia lo stato in qualcosa di male ... allora quell'oggetto in sostanza garantisce che è "sempre valido". Questa è davvero una bella proprietà!

Quindi il problema generalmente arriva quando proviamo a dividere tutto in piccoli pezzi per testare e deridere cose. Perché se un oggetto è veramente incapsulato correttamente, non puoi davvero entrare lì e sostituire FooBarService con il tuo FooBarService deriso e tu (probabilmente) non puoi semplicemente cambiare i valori willy-nilly per adattarli ai tuoi test (o, potrebbe richiedere un molto più codice di un semplice compito).

Così otteniamo l'altra "scuola di pensiero", che è SOLIDA. E in questa scuola di pensiero è molto più probabile che non dovremmo fare un vero lavoro nel costruttore. Il codice SOLID è spesso (ma non sempre) più facile da testare. Ma può anche essere più difficile ragionare. Spezziamo il nostro codice in piccoli oggetti con una sola responsabilità, e quindi la maggior parte dei nostri oggetti non incapsula più il loro stato (e generalmente contiene uno stato o un comportamento). Il codice di convalida è generalmente estratto in una classe di validatori e tenuto separato dallo stato. Ma ora abbiamo perso coesione, non possiamo più essere sicuri che i nostri oggetti siano validi quando li otteniamo ed essere completamente sicuri di dover sempre convalidare che le precondizioni che pensiamo di avere riguardo l'oggetto è vero prima di tentare di fare qualcosa con l'oggetto. (Ovviamente, in generale si esegue la convalida in un livello e si assume che l'oggetto sia valido ai livelli inferiori.) Ma è più facile da testare!

Quindi chi ha ragione?

Nessuno davvero. Entrambe le scuole di pensiero hanno i loro meriti. Attualmente SOLID è di gran moda e tutti parlano di SRP e Open / Closed e di tutto quel jazz. Ma solo perché qualcosa è popolare non significa che sia la scelta di design corretta per ogni singola applicazione. Quindi dipende. Se stai lavorando in una base di codice che segue in modo pesante i principi SOLID, allora sì, il vero lavoro nel costruttore è probabilmente una cattiva idea. Altrimenti, guarda la situazione e cerca di usare il tuo giudizio. Quali proprietà il tuo oggetto guadagna dal lavoro svolto nel costruttore, quali proprietà perde ? Quanto si adatta all'architettura generale della tua applicazione?

Il vero lavoro nel costruttore non è un antipattern, può essere piuttosto il contrario quando viene usato nei luoghi corretti. Ma dovrebbe essere chiaramente documentato (insieme a quali eccezioni possono essere lanciate, se ce ne sono) e come con qualsiasi decisione progettuale - dovrebbe adattarsi allo stile generale usato nella base di codice corrente.

    
risposta data 21.12.2015 - 15:08
fonte
0

C'è un problema fondamentale con questa regola ed è questa, cosa costituisce "lavoro reale"?

Puoi vedere dal articolo originale pubblicato nel domanda che l'autore tenta di definire quale "vero lavoro" è, ma è gravemente imperfetto. Perché una pratica sia buona deve essere un principio ben definito. Con ciò intendo che per quanto riguarda l'ingegneria del software l'idea dovrebbe essere portatile (agnostica a qualsiasi lingua), testata e provata. La maggior parte di ciò che viene discusso in quell'articolo non si adatta a quel primo criterio. Ecco alcuni indicatori che l'autore menziona in quell'articolo di ciò che costituisce "lavoro reale" e perché non sono cattive definizioni.

Uso della parola chiave new . Quella definizione è fondamentalmente errata perché è specifica per un dominio. Alcune lingue non utilizzano la parola chiave new . In definitiva ciò a cui sta suggerendo è che non dovrebbe essere la costruzione di altri oggetti. Tuttavia in molte lingue anche i valori più basilari sono essi stessi oggetti. Quindi qualsiasi valore assegnato nel costruttore sta anche costruendo un nuovo oggetto. Ciò rende quest'idea limitata a certe lingue e un cattivo indicatore di ciò che costituisce "un vero lavoro".

Oggetto non completamente inizializzato dopo il completamento del costruttore . Questa è una buona regola, ma contraddice anche molte delle altre regole menzionate in quell'articolo. Un buon esempio di come ciò potrebbe contraddire gli altri è citato il question che mi ha portato qui. In questa domanda qualcuno si preoccupa di usare il metodo sort in un costruttore in quello che sembra essere JavaScript a causa di questo principio. In questo esempio la persona stava creando un oggetto che rappresentava una lista ordinata di altri oggetti. A scopo di discussione immaginiamo di avere una lista di oggetti non ordinata e abbiamo bisogno di un nuovo oggetto per rappresentare una lista ordinata. Abbiamo bisogno di questo nuovo oggetto perché parte del nostro software si aspetta un elenco ordinato, e consente di chiamare questo oggetto SortedList . Questo nuovo oggetto accetta un elenco non ordinato e l'oggetto risultante dovrebbe rappresentare una lista ordinata di oggetti. Se dovessimo seguire le altre regole menzionate in quel documento, vale a dire nessuna chiamata al metodo statico, nessuna struttura di flusso di controllo, nient'altro che assegnazione, quindi l'oggetto risultante non sarebbe costruito in uno stato valido infrangendo l'altra regola di essere completamente inizializzata dopo che il costruttore ha finito. Per risolvere questo problema, dovremmo fare un lavoro di base per rendere ordinata la lista non ordinata nel costruttore. In questo modo si rompono le altre 3 regole, rendendo irrilevanti le altre regole.

In definitiva questa regola di non fare "lavoro reale" in un costruttore è mal definita e imperfetta. Cercare di definire quale "vero lavoro" è un esercizio di futilità. La regola migliore in questo articolo è che quando un costruttore termina dovrebbe essere completamente inizializzato. Ci sono una miriade di altre buone pratiche che limiterebbero quanto lavoro svolto in un costruttore. La maggior parte di questi può essere riassunta nei principi SOLID, e quegli stessi principi non ti impedirebbero di lavorare nel costruttore.

PS. Mi sento obbligato a dire che mentre asserisco che non c'è nulla di sbagliato nel fare un po 'di lavoro nel costruttore, non è nemmeno il caso di fare un sacco di lavoro. SRP suggerirebbe che un costruttore dovrebbe fare abbastanza lavoro per renderlo valido. Se il tuo costruttore ha troppe righe di codice (molto soggettivo, lo so), probabilmente sta violando questo principio e probabilmente potrebbe essere suddiviso in metodi e oggetti più piccoli definiti meglio.

    
risposta data 28.08.2018 - 19:41
fonte