Perché dovrei usare una classe factory invece della costruzione diretta degli oggetti?

142

Ho visto la cronologia di diversi progetti di libreria di classi С # e Java su GitHub e CodePlex e vedo una tendenza al passaggio a classi di fabbrica anziché all'istanza di oggetti diretta.

Perché dovrei usare estesamente le classi di fabbrica? Ho una buona libreria, in cui gli oggetti vengono creati alla vecchia maniera - invocando costruttori pubblici di classi. Negli ultimi commit gli autori hanno rapidamente modificato tutti i costruttori pubblici di migliaia di classi all'interno e creato anche una grande classe factory con migliaia di% di metodi staticiCreateXXX che restituiscono solo nuovi oggetti chiamando i costruttori interni delle classi. L'API del progetto esterno è rotta, ben eseguita.

Perché un tale cambiamento sarebbe utile? Qual è il punto di refactoring in questo modo? Quali sono i vantaggi della sostituzione di chiamate a costruttori di classi pubbliche con chiamate di metodo factory statiche?

Quando dovrei usare i costruttori pubblici e quando dovrei usare le fabbriche?

    
posta rufanov 14.08.2014 - 05:27
fonte

9 risposte

52

Le classi di fabbrica sono spesso implementate perché consentono al progetto di seguire più da vicino i principi SOLID . In particolare, i principi di inversione di separazione e dipendenza dell'interfaccia.

Fabbriche e interfacce consentono una flessibilità molto più a lungo termine. Permette un design più disaccoppiato e quindi più testabile. Ecco un elenco non esaustivo dei motivi per cui potresti seguire questo percorso:

  • Ti consente di introdurre facilmente un contenitore Inversion of Control (IoC)
  • Rende il tuo codice più testabile in quanto puoi prendere in giro le interfacce
  • Ti dà molta più flessibilità quando arriva il momento di cambiare l'applicazione (cioè puoi creare nuove implementazioni senza cambiare il codice dipendente)

Considera questa situazione.

L'assembly A (- > significa dipende da):

Class A -> Class B
Class A -> Class C
Class B -> Class D

Voglio spostare la Classe B all'Assemblea B, che dipende dall'assemblaggio A. Con queste concrete dipendenze devo spostare la maggior parte della mia intera gerarchia di classi. Se utilizzo le interfacce, posso evitare gran parte del dolore.

Assembly A:

Class A -> Interface IB
Class A -> Interface IC
Class B -> Interface IB
Class C -> Interface IC
Class B -> Interface ID
Class D -> Interface ID

Ora posso spostare la classe B dall'assemblea B senza alcun tipo di sofferenza. Dipende comunque dalle interfacce nell'assemblaggio A.

L'uso di un contenitore IoC per risolvere le tue dipendenze ti offre ancora più flessibilità. Non è necessario aggiornare ogni chiamata al costruttore ogni volta che si modificano le dipendenze della classe.

Seguendo il principio di separazione delle interfacce e il principio di inversione delle dipendenze ci consente di creare applicazioni altamente flessibili e disaccoppiate. Una volta che hai lavorato su uno di questi tipi di applicazioni, non vorrai più tornare a utilizzare la parola chiave new .

    
risposta data 14.08.2014 - 05:51
fonte
111

Come whatsisname detto, credo che questo sia il caso del culto del design del software. Le fabbriche, in particolare il tipo astratto, sono utilizzabili solo quando il modulo crea più istanze di una classe e si desidera fornire all'utente la capacità di questo modulo di specificare il tipo da creare. Questo requisito è in realtà abbastanza raro, perché la maggior parte delle volte hai solo bisogno di un'istanza e puoi semplicemente passare quell'istanza direttamente invece di creare un factory esplicito.

Il fatto è che le fabbriche (e i singleton) sono estremamente facili da implementare e quindi le persone li usano molto, anche in luoghi dove non sono necessari. Quindi, quando il programmatore pensa "Quali schemi di progettazione dovrei usare in questo codice?" la fabbrica è la prima cosa che gli viene in mente.

Molte fabbriche sono create perché "Forse, un giorno, avrò bisogno di creare quelle classi in modo diverso" in mente. Che è una chiara violazione di YAGNI .

E le fabbriche diventano obsolete quando si introduce il framework IoC, perché IoC è solo una specie di fabbrica. E molti framework IoC sono in grado di creare implementazioni di specifiche fabbriche.

Inoltre, non esiste un modello di progettazione che dice di creare enormi classi statiche con metodi CreateXXX che chiamano solo costruttori. E in particolare non si chiama Fabbrica (né Fabbrica Astratta).

    
risposta data 14.08.2014 - 08:45
fonte
66

Il modello Factory vogue deriva da una credenza quasi dogmatica tra i programmatori nei linguaggi "C-style" (C / C ++, C #, Java) che l'uso della parola chiave "new" è negativo e dovrebbe essere evitato a tutti i costi (o almeno centralizzato). Questo, a sua volta, deriva da un'interpretazione ultra-rigorosa del Principio di Responsabilità Unica (la "S" di SOLID), e anche del Principio di Inversione di Dipendenza (la "D"). In parole povere, l'SRP afferma che idealmente un oggetto di codice dovrebbe avere un "motivo per cambiare" e uno solo; che "la ragione per cambiare" è lo scopo centrale di quell'oggetto, la sua "responsabilità" nella base di codice, e qualsiasi altra cosa che richiede una modifica al codice non dovrebbe richiedere l'apertura di quel file di classe. Il DIP è ancora più semplice; un oggetto codice non dovrebbe mai dipendere da un altro oggetto concreto, ma invece da un'astrazione.

Caso in questione, utilizzando "nuovo" e un costruttore pubblico, si sta accoppiando il codice chiamante a uno specifico metodo di costruzione di una specifica classe concreta. Il tuo codice ora deve sapere che esiste una classe MyFooObject e ha un costruttore che accetta una stringa e un int. Se quel costruttore ha bisogno di più informazioni, tutti gli usi del costruttore devono essere aggiornati per passare in quelle informazioni incluso quello che stai scrivendo ora, e quindi è necessario che abbiano qualcosa di valido da passare, e quindi devono avere o o essere cambiato per ottenerlo (aggiungendo più responsabilità agli oggetti che consumano). Inoltre, se MyFooObject viene mai sostituito nella base di codice da BetterFooObject, tutti gli usi della vecchia classe devono cambiare per costruire il nuovo oggetto invece di quello vecchio.

Quindi, invece, tutti i consumatori di MyFooObject dovrebbero essere direttamente dipendenti da "IFooObject", che definisce il comportamento delle classi di implementazione incluso MyFooObject. Ora, i consumatori di IFooObjects non possono semplicemente costruire un IFooObject (senza avere la consapevolezza che una particolare classe concreta è un IFooObject, di cui non hanno bisogno), quindi invece deve essere data un'istanza di una classe o metodo di implementazione IFooObject dall'esterno, da un altro oggetto che ha la responsabilità di sapere come creare l'IFooObject corretto per la circostanza, che nel nostro linguaggio è solitamente nota come Fabbrica.

Ora, ecco dove la teoria incontra la realtà; un oggetto può mai essere chiuso a tutti i tipi di modifiche per tutto il tempo. Caso in questione, IFooObject è ora un oggetto codice aggiuntivo nella base di codice, che deve cambiare ogni volta che l'interfaccia richiesta dai consumatori o dalle implementazioni di IFooObjects cambia. Ciò introduce un nuovo livello di complessità coinvolto nel cambiare il modo in cui gli oggetti interagiscono tra loro attraverso questa astrazione. Inoltre, i consumatori dovranno ancora cambiare, e in modo più approfondito, se l'interfaccia stessa viene sostituita da una nuova.

Un buon programmatore sa come bilanciare YAGNI ("Non ne hai bisogno") con SOLID, analizzando il design e trovando i posti che molto probabilmente dovranno cambiare in un modo particolare, e rifattorizzandoli per essere più tollerante a quel tipo di cambiamento, perché in quel caso "voi vi avremo bisogno".

    
risposta data 14.08.2014 - 18:13
fonte
29

I costruttori stanno bene quando contengono codice breve e semplice.

Quando l'inizializzazione diventa più che l'assegnazione di poche variabili ai campi, una fabbrica ha un senso. Ecco alcuni dei vantaggi:

  • Il codice lungo e complicato ha più senso in una classe dedicata (una fabbrica). Se lo stesso codice viene inserito in un costruttore che chiama un gruppo di metodi statici, questo inquinerà la classe principale.

  • In alcune lingue e in alcuni casi, lanciare eccezioni nei costruttori è una pessima idea , dal momento che può introdurre bug.

  • Quando invochi un costruttore, tu, il chiamante, devi conoscere il tipo esatto dell'istanza che desideri creare. Questo non è sempre il caso (come Feeder , ho solo bisogno di costruire il Animal per nutrirlo, non mi interessa se è un Dog o un Cat ).

risposta data 14.08.2014 - 05:40
fonte
9

Se lavori con interfacce, puoi rimanere indipendente dall'attuale implementazione. Una fabbrica può essere configurata (tramite proprietà, parametri o qualche altro metodo) per creare un'istanza di una serie di diverse implementazioni.

Un semplice esempio: vuoi comunicare con un dispositivo ma non sai se sarà via Ethernet, COM o USB. Definisci un'interfaccia e 3 implementazioni. In fase di esecuzione è possibile selezionare il metodo desiderato e la fabbrica fornirà l'implementazione appropriata.

Usalo spesso ...

    
risposta data 14.08.2014 - 07:41
fonte
6

È un sintomo di una limitazione nei sistemi di moduli di Java / C #.

In linea di principio, non c'è motivo per cui non si dovrebbe essere in grado di sostituire un'implementazione di una classe con un'altra con le stesse firme del costruttore e del metodo. Ci sono sono lingue che consentono questo. Tuttavia, Java e C # insistono sul fatto che ogni classe ha un identificativo univoco (il nome completo) e il codice client finisce con una dipendenza hard-coded su di esso.

Puoi sorta di aggirare il problema smanettando con il file system e le opzioni del compilatore in modo che com.example.Foo si associ a un file diverso, ma ciò è sorprendente e non intuitivo. Anche se lo fai, il tuo codice è ancora legato a una sola implementazione della classe. Cioè Se scrivi una classe Foo che dipende da una classe MySet , puoi scegliere un'implementazione di MySet al momento della compilazione, ma non puoi ancora creare un'istanza di Foo s utilizzando due diverse implementazioni di MySet .

Questa sfortunata decisione di progettazione obbliga le persone a usare interface s inutilmente per rendere il loro codice a prova di futuro contro la possibilità che in seguito avranno bisogno di una diversa implementazione di qualcosa, o per facilitare il test unitario. Questo non è sempre fattibile; se hai metodi che guardano i campi privati di due istanze della classe, non saranno in grado di implementarli in un'interfaccia. Ecco perché, ad esempio, non vedi union nell'interfaccia Set di Java. Tuttavia, al di fuori dei tipi e delle raccolte numeriche, i metodi binari non sono comuni, quindi di solito puoi farla franca.

Naturalmente, se chiami new Foo(...) hai ancora una dipendenza dalla classe , quindi hai bisogno di una fabbrica se vuoi che una classe sia in grado di creare un'istanza direttamente. Tuttavia, di solito è meglio accettare l'istanza nel costruttore e consentire a qualcun altro di decidere quale implementazione utilizzare.

Sta a te decidere se valga la pena aumentare il codice con le interfacce e le fabbriche. Da un lato, se la classe in questione è interna alla base di codice, il refactoring del codice in modo che utilizzi una classe diversa o un'interfaccia in futuro è banale; potresti invocare YAGNI e procedere al refactoring in un secondo momento, se la situazione si presentasse. Ma se la classe fa parte dell'API pubblica di una libreria che hai pubblicato, non hai la possibilità di correggere il codice del client. Se non usi un interface e in seguito hai bisogno di più implementazioni, rimarrai bloccato tra un rock e un luogo difficile.

    
risposta data 14.08.2014 - 13:54
fonte
2

Secondo me, stanno semplicemente usando la Simple Factory, che non è un modello di progettazione appropriato e non deve essere confusa con la Fabbrica astratta o il Metodo di fabbrica.

E dal momento che hanno creato una "enorme classe di tessuto con migliaia di metodi statici di CreateXXX", questo suona come un anti-modello (forse una classe di Dio?)

Penso che i metodi di Simple Factory e di creator statico (che non richiede una classe esterna), in alcuni casi, possano essere utili. Ad esempio, quando la costruzione di un oggetto richiede vari passaggi come l'istanziatura di altri oggetti (ad esempio, favorendo la composizione).

Non chiamerei nemmeno chiamare una Factory, ma solo un mucchio di metodi incapsulati in alcune classi casuali con il suffisso "Factory".

    
risposta data 14.08.2014 - 14:09
fonte
1

Come utente di una libreria, se la libreria ha metodi di produzione, dovresti usarli. Si presuppone che il metodo factory fornisca all'autore della libreria la flessibilità necessaria per apportare determinate modifiche senza influenzare il codice. Ad esempio, potrebbero restituire un'istanza di alcune sottoclassi in un metodo factory, che non funzionerebbe con un semplice costruttore.

Come creatore di una biblioteca, useresti i metodi di fabbrica se vuoi usare quella flessibilità da solo.

Nel caso in cui descrivi, sembra avere l'impressione che sostituire i costruttori con metodi di produzione sia stato inutile. Era certamente un dolore per tutti i soggetti coinvolti; una libreria non dovrebbe rimuovere nulla dalla sua API senza una buona ragione. Quindi, se avessi aggiunto metodi di produzione, avrei lasciato i costruttori esistenti disponibili, forse deprecati, fino a quando un metodo factory non chiamerà più quel costruttore e il codice usando il costruttore semplice funziona meno bene di quanto dovrebbe . La tua impressione potrebbe essere giusta.

    
risposta data 29.02.2016 - 09:53
fonte
-4

Questo sembra essere obsoleto nell'età di Scala e nella programmazione funzionale. Una solida base di funzioni sostituisce le classi di gazillion.

Da notare anche che il doppio% di{{ di Java non funziona più quando si utilizza una fabbrica i.e

someFunction(new someObject() {{
    setSomeParam(...);
    etc..
}})

Che ti consente di creare una classe anonima e di personalizzarla.

Nel dilemma dello spazio temporale il fattore tempo è ora molto ridotto, grazie alle CPU veloci che la programmazione funzionale consente la riduzione dello spazio, la dimensione del codice è ora pratica.

    
risposta data 07.03.2016 - 21:41
fonte

Leggi altre domande sui tag