Perché dovrei usare una classe di fabbrica anziché la costruzione diretta di oggetti?


162

Ho visto la storia di diversi progetti di librerie di classi С # e Java su GitHub e CodePlex, e vedo una tendenza a passare alle classi di fabbrica piuttosto che alla creazione diretta di oggetti.

Perché dovrei usare estesamente le classi di fabbrica? Ho una biblioteca abbastanza buona, in cui gli oggetti vengono creati alla vecchia maniera, invocando costruttori pubblici di classi. Negli ultimi commit gli autori hanno rapidamente cambiato tutti i costruttori pubblici di migliaia di classi in interni, e hanno anche creato un'enorme classe di fabbrica con migliaia di CreateXXXmetodi statici che restituiscono semplicemente nuovi oggetti chiamando i costruttori interni delle classi. L'API del progetto esterno è rotta, ben fatta.

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 classe pubblica con chiamate a metodi statici di fabbrica?

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



1
Che cos'è una classe / metodo di tessuto?
Doval,

Risposte:


56

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

Le fabbriche e le interfacce consentono una flessibilità molto più a lungo termine. Consente un design più disaccoppiato - e quindi più testabile. Ecco un elenco non esaustivo del motivo 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 simulare 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'Assemblea A (-> significa dipende da):

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

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

Assemblea 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 nell'assemblea B senza alcun dolore. Dipende ancora dalle interfacce nell'assieme A.

L'uso di un contenitore IoC per risolvere le tue dipendenze ti consente una flessibilità ancora maggiore. Non è necessario aggiornare ogni chiamata al costruttore ogni volta che si cambiano le dipendenze della classe.

Seguire il principio di segregazione dell'interfaccia e il principio di inversione di dipendenza ci consente di creare applicazioni altamente flessibili e disaccoppiate. Dopo aver lavorato su uno di questi tipi di applicazioni, non vorrai più tornare a utilizzare la newparola chiave.


40
Le fabbriche IMO sono la cosa meno importante per SOLID. Puoi fare SOLID bene senza fabbriche.
Euforico

26
Una cosa che non ha mai avuto senso per me è che se usi le fabbriche per creare nuovi oggetti, devi prima fare la fabbrica. Quindi cosa ti prende esattamente? Si presume che qualcun altro ti darà la fabbrica invece di crearne un'istanza da solo o qualcos'altro? Questo dovrebbe essere menzionato nella risposta, altrimenti non è chiaro come le fabbriche risolvano effettivamente qualsiasi problema.
Mehrdad,

8
@ BЈовић - Tranne il fatto che ogni volta che aggiungi una nuova implementazione, ora puoi aprire la fabbrica e modificarla per tenere conto della nuova implementazione - violando esplicitamente OCP.
Telastyn,

6
@Telastyn Sì, ma il codice che utilizza gli oggetti creati non cambia. Questo è più importante delle modifiche alla fabbrica.
BЈовић,

8
Le fabbriche sono utili in alcune aree a cui la risposta si è rivolta. Non sono appropriati da usare ovunque . Ad esempio, l'uso delle fabbriche per creare stringhe o elenchi sarebbe troppo lungo. Anche per gli UDT non sono sempre necessari. La chiave è usare una fabbrica quando è necessario disaccoppiare l'implementazione esatta di un'interfaccia.

126

Come detto whatsisname , credo che questo sia il caso della progettazione di software di culto delle merci . Le fabbriche, specialmente quelle astratte, sono utilizzabili solo quando il tuo modulo crea più istanze di una classe e vuoi dare all'utente questo modulo la possibilità di specificare quale tipo creare. Questo requisito è in realtà piuttosto raro, perché la maggior parte delle volte hai solo bisogno di un'istanza e puoi semplicemente passare quell'istanza direttamente invece di creare una factory esplicita.

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

Molte fabbriche sono create perché "Forse, un giorno, dovrò 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 fabbriche specifiche.

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


6
Sono d'accordo con la maggior parte dei tuoi punti tranne "IoC è una specie di fabbrica" : i contenitori IoC non sono fabbriche . Sono un metodo conveniente per ottenere l'iniezione di dipendenza. Sì, alcuni sono in grado di costruire fabbriche automatiche, ma non sono fabbriche stesse e non dovrebbero essere trattati come tali. Contenderei anche il punto YAGNI. È utile poter sostituire un doppio test nel test unitario. Rifattorizzare tutto per fornire questo dopo il fatto è un dolore nel culo . Pianifica in anticipo e non innamorarti della scusa "YAGNI"
AlexFoxGill

11
@AlexG - eh ... in pratica, praticamente tutti i contenitori IoC funzionano come fabbriche.
Telastyn,

8
@AlexG L'obiettivo principale di IoC è la costruzione di oggetti concreti per il passaggio in altri oggetti in base alla configurazione / convenzione. Questa è la stessa cosa che usare factory. E non è necessario che la fabbrica sia in grado di creare una simulazione per il test. Devi solo istanziare e passare la derisione direttamente. Come ho detto nel primo paragrafo. Le fabbriche sono utili solo quando si desidera passare la creazione di un'istanza all'utente del modulo, che chiama la fabbrica.
Euforico

5
C'è una distinzione da fare. Il modello di fabbrica viene utilizzato da un consumatore per creare entità durante il runtime di un programma. Un contenitore IoC viene utilizzato per creare il grafico a oggetti del programma durante l'avvio. Puoi farlo a mano, il contenitore è solo una comodità. Il consumatore di una fabbrica non dovrebbe essere a conoscenza del contenitore IoC.
AlexFoxGill

5
Ri: "E non è necessario che la fabbrica sia in grado di creare una simulazione per il test. Devi solo istanziare e passare la simulazione direttamente" - ancora una volta, circostanze diverse. Si utilizza una factory per richiedere un'istanza : il consumatore ha il controllo di questa interazione. Fornire un'istanza tramite il costruttore o un metodo non funziona, è uno scenario diverso.
AlexFoxGill

79

La moda del modello Factory deriva da una credenza quasi dogmatica tra i programmatori nei linguaggi "C-style" (C / C ++, C #, Java) che l'uso della "nuova" parola chiave è male e dovrebbe essere evitato a tutti i costi (o a meno centralizzato). Questo, a sua volta, deriva da un'interpretazione ultra-rigorosa del principio di responsabilità singola (la "S" di SOLID) e anche del principio di inversione di dipendenza (la "D"). In poche parole, l'SRP afferma che idealmente un oggetto codice dovrebbe avere una "ragione per cambiare" e una sola; quel "motivo per cambiare" è lo scopo centrale di quell'oggetto, la sua "responsabilità" nella base di codice e qualsiasi altra cosa che richieda 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,

Nel caso specifico, usando "new" e un costruttore pubblico, si sta accoppiando il codice chiamante a un metodo di costruzione specifico di una specifica classe di calcestruzzo. 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 sempre bisogno di più informazioni, tutti gli usi del costruttore devono essere aggiornati per passare quelle informazioni incluso quello che stai scrivendo ora, e quindi devono avere qualcosa di valido per passare, e quindi devono avere o essere cambiato per ottenerlo (aggiungendo più responsabilità agli oggetti che consumano). Inoltre, se MyFooObject viene mai sostituito nel codebase da BetterFooObject, tutti gli usi della vecchia classe devono cambiare per costruire il nuovo oggetto invece di quello vecchio.

Pertanto, invece, tutti i consumatori di MyFooObject dovrebbero dipendere direttamente 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 devono ricevere un'istanza di una classe o di un metodo che implementano 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 è generalmente conosciuta come Fabbrica.

Ora, ecco dove la teoria incontra la realtà; un oggetto non può mai essere chiuso a tutti i tipi di modifica in ogni momento. Caso in questione, IFooObject è ora un oggetto codice aggiuntivo nel codebase, che deve cambiare ogni volta che cambia l'interfaccia richiesta dai consumatori o le implementazioni di IFooObjects. 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 più in profondità, se l'interfaccia stessa verrà sostituita da una nuova.

Un buon programmatore sa come bilanciare YAGNI ("Non ne avrai bisogno") con SOLID, analizzando il design e trovando luoghi che molto probabilmente dovranno cambiare in un modo particolare e riformattandoli per essere più tolleranti nei confronti di che tipo di cambiamento, perché in tal caso "tu sei andando bisogno".


21
Adoro questa risposta, in particolare dopo aver letto tutti gli altri. Posso aggiungere che quasi tutti i (buoni) nuovi programmatori sono troppo dogmatici riguardo ai principi per il semplice fatto che vogliono davvero essere buoni ma non hanno ancora imparato il valore di mantenere le cose semplici e non esagerare.
Jean

1
Un altro problema con i costruttori pubblici è che non esiste un modo carino per una classe Foodi specificare che un costruttore pubblico dovrebbe essere utilizzabile per la creazione di Fooistanze o la creazione di altri tipi all'interno dello stesso pacchetto / assembly , ma non dovrebbe essere utilizzabile per la creazione di tipi derivati ​​altrove. Non conosco ragioni particolarmente convincenti che un linguaggio / framework non possa definire costruttori separati da usare nelle newespressioni, rispetto all'invocazione dei costruttori di sottotipi, ma non conosco alcun linguaggio che faccia questa distinzione.
supercat,

1
Un costruttore protetto e / o interno sarebbe un tale segnale; questo costruttore sarebbe disponibile per consumare il codice solo in una sottoclasse o all'interno dello stesso assembly. C # non ha una combinazione di parole chiave per "protetto e interno", il che significa che solo i sottotipi all'interno dell'assieme potrebbero usarlo, ma MSIL ha un identificatore di ambito per quel tipo di visibilità, quindi è concepibile che le specifiche C # possano essere estese per fornire un modo per utilizzarlo . Ma questo non ha molto a che fare con l'uso delle fabbriche (a meno che non si usi la restrizione della visibilità per imporre l'uso di una fabbrica).
KeithS,

2
Risposta perfetta Proprio al punto con la parte "teoria incontra la realtà". Basti pensare a quante migliaia di sviluppatori e tempo umano ha trascorso il culto delle merci lodando, giustificando, implementando, usandoli per poi cadere nella stessa situazione che hai descritto, è semplicemente esasperante. Dopo YAGNI, Ive non ha mai identificato la necessità di implementare una fabbrica
Breno Salgado il

Sto programmando su una base di codice in cui, poiché l'implementazione OOP non consente di sovraccaricare il costruttore, l'uso di costruttori pubblici è effettivamente vietato. Questo è già un problema minore nelle lingue che lo consentono. Come creare la temperatura. Sia Fahrenheit che Celsius sono galleggianti. Puoi comunque inscatolarli, quindi risolvere il problema.
jgmjgm,

33

I costruttori vanno bene quando contengono un codice breve e semplice.

Quando l'inizializzazione diventa più che l'assegnazione di alcune variabili ai campi, una fabbrica ha 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 sacco 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 costruirlo Animalper nutrirlo; non mi interessa se è un Dogo un Cat).


2
Le scelte non sono solo "fabbrica" ​​o "costruttore". A Feederpotrebbe non usare nessuno dei due, e invece chiamarlo è Kennelil getHungryAnimalmetodo dell'oggetto .
DougM,

4
+1 Trovo che aderire a una regola che non abbia assolutamente alcuna logica in un costruttore non è una cattiva idea. Un costruttore deve essere utilizzato solo per impostare lo stato iniziale dell'oggetto assegnando i valori degli argomenti alle variabili di istanza. Se è necessario qualcosa di più complicato, almeno creare un metodo factory (class) per creare l'istanza.
KaptajnKold,

Questa è l'unica risposta soddisfacente che ho visto qui, mi ha salvato dal dover scrivere la mia. Le altre risposte riguardano solo concetti astratti.
TheCatWhisperer

Ma anche questo argomento può essere ritenuto valido Builder Pattern. No?
soufrk,

I costruttori governano
Breno Salgado il

10

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

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 è quindi possibile selezionare il metodo desiderato e la fabbrica fornirà l'implementazione appropriata.

Usalo spesso ...


5
Aggiungo che è ottimo da usare quando ci sono più implementazioni di un'interfaccia e il codice chiamante non sa o non dovrebbe sapere quale scegliere. Ma quando un metodo di fabbrica è semplicemente un wrapper statico attorno a un singolo costruttore come nella domanda, questo è l'anti-pattern. Devono esserci più implementazioni da cui scegliere o la fabbrica si mette in mezzo e aggiunge complessità inutili.

Ora hai la flessibilità aggiuntiva e in teoria la tua applicazione può usare Ethernet, COM, USB e seriale, è pronta per essere utilizzata o in teoria. In realtà la tua app comunicherà solo su Ethernet .... sempre.
Pieter B,

7

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

In linea di principio, non vi è alcun motivo per cui non si dovrebbe essere in grado di scambiare un'implementazione di una classe con un'altra con le stesse firme del metodo e del costruttore. Ci sono lingue che lo consentono. Tuttavia, Java e C # insistono sul fatto che ogni classe ha un identificatore univoco (il nome completo) e il codice client finisce con una dipendenza hardcoded da esso.

È possibile sorta di ottenere intorno a questo giocherellare con il file system e opzioni del compilatore in modo che com.example.Foole mappe in un file diverso, ma questo è sorprendente e poco intuitivo. Anche se lo fai, il tuo codice è ancora legato a una sola implementazione della classe. Ad esempio, se scrivi una classe Fooche dipende da una classe MySet, puoi scegliere un'implementazione di MySetin fase di compilazione, ma non puoi ancora creare un'istanza Foousando due diverse implementazioni di MySet.

Questa sfortunata decisione di progettazione costringe le persone a utilizzare interfaceinutilmente le s per il loro codice a prova di futuro contro la possibilità che in seguito avranno bisogno di una diversa implementazione di qualcosa, o per facilitare i test unitari. Questo non è sempre fattibile; se hai dei metodi che guardano i campi privati ​​di due istanze della classe, non sarai in grado di implementarli in un'interfaccia. Ecco perché, ad esempio, non vedi unionnell'interfaccia di Java Set. Tuttavia, al di fuori dei tipi numerici e delle raccolte, i metodi binari non sono comuni, quindi di solito puoi cavartela.

Naturalmente, se chiami new Foo(...)hai ancora una dipendenza dalla classe , quindi hai bisogno di una factory se vuoi che una classe sia in grado di istanziare direttamente un'interfaccia. Tuttavia, di solito è un'idea migliore accettare l'istanza nel costruttore e lasciare che qualcun altro decida quale implementazione utilizzare.

Sta a te decidere se vale la pena gonfiare la tua base di codice con interfacce e fabbriche. Da un lato, se la classe in questione è interna alla tua base di codice, il refactoring del codice in modo che utilizzi una classe diversa o un'interfaccia in futuro è banale; potresti invocare YAGNI e refactor più tardi se si presenta la situazione. Ma se la classe fa parte dell'API pubblica di una libreria che hai pubblicato, non hai la possibilità di correggere il codice client. Se non usi un interfacee in seguito hai bisogno di più implementazioni, rimarrai bloccato tra una roccia e un luogo difficile.


2
Vorrei che Java e derivati ​​come .NET avessero una sintassi speciale per una classe che creava istanze di se stesso, e altrimenti sarebbe stato newsemplicemente uno zucchero sintattico per chiamare un metodo statico appositamente chiamato (che sarebbe generato automaticamente se un tipo avesse un "costruttore pubblico "ma non includeva esplicitamente il metodo). IMHO, se il codice vuole solo una noiosa cosa predefinita che implementa List, dovrebbe essere possibile che l'interfaccia ne dia una senza che il client debba conoscere una particolare implementazione (ad es ArrayList.).
supercat

2

A mio avviso, stanno solo utilizzando la Simple Factory, che non è un modello di progettazione adeguato e non deve essere confuso con la Abstract Factory o il Metodo Factory.

E dal momento che hanno creato una "enorme classe di fabric con migliaia di metodi statici CreateXXX", sembra un anti-pattern (una classe God forse?).

Penso che i metodi Simple Factory e i creatori statici (che non richiedono una classe esterna), possano essere utili in alcuni casi. Ad esempio, quando la costruzione di un oggetto richiede vari passaggi come l'installazione di altri oggetti (ad es. Favorire la composizione).

Non chiamerei nemmeno una Factory, ma solo un mucchio di metodi incapsulati in una classe casuale con il suffisso "Factory".


1
Simple Factory ha il suo posto. Immagina che un'entità abbia due parametri di costruzione int xe IFooService fooService. Non vuoi passare fooServicedappertutto, quindi crei una fabbrica con un metodo Create(int x)e inietti il ​​servizio all'interno della fabbrica.
AlexFoxGill

4
@AlexG E poi, devi passare IFactoryovunque invece che IFooService.
Euforico

3
Sono d'accordo con Euforico; nella mia esperienza, gli oggetti che vengono iniettati in un grafico dall'alto tendono ad essere di un tipo di cui hanno bisogno tutti gli oggetti di livello inferiore, quindi passare in giro IFooServices non è un grosso problema. Sostituire un'astrazione con un'altra non compie altro che offuscare ulteriormente la base di codice.
KeithS

questo sembra del tutto irrilevante per la domanda posta: "Perché dovrei usare una classe di fabbrica invece della costruzione diretta di oggetti? Quando dovrei usare costruttori pubblici e quando dovrei usare le fabbriche?" Vedi Come rispondere
moscerino

1
Questo è solo il titolo della domanda, penso che ti sia perso il resto. Vedi l'ultimo punto di collegamento fornito;).
FranMowinckel,

1

Come utente di una libreria, se la libreria ha metodi di fabbrica, è necessario utilizzarli. Supponresti che il metodo factory offra all'autore della libreria la flessibilità di apportare determinate modifiche senza influire sul 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 tu stesso quella flessibilità.

Nel caso che descrivi, sembra che tu abbia l'impressione che sostituire i costruttori con metodi di fabbrica sia stato inutile. È stato sicuramente un dolore per tutti i soggetti coinvolti; una libreria non dovrebbe rimuovere nulla dalla sua API senza un'ottima ragione. Quindi, se avessi aggiunto metodi di fabbrica, avrei lasciato disponibili i costruttori esistenti, forse deprecati, fino a quando un metodo di fabbrica non chiamerà più quel costruttore e il codice usando il costruttore semplice funzionerà meno di quanto dovrebbe. La tua impressione potrebbe benissimo essere giusta.


Nota anche; Se lo sviluppatore deve fornire una classe derivata con funzionalità extra (proprietà extra, inizializzazione), lo sviluppatore può farlo. (presupponendo che i metodi di fabbrica siano sostituibili). L'autore dell'API può anche fornire una soluzione per le esigenze speciali dei clienti.
Eric Schneider,

-4

Questo sembra essere obsoleto nell'era della Scala e nella programmazione funzionale. Una solida base di funzioni sostituisce un gazillion classi.

Da notare anche che il doppio di Java {{non funziona più quando si utilizza una factory, ad es

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

Che può permetterti di creare una classe anonima e personalizzarla.

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


2
sembra più un commento tangenziale (vedi Come rispondere ), e non sembra offrire nulla di sostanziale rispetto ai punti fatti e spiegati nelle precedenti risposte 9
moscerino
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.