Legittimo "vero lavoro" in un costruttore?


23

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

Ciò con cui sto lottando, tuttavia, è 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 con la creazione di questi oggetti.)

Ma continuo a leggere che un costruttore non dovrebbe fare alcun lavoro reale (es. Flaw: Costruttore fa il vero lavoro ). Questo ha perfettamente senso per me se "lavoro reale" significa costruire oggetti dipendenti di peso elevato che uno potrebbe voler stub per testare. (Questi dovrebbero essere passati tramite DI.)

Ma che dire degli 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, per esempio, un metodo buildNodeTree ())?

Non voglio davvero creare l'albero dei nodi al di fuori di ModelDef e poi passarlo (tramite il costruttore DI), perché la creazione dell'albero dei nodi analizzando lo schema richiede una quantità significativa di codice complesso - codice che deve essere testato a fondo . Non voglio relegarlo al codice "colla" (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 "builder" separato, ma esito a chiamarlo "builder", perché non corrisponde realmente al Builder Pattern (che sembra essere più interessato all'eliminazione del telescoping costruttori). Ma anche se l'ho chiamato in modo diverso (ad es. NodeTreeConstructor), sembra comunque un po 'un trucco solo per evitare che il costruttore ModelDef costruisca l'albero dei nodi. Deve essere costruito da qualche parte; perché non nell'oggetto che lo possiede?


7
Tuttavia, dovresti sempre diffidare di affermazioni generali come quelle. La regola generale è il codice in modo chiaro, funzionale, facile da testare, riutilizzare e mantenere, qualunque sia il modo, che varia a seconda della situazione. Se non ottieni altro che complessità del codice e confusione nel tentativo di seguire una "regola" del genere, non è una regola adatta alla tua situazione. Tutti questi "modelli" e caratteristiche del linguaggio sono strumenti; usa il migliore per il tuo lavoro specifico.
Jason C,

Risposte:


26

E, oltre a quanto suggerito da Ross Patterson, considera questa posizione che è esattamente l'opposto:

  1. Prendi massime come "Non farai alcun vero lavoro nei tuoi costruttori" con un granello di sale.

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

    a) un semplice costruttore e un mucchio di complessi metodi statici di fabbrica, e

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

Una parte considerevole del sentimento negativo verso l'esecuzione di qualsiasi lavoro reale nei costruttori deriva da un certo periodo della storia del C ++ in cui si è discusso su quale stato rimarrà l'oggetto se viene generata un'eccezione all'interno del costruttore e se il distruttore dovrebbe essere invocato in un tale evento. Quella parte della storia del C ++ è finita e il problema è stato risolto, mentre in linguaggi come Java non c'è mai stato nessun problema di questo tipo per cominciare.

La mia opinione è che se semplicemente eviti di usare newnel costruttore (come indica la tua intenzione di utilizzare l'iniezione di dipendenza), dovresti andare bene. Rido di affermazioni come "la logica condizionale o ciclica in un costruttore è un segnale di avvertimento di un difetto".

Oltre a tutto ciò, personalmente, toglierei la logica di analisi XML 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 ModelDefclasse.

Emendamento

Suppongo che se si dispone di un metodo al di fuori del ModelDefquale crea un ModelDefda XML, sarà necessario creare un'istanza di una struttura dinamica di dati ad albero temporaneo, popolarla analizzando l'XML e quindi creare il nuovo ModelDefpassaggio di tale struttura come parametro del costruttore. Quindi, ciò potrebbe forse essere considerato come un'applicazione del modello "Builder". C'è un'analogia molto stretta tra ciò che vuoi fare e la coppia String& StringBuilder. Tuttavia, ho trovato queste domande e risposte che sembrano in disaccordo, per motivi che non mi sono chiari: Stackoverflow - StringBuilder e Builder Pattern . Quindi, per evitare un lungo dibattito qui sul fatto che il StringBuildermodello "builder" sia o meno implementato, direi di essere libero di essere ispirato da comeStrungBuilder lavora per trovare una soluzione che soddisfi le tue esigenze e rimandare chiamandola un'applicazione del modello "Builder" fino a quando quel piccolo dettaglio è stato risolto.

Vedi questa nuova domanda: Programmatori SE: "StringBuilder" è un'applicazione del Builder Design Pattern?


3
@RichardLevasseur Lo ricordo solo come un argomento di preoccupazione e dibattito tra i programmatori C ++ nei primi anni a metà degli anni novanta. Se guardi questo post: gotw.ca/gotw/066.htm vedrai che è abbastanza complicato e le cose abbastanza complicate tendono ad essere controverse. Non lo so per certo, ma penso che all'inizio degli anni novanta una parte di quella roba non fosse nemmeno stata ancora standardizzata. Mi dispiace, non posso fornire un buon riferimento.
Mike Nakis,

1
@Gurtz Vorrei pensare a una classe come "utility xml" specifiche dell'applicazione, poiché il formato del file xml (o la struttura del documento) è probabilmente legato alla particolare applicazione che stai sviluppando, indipendentemente dalle possibilità di riutilizzare il "ModelDef".
Mike Nakis,

1
@Gurtz così, probabilmente li renderei metodi di istanza della classe principale "Applicazione", o se questo è troppo fastidioso, quindi metodi statici di qualche classe di supporto, in un modo molto simile a quello suggerito da Ross Patterson.
Mike Nakis,

1
@Gurtz si scusa per non aver affrontato specificamente l'approccio "builder" in precedenza. Ho modificato la mia risposta.
Mike Nakis,

3
@Gurtz È possibile ma al di fuori della curiosità accademica, non importa. Non farti risucchiare dal "pattern anti-pattern". I modelli sono in realtà solo nomi per descrivere le tecniche di codifica comuni / utili ad altri comodamente. Fai quello che devi fare, schiaffeggia un'etichetta su di essa in un secondo momento se devi descriverla. È perfettamente OK implementare qualcosa che è "un po 'come il modello del costruttore, in un certo senso, forse", purché il tuo codice abbia un senso. È ragionevole concentrarsi sui modelli quando si apprendono nuove tecniche, ma non cadere in una trappola di pensare che tutto ciò che fai deve essere un modello chiamato.
Jason C il

9

Fornisci già i motivi migliori per non eseguire questo lavoro nel ModelDefcostruttore:

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

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


Grazie per la risposta! Per quanto riguarda la proposta di metodi statici. Si tratterebbe di fabbriche statiche che creano un'istanza di ModelDef (dalle varie fonti XML)? O sarebbero responsabili del caricamento di un oggetto ModelDef già creato? In quest'ultimo caso, sarei preoccupato di avere l'oggetto inizializzato solo parzialmente (poiché un ModelDef necessita di un albero dei nodi per essere completamente inizializzato). Pensieri?
Gurtz,

3
Mi scusi per il collegamento, ma sì, ciò che Ross intende è metodi di fabbrica statici che restituiscono istanze completamente costruite. Il prototipo completo sarebbe qualcosa di simile public static ModelDef createFromXmlString( string xmlDocument ). Questa è una pratica abbastanza comune. A volte lo faccio anche io. Il mio suggerimento che puoi anche fare solo i costruttori è un mio tipo di risposta standard in situazioni in cui sospetto che approcci alternativi vengano respinti come "non kosher" senza una buona ragione.
Mike Nakis,

1
@ Mike-Nakis, grazie per il chiarimento. Quindi, in questo caso, il metodo factory statico comporterebbe l'albero dei nodi e quindi lo passerebbe al costruttore (possibilmente privato) di ModelDef. Ha senso. Grazie.
Gurtz,

@Gurtz Esatto.
Ross Patterson,

5

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

Nell'orientamento più "classico" degli oggetti parliamo di oggetti che incapsulano stato e comportamento. Pertanto 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 valido l'oggetto). Garantire che un oggetto sia inizializzato a uno stato valido sicuramente mi sembra un vero lavoro. E questa idea ha dei meriti, se si dispone di un oggetto che consente l'inizializzazione solo a uno stato valido tramite il costruttore e l'oggetto lo incapsula correttamente in modo che ogni metodo che modifica lo stato controlla anche che non cambi lo stato in qualcosa di brutto ... quindi quell'oggetto in sostanza garantisce che è "sempre valido". Questa è davvero una bella proprietà!

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

Quindi otteniamo l'altra "scuola di pensiero", che è SOLIDO. E in questa scuola di pensiero è molto più vero 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 su. Suddividiamo 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 stato o comportamento). Il codice di convalida viene generalmente estratto in una classe di validazione e tenuto separato dallo stato. Ma ora abbiamo perso la coesione, non possiamo più essere sicuri che i nostri oggetti siano validi quando li otteniamo ed essere completamentecerto che dobbiamo sempre convalidare che i presupposti che pensiamo di avere sull'oggetto siano veri prima di tentare di fare qualcosa con l'oggetto. (Naturalmente, in generale si esegue la convalida in un livello e quindi si assume che l'oggetto sia valido ai livelli inferiori.) Ma è più facile testarlo!

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 pesantemente i principi SOLIDI, sì, il vero lavoro nel costruttore è probabilmente una cattiva idea. Ma altrimenti, guarda la situazione e prova a usare il tuo giudizio. Quali proprietà ottiene il tuo oggetto facendo un lavoro nel costruttore, quali proprietà perde ? In che misura si adatta all'architettura generale dell'applicazione?

Il vero lavoro nel costruttore non è un modello, può essere esattamente l'opposto se usato nei posti corretti. Ma dovrebbe essere documentato chiaramente (insieme a quali eccezioni possono essere generate, se presenti) e come con qualsiasi decisione progettuale - dovrebbe adattarsi allo stile generale utilizzato nella base di codice corrente.


Questa è una risposta fantastica
jrahhali,

0

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

Dall'articolo originale pubblicato nella domanda puoi vedere che l'autore tenta di definire cosa sia il "vero lavoro", ma è gravemente difettoso. 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 in qualsiasi lingua), testata e dimostrata. Gran parte di ciò che viene sostenuto in quell'articolo non rientra in quel primo criterio. Ecco alcuni indicatori che l'autore menziona in quell'articolo di ciò che costituisce "lavoro reale" e perché non sono definizioni sbagliate.

Uso della newparola chiave . Tale definizione è fondamentalmente errata perché è specifica del dominio. Alcune lingue non usano la newparola chiave. Alla fine, ciò a cui accenna è che non dovrebbe costruire altri oggetti. Tuttavia in molte lingue anche i valori più elementari sono essi stessi oggetti. Quindi qualsiasi valore assegnato nel costruttore sta anche costruendo un nuovo oggetto. Ciò rende questa idea limitata a determinate lingue e un cattivo indicatore di ciò che costituisce "un vero lavoro".

Oggetto non completamente inizializzato al termine del costruttore . Questa è una buona regola, ma contraddice anche molte delle altre regole menzionate in quell'articolo. Un buon esempio di come ciò possa contraddire gli altri è la domanda che mi ha portato qui. In quella domanda qualcuno si preoccupa dell'uso del sortmetodo 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 un elenco ordinato di altri oggetti. Ai fini della discussione, immagina di avere un elenco di oggetti non ordinato e di avere bisogno di un nuovo oggetto per rappresentare un elenco ordinato. Abbiamo bisogno di questo nuovo oggetto perché una parte del nostro software prevede un elenco ordinato e consente di chiamare questo oggettoSortedList. Questo nuovo oggetto accetta un elenco non ordinato e l'oggetto risultante dovrebbe rappresentare un elenco di oggetti ora ordinato. Se dovessimo seguire le altre regole menzionate in quel documento, vale a dire nessuna chiamata a metodo statico, nessuna struttura di flusso di controllo, niente più che assegnazione, allora l'oggetto risultante non verrebbe costruito in uno stato valido infrangendo l'altra regola di esso completamente inizializzata al termine del costruttore. Per risolvere questo problema, dovremmo eseguire alcune operazioni di base per rendere l'elenco non ordinato ordinato nel costruttore. In questo modo si infrangerebbero le altre 3 regole, rendendo le altre regole irrilevanti.

In definitiva questa regola di non fare "lavoro reale" in un costruttore è mal definita e imperfetta. Cercare di definire quale "lavoro reale" è un esercizio di futilità. La regola migliore in quell'articolo è che quando un costruttore finisce dovrebbe essere completamente inizializzato. Esistono molte altre buone pratiche che limiterebbero la quantità di lavoro svolto in un costruttore. Molti di questi possono essere riassunti in principi SOLIDI, e quegli stessi principi non ti impedirebbero di fare lavori nel costruttore.

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

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.