Separazione di costruzione e inizializzazione

5

Sono confuso da questo post di Mark Seeman.

E il suo commento su IInitializable di seguito:

The problem with an Initialize method is the same as with Property Injection (A.K.A. Setter Injection): it creates a temporal coupling between the Initialize method and all other members of the class. Unless you truly can invoke any other member of the class without first invoking the Initialize method, such API design is deceitful and will lead to run-time exceptions. It also becomes much harder to ensure that the object is always in a consistent state.

Nello stesso tempo scrive:

This issue is similar to the issue of invoking virtual members from the constructor. Conceptually, an injected dependency is equivalent to a virtual member.

Penso che questa affermazione sia vera solo se ammetti che costruita! = inizializzata.

Cosa otteniamo ora:
Le dipendenze sono iniettate nel costruttore ma non è consigliabile utilizzarle.
La fase di inizializzazione porta alla complessità e dovrebbe essere evitata.

Non è contraddittorio?

Immagina che la classe abbia bisogno di impostare il suo stato usando le dipendenze fornite. Caricamento delle impostazioni salvate, ad esempio.
Init è male, il costruttore è cattivo, quindi dove eseguire questa operazione?

E un altro punto:
Non sono metodi come Connection.Open () solo un altro nome per Initialize?

Domanda:
Quindi qualcuno può descrivere un buon schema di inizializzazione nel contesto di Dipendenza Iniezione che risolve le preoccupazioni che Mark Seeman porta in primo piano?

    
posta Pavel Voronin 25.07.2013 - 23:49
fonte

4 risposte

4

Il metodo Initialise dedicato è sbagliato - se lo usi devi costruire un oggetto e quindi non usarlo affatto finché non hai chiamato con successo Init e always destroy se la chiamata Init fallisce. È un pasticcio di inizializzazione che è gestito molto meglio dal costruttore.

Se restituisci un oggetto costruito con successo che contiene tutto ciò di cui ha bisogno per iniziare a lavorare, allora hai un tempo molto più semplice come programmatore che usa quella classe.

Allo stesso modo, non dovrebbe esserci un problema se la dipendenza iniettata viene risolta durante la costruzione.

Tuttavia ciò significa impostare l'oggetto DI durante la costruzione - "Iniezione costruttore", e presumo che stia parlando di 'injection proprietà' dove la configurazione viene passata come un insieme di chiamate di proprietà dopo che l'oggetto è stato costruito. Questo è lo stesso problema in cui hai avuto un metodo Init, ma ora hai un metodo SetConfigX. I nomi sono diversi, ovviamente, ma il principio è lo stesso: si finisce con un oggetto semi-costruito che allora si riempie con il resto del suo stato prima che possa essere usato.

    
risposta data 26.07.2013 - 14:20
fonte
3

I thinks this statement is true only if admit that constructed != initialized.

Ti manca il punto che sembra. Chiamare metodi virtuali dal costruttore implicitamente significa che l'oggetto non è ancora completamente costruito (e inizializzato). I concetti sono gli stessi. Non è possibile chiamare metodi virtuali finché tutti i costruttori non sono stati eseguiti (l'oggetto è completamente inizializzato). Allo stesso modo, non è possibile chiamare metodi che utilizzano dipendenze iniettate fino a quando non vengono popolati (l'oggetto è completamente inizializzato).

Dependecnies are injected in constructor but it is not recommended to use them.

Questo non è affatto ciò che l'articolo dice. Sta dicendo che il costruttore dovrebbe essere limitato ad accettare dipendenze, non cercandole o configurandole, o qualsiasi altra cosa. Dice anche che l'iniezione del costruttore è insufficiente per alcune esigenze (dipendenze circolari) e scomoda in altre (dove la definizione del costruttore impone grandi gerarchie di ereditarietà).

Non voglio mettere le parole nella bocca dello scrittore dell'articolo, ma raccomanderei l'uso dell'iniezione del costruttore finché non trovi una buona ragione per non . È il più semplice da implementare. È il più facile da eseguire il debug. È il più facile da leggere.

So can anyone describe a good initialization pattern in the context of Dependency Injection that addresses the concerns Mark Seeman brings up?

Personalmente, mi piacciono le fabbriche che fanno girare un oggetto (o una serie di oggetti) con tutte le loro dipendenze popolate. Questo limita i luoghi in cui occorre prestare attenzione ai costruttori dei componenti e alle fabbriche stesse. Se le dipendenze non vengono trovate, si ottiene immediatamente un errore. Se ottieni indietro gli oggetti, sai che sono in buono stato.

Non è qualcosa che può essere applicato (bene) a tutti i problemi, ma nella mia esperienza, può essere applicato bene alla maggior parte dei problemi e isola un po 'di complessità.

    
risposta data 26.07.2013 - 15:02
fonte
0

Dependencies are injected in constructor but it is not recommended to use them.

Penso che l'autore stia dicendo il contrario di ciò che si concentra sull'accoppiamento lento. Ha detto:

"The Constructor Injection design pattern is a extremely useful way to implement loose coupling"

L'accoppiamento lento è il fulcro del buon design del software. Rende le classi riutilizzabili, estendibili e verificabili.

Initialize phase brings complexity and should be avoided.

Sì, ma ciò non significa che possa sempre essere evitato. I costruttori non hanno un valore di ritorno e non tutte le lingue supportano il lancio di eccezioni. Alcuni libri sostengono che un costruttore dovrebbe sempre avere successo e sono d'accordo con questa regola.

Ecco alcuni esempi di codice sorgente di entrambi gli stili. Entrambi gli approcci funzionano, ma quale è meglio?

try
{
    FileReader f = new FileReader("something.txt");
    String str = f.read();
}
catch(FileNotFound e) {...}

o

FileReader f = new FileReader("something.txt");
if(f.exists())
{
    String str = f.read();
}

Il primo esempio ha l'oggetto FileReader dipendente da una risorsa esterna. Se non può accedervi, fallisce durante il costruttore. Ciò crea dipendenza oltre al controller del programmatore e anche test delle unità.

Nel secondo esempio non c'è dipendenza. L'oggetto può essere creato anche se la risorsa non esiste. La dipendenza ora dipende dal programmatore da applicare.

Quindi, in che modo questo si riferisce a initializing di un oggetto. È molto semplice. Chi dovrebbe essere responsabile? L'oggetto o il programmatore. Questo dipende dall'autore dell'oggetto. Lui / lei potrebbe avere delle buone ragioni per rinunciare al controllo della costruzione dell'oggetto per il programmatore che lo usa.

Se initializing un oggetto è un'attività complessa, quindi localizzi quel codice in una classe factory in modo da avere un posto dove andare per apportare modifiche.

Are not methods like Connection.Open() just another name for Initialize?

Il metodo Connection.Open() è un inizializzatore solo se Connection.Read() fallisce se Open() non è stato chiamato.

Ecco il problema di cui parla l'autore.

Connection con = new Connection();
con->Read(); // this will fail, Open() was not called

Per correggere il codice precedente. Devi scrivere questo, e questo è un cattivo progetto.

Connection con = new Connection();
con->Open("192.168.1.1");  // bug fix, forgot to call Open()
con->Read();

Ho letto molti commenti nel codice sorgente da programmatori che scrivono "bug fix, ho dimenticato di chiamare X (...)". L'argomento è che il bug era evitabile in primo luogo. Se l'autore della classe Connection non ha utilizzato un inizializzatore.

Ecco la soluzione al problema.

Connection con = new Connection("192.168.1.1");
con->Read();

Ora, come si gestisce una connessione fallita viene risposto più in alto nella mia risposta. O il costruttore lancia un'eccezione o il programmatore deve chiamare isOpen() prima di read() .

So can anyone describe a good initialization pattern in the context of Dependency Injection that addresses the concerns Mark Seeman brings up?

Potrebbe essere difficile da capire, ma la risposta è in Single Responsibility Principle .

Per il mio esempio con l'oggetto Connection . Ha infranto la regola SRP. L'oggetto Connection si apre e legge dalla risorsa. Sono due diverse responsabilità. Possiamo risolvere questo problema fratturando l'oggetto in più pezzi ciascuno con le proprie responsabilità.

Ecco un esempio;

try
{
    SocketAddress addr = new SocketAddress("192.168.1.1");
    try
    {
        Socket s = new Socket(addr);
        try
        {
            SocketReader r = new SocketReader(s);
            if(r != null)
            {
                String str = r->Read();
            }
        } catch(ReadFailure e) {..}
    } catch(ConnectionFailure e) {..}
} catch(BadAddress e) {..}

Ogni oggetto è responsabile solo di una cosa.

  • SocketAddress verrà costruito correttamente solo se l'indirizzo è valido.
  • Socket verrà costruito solo se può effettuare una connessione all'indirizzo
  • SocketReader funziona solo se può leggere.

Come puoi vedere. Devi scrivere molto più codice sorgente, e questo è il motivo per cui spesso vediamo l'iniezione della dipendenza evitata. È un lavoro extra da parte del programmatore.

    
risposta data 26.07.2013 - 16:39
fonte
0

Per flessibilità, NON fare l'inizializzazione nei costruttori.

Per semplicità, se puoi permettertelo e sei sicuro non ti causerà mal di testa in seguito, esegui l'inizializzazione nei costruttori (RAII). Quindi puoi fare le solite supposizioni RAII sicure riguardo alle deallocazioni (monolitiche?)

Tutta la programmazione si basa sull'ordinamento corretto degli eventi. Non c'è nulla di " ingannevole " sull'attribuzione alle aspettative del programmatore di eseguire certe operazioni prima di certe altre operazioni (in questo caso , costruendo - > inizializzando tutti i membri - > usando l'oggetto). L'idea di Seeman di "inganno" è chiaramente assurda.

Di solito tendo a favorire la flessibilità (la prima) in questi giorni perché sono stato bruciato troppe volte liquidando la flessibilità necessaria all'iniezione / inizializzazione individuale dei membri. (a favore, preferisco C a C ++ per lo stesso motivo: flessibilità.). L'iniezione del costruttore è RAII. RAII ammette (nel suo stesso nome) di essere una fusione di preoccupazioni. Di conseguenza, gli oggetti non possono entrare in un pool per essere riconfigurati / riutilizzati, ma devono essere ricostruiti, il che significa allocazioni extra di runtime. Manca un reale controllo sull'ordinamento delle istruzioni (preoccupazione dinamica DI). E naturalmente ci sono gli elenchi dei parametri lunghi e brutti.

    
risposta data 13.12.2014 - 17:04
fonte