Fabbrica statica vs fabbrica come singleton


24

In alcuni dei miei codici, ho una fabbrica statica simile a questa:

public class SomeFactory {

    // Static class
    private SomeFactory() {...}

    public static Foo createFoo() {...}

    public static Foo createFooerFoo() {...}
}

Durante una revisione del codice, è stato proposto che questo dovesse essere un singleton e iniettato. Quindi, dovrebbe apparire così:

public class SomeFactory {

    public SomeFactory() {}

    public Foo createFoo() {...}

    public Foo createFooerFoo() {...}
}

Alcune cose da evidenziare:

  • Entrambe le fabbriche sono apolidi.
  • L'unica differenza tra i metodi sono i loro ambiti (istanza vs statica). Le implementazioni sono le stesse.
  • Foo è un bean che non ha un'interfaccia.

Gli argomenti che ho avuto per diventare statico sono stati:

  • La classe è senza stato, pertanto non è necessario creare un'istanza
  • Sembra più naturale poter chiamare un metodo statico piuttosto che dover istanziare una fabbrica

Gli argomenti per la fabbrica come singleton erano:

  • È bello iniettare tutto
  • Nonostante l'apolidia della fabbrica, i test sono più facili con l'iniezione (facile da deridere)
  • Dovrebbe essere deriso durante il test del consumatore

Ho alcuni seri problemi con l'approccio singleton poiché sembra suggerire che nessun metodo dovrebbe mai essere statico. Sembra anche suggerire che programmi di utilità come quelli StringUtilsdovrebbero essere avvolti e iniettati, il che sembra sciocco. Infine, implica che dovrò deridere la fabbrica ad un certo punto, il che non sembra giusto. Non riesco a pensare a quando dovrei prendere in giro la fabbrica.

Cosa pensa la comunità? Anche se non mi piace l'approccio singleton, non mi sembra di avere un argomento terribilmente forte contro di esso.


2
La fabbrica viene utilizzata da qualcosa . Per testare quella cosa in isolamento devi fornirle una derisione della tua fabbrica. Se non deridi la tua fabbrica, un bug può causare un test unitario fallito quando non dovrebbe.
Mike,

Quindi, stai sostenendo contro le utility statiche in generale o solo fabbriche statiche?
bstempi,

1
Sto sostenendo contro di loro in generale. In C # per esempio le classi DateTimee Filesono notoriamente difficili da testare esattamente per le stesse ragioni. Se hai una classe, ad esempio, che imposta la Createddata DateTime.Nownel costruttore come fai a creare un unit test con due di questi oggetti che sono stati creati a 5 minuti di distanza? E gli anni a parte? Non puoi davvero farlo (senza molto lavoro).
Mike,

2
La tua fabbrica singleton non dovrebbe avere un privatecostruttore e un getInstance()metodo? Mi dispiace, incorreggibile raccoglitore di nit!
TMN,

1
@Mike - D'accordo - Ho spesso usato una classe DateTimeProvider che restituisce banalmente DateTime.Now. Puoi quindi scambiarlo con un finto che restituisce un tempo predeterminato, nei tuoi test. È un lavoro extra che lo passa a tutti i costruttori - il più grande mal di testa è quando i colleghi non lo acquistano al 100% e inseriscono riferimenti espliciti a DateTime. Ora per pigrizia ... :)
Julia Hayward,

Risposte:


15

Perché dovresti separare la tua fabbrica dal tipo di oggetto che crea?

public class Foo {

    // Private constructor
    private Foo(...) {...}

    public static Foo of(...) { return new Foo(...); }
}

Questo è ciò che Joshua Bloch descrive come il suo Articolo 1 a pagina 5 del suo libro "Efficace Java". Java è abbastanza dettagliato senza aggiungere ulteriori classi, singleton e quant'altro. Ci sono alcuni casi in cui ha senso una classe di fabbrica separata, ma sono pochi e lontani tra loro.

Per parafrasare l'articolo 1 di Joshua Bloch, diversamente dai costruttori, i metodi statici di fabbrica possono:

  • Hanno nomi in grado di descrivere tipi specifici di creazione di oggetti (Bloch utilizza probablePrime (int, int, Random) come esempio)
  • Restituisce un oggetto esistente (pensa: peso mosca)
  • Restituisce un sottotipo della classe originale o un'interfaccia implementata
  • Riduci la verbosità (specificando i parametri di tipo - vedi Bloch per esempio)

svantaggi:

  • Le classi senza un costruttore pubblico o protetto non possono essere sottoclassate (ma puoi usare costruttori protetti e / o Articolo 16: favorire la composizione sull'eredità)
  • I metodi statici di fabbrica non si distinguono dagli altri metodi statici (utilizzare un nome standard come of () o valueOf ())

Davvero, dovresti portare il libro di Bloch al tuo revisore del codice e vedere se tutti e due siete d'accordo con lo stile di Bloch.


Ci ho pensato anche io e penso che mi piaccia. Non ricordo perché li ho separati in primo luogo.
bstempi,

In generale, siamo d'accordo sullo stile di Bloch. Alla fine, trasferiremo i nostri metodi di fabbrica nella classe che stanno istanziando. L'unica differenza tra la nostra soluzione e la tua è che il costruttore rimarrà pubblico in modo che le persone possano ancora istanziare direttamente la classe. Grazie per la tua risposta!
bstempi,

Ma ... è orribile. Molto probabilmente vorrai una classe che implementa IFoo, e farla passare. Usando questo schema, non è più possibile; devi codificare IFoo per indicare Foo per ottenere istanze. Se hai IFooFactory che contiene un IFoo create (), il codice client non deve conoscere i dettagli di implementazione di quale Foo concreto è stato scelto come IFoo.
Wilbert,

@Wilbert public static IFoo of(...) { return new Foo(...); } Qual è il problema? Gli elenchi Bloch soddisfano la tua preoccupazione come uno dei vantaggi di questo progetto (secondo punto elenco sopra). Non devo capire il tuo commento.
GlenPeterson,

Per creare un'istanza, questo progetto prevede: Foo.of (...). Tuttavia, l'esistenza di Foo non dovrebbe mai essere scoperta, solo IFoo dovrebbe essere noto (poiché Foo non è che un impianto concreto di IFoo). Avere un metodo statico di fabbrica sull'impianto di cemento interrompe questo e porta a un accoppiamento tra il codice client e l'implementazione concreta.
Wilbert,

5

Appena al di sopra della mia testa, ecco alcuni dei problemi con la statica in Java:

  • i metodi statici non giocano secondo le "regole" OOP. Non puoi forzare una classe a implementare metodi statici specifici (per quanto ne so). Quindi non puoi avere più classi che implementano lo stesso insieme di metodi statici con le stesse firme. Quindi sei bloccato con uno di quello che stai facendo. Non puoi nemmeno sottoclassare una classe statica. Quindi nessun polimorfismo, ecc.

  • poiché i metodi non sono oggetti, i metodi statici non possono essere passati e non possono essere utilizzati (senza fare una tonnellata di codice di boilerplate), tranne per il codice che è strettamente collegato a loro - il che rende altamente il codice client accoppiati. (Per essere onesti, questo non è solo colpa della statica).

  • i campi statici sono abbastanza simili ai globi globali


Hai nominato:

Ho alcuni seri problemi con l'approccio singleton poiché sembra suggerire che nessun metodo dovrebbe mai essere statico. Sembra anche suggerire che utilità come StringUtils dovrebbero essere racchiuse e iniettate, il che sembra sciocco. Infine, implica che dovrò deridere la fabbrica ad un certo punto, il che non sembra giusto. Non riesco a pensare a quando dovrei prendere in giro la fabbrica.

Il che mi rende curioso di chiederti:

  • quali sono secondo te i vantaggi dei metodi statici?
  • credi che StringUtilssia progettato e implementato correttamente?
  • perché pensi che non dovrai mai deridere la fabbrica?

Ehi, grazie per la risposta! Nell'ordine che hai chiesto: (1) I vantaggi dei metodi statici sono lo zucchero sintattico e la mancanza di un'istanza non necessaria. È bello leggere un codice che ha un'importazione statica e dire qualcosa di simile Foo f = createFoo();piuttosto che avere un'istanza denominata. L'istanza stessa non fornisce alcuna utilità. (2) Penso di sì. È stato progettato per superare il fatto che molti di questi metodi non esistono all'interno della Stringclasse. Sostituire qualcosa di fondamentale come Stringnon è un'opzione, quindi i programmi di utilità sono la strada da percorrere. (proseguendo)
bstempi,

Sono facili da usare e non richiedono di preoccuparti in nessun caso. Si sentono naturali. (3) Perché la mia fabbrica è molto simile ai metodi di fabbrica all'interno Integer. Sono molto semplici, hanno pochissima logica e sono lì solo per offrire comodità (ad esempio, non c'è altro motivo OO per averli). La parte principale del mio argomento è che non fanno affidamento su nessuno stato - non c'è motivo di isolarli durante i test. Le persone non StringUtilssi prendono gioco dei test: perché dovrebbero deridere questa semplice fabbrica di convenienza?
bstempi,

3
Non prendono StringUtilsin giro perché c'è una ragionevole certezza che i metodi lì non hanno effetti collaterali.
Robert Harvey,

@RobertHarvey Suppongo che sto facendo lo stesso reclamo sulla mia fabbrica - non modifica nulla. Proprio come StringUtilsrestituisce alcuni risultati senza alterare gli input, anche la mia fabbrica non modifica nulla.
bstempi,

@bstempi Stavo cercando un po 'di metodo socratico lì. Immagino di farci schifo.

0

Penso che l'applicazione che stai costruendo debba essere abbastanza semplice, altrimenti sei relativamente all'inizio del suo ciclo di vita. L'unico altro scenario a cui riesco a pensare dove non avresti già avuto problemi con la tua statica è se sei riuscito a limitare le altre parti del codice che conoscono la tua Fabbrica a uno o due posti.

Immagina che la tua Applicazione si evolva e ora in alcuni casi devi produrre una FooBar(sottoclasse di Foo). Ora devi esaminare tutti i punti del codice che conoscono il tuo SomeFactorye scrivere una sorta di logica che guarda someConditione chiama SomeFactoryo SomeOtherFactory. In realtà, guardando il tuo codice di esempio, è peggio di così. Intendi semplicemente aggiungere un metodo dopo l'altro per creare Foos diversi e tutto il codice del tuo client deve verificare una condizione prima di capire quale Foo creare. Ciò significa che tutti i clienti devono avere una conoscenza intima di come funziona la tua fabbrica e tutti devono cambiare ogni volta che è necessario (o rimosso!) Un nuovo Foo.

Ora immagina se hai appena creato un IFooFactoryche fa un IFoo. Ora, produrre una sottoclasse diversa o persino un'implementazione completamente diversa è un gioco da ragazzi: basta alimentare la giusta IFooFactory più in alto nella catena e poi quando il client lo ottiene, chiama gimmeAFoo()e otterrà il giusto foo perché ha ottenuto la fabbrica giusta .

Forse ti starai chiedendo come si svolge nel codice di produzione. Per darvi un esempio dalla base di codice su cui sto lavorando che inizia a stabilizzarsi dopo poco più di un anno di progetti di avviamento, ho un sacco di fabbriche che vengono utilizzate per creare oggetti di dati di domande (sono qualcosa come modelli di presentazione ). Ho QuestionFactoryMapfondamentalmente un hash per cercare quale factory di domande usare quando. Quindi, per creare un'applicazione che pone domande diverse, ho inserito diverse fabbriche nella mappa.

Le domande possono essere presentate in Istruzioni o Esercizi (che implementano entrambi IContentBlock), in modo che ciascuno InstructionsFactoryo ExerciseFactoryottenga una copia di QuestionFactoryMap. Quindi, le varie fabbriche di Istruzioni ed Esercizi vengono consegnate alle ContentBlockBuilderquali mantiene di nuovo un hash di cui utilizzare quando. E poi quando l'XML viene caricato, ContentBlockBuilder richiama la Fabbrica di Esercizi o Istruzioni fuori dall'hash e glielo chiede createContent(), quindi la Fabbrica delega la costruzione delle domande a qualsiasi cosa estragga dalla sua QuestionFactoryMap interna in base a ciò si trova in ciascun nodo XML rispetto all'hash. Sfortunatamente , quella cosa è un BaseQuestionanziché unIQuestion, ma questo dimostra che anche quando stai attento al design puoi commettere errori che possono perseguitarti.

Il tuo intestino ti sta dicendo che queste statistiche ti faranno del male, e sicuramente ce ne sono molte in letteratura per sostenere il tuo intestino. Non perderai mai avendo l'iniezione delle dipendenze che finisci per usare solo per i tuoi test di unità (non che io creda che se costruisci un buon codice che non ti ritroverai ad usarlo più di quello), ma la statica può distruggere completamente la tua abilità per modificare rapidamente e facilmente il tuo codice. Quindi perché andarci?


Pensa per un momento ai metodi di fabbrica nella Integerclasse: cose semplici come la conversione da una stringa. Questo è quello che Foofa. Il mio Foonon è destinato ad essere esteso (colpa mia per non averlo dichiarato), quindi non ho i tuoi problemi polimorfici. Capisco il valore di avere fabbriche polimorfiche e le ho usate in passato ... Non penso proprio che siano appropriate qui. Potresti immaginare se devi istanziare una fabbrica per fare le semplici operazioni su Integerche sono attualmente integrate in fabbriche statiche?
bstempi,

Inoltre, i tuoi presupposti sull'applicazione sono piuttosto fuori. L'applicazione è abbastanza coinvolta. Questo Foo, tuttavia, è molto più semplice di molte classi. È solo un semplice POJO.
bstempi,

Se Foo è pensato per essere esteso, allora proprio lì c'è una ragione per cui Factory è un'istanza: tu fornisci l'istanza corretta della factory per darti la sottoclasse corretta invece di chiamare if (this) then {makeThisFoo()} else {makeThatFoo()}tutto il tuo codice. Una cosa che ho notato delle persone con un'architettura cronicamente negativa è che sono come le persone con dolore cronico. In realtà non sanno cosa si prova a non provare dolore, quindi il dolore che provano non sembra poi così grave.
Amy Blankenship,

Inoltre, potresti voler controllare questo link sui problemi con i metodi statici (anche quelli matematici!)
Amy Blankenship,

Ho aggiornato la domanda. Tu e un'altra persona menzionate l'estensione Foo. Giustamente, non l'ho mai dichiarato definitivo. Fooè un bean che non è pensato per essere esteso.
bstempi,
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.