C'è questa cosa in C ++ che mi ha fatto sentire a disagio per un bel po 'di tempo, perché onestamente non so come farlo, anche se sembra semplice:
Come posso implementare correttamente il metodo Factory in C ++?
Obiettivo: consentire al client di creare un'istanza di un oggetto utilizzando i metodi di fabbrica anziché i costruttori dell'oggetto, senza conseguenze inaccettabili e un impatto sulle prestazioni.
Per "modello di metodo di fabbrica" intendo sia metodi di fabbrica statici all'interno di un oggetto o metodi definiti in un'altra classe, sia funzioni globali. In genere "il concetto di reindirizzare il modo normale di istanziazione della classe X in qualsiasi altro luogo rispetto al costruttore".
Lasciami sfogliare alcune possibili risposte a cui ho pensato.
0) Non creare fabbriche, creare costruttori.
Sembra bello (e in effetti spesso la soluzione migliore), ma non è un rimedio generale. Prima di tutto, ci sono casi in cui la costruzione di oggetti è un'attività abbastanza complessa da giustificare la sua estrazione in un'altra classe. Ma anche mettere da parte questo fatto, anche per oggetti semplici che usano solo costruttori spesso non lo faranno.
L'esempio più semplice che conosco è una classe Vector 2D. Così semplice, ma difficile. Voglio essere in grado di costruirlo sia da coordinate cartesiane che polari. Ovviamente, non posso fare:
struct Vec2 {
Vec2(float x, float y);
Vec2(float angle, float magnitude); // not a valid overload!
// ...
};
Il mio modo di pensare naturale è quindi:
struct Vec2 {
static Vec2 fromLinear(float x, float y);
static Vec2 fromPolar(float angle, float magnitude);
// ...
};
Il che, invece dei costruttori, mi porta all'utilizzo di metodi statici di fabbrica ... il che significa essenzialmente che sto implementando il modello di fabbrica, in qualche modo ("la classe diventa la propria fabbrica"). Sembra bello (e si adatterebbe a questo caso particolare), ma in alcuni casi fallisce, che descriverò al punto 2. Continua a leggere.
un altro caso: provare a sovraccaricare di due typedef opachi di alcune API (come GUID di domini non correlati, o un GUID e un campo di bit), tipi semanticamente totalmente diversi (quindi - in teoria - sovraccarichi validi) ma che in realtà si rivelano essere i stessa cosa - come ints senza segno o puntatori vuoti.
1) The Java Way
Java è semplice, poiché abbiamo solo oggetti allocati dinamici. Fare una fabbrica è banale come:
class FooFactory {
public Foo createFooInSomeWay() {
// can be a static method as well,
// if we don't need the factory to provide its own object semantics
// and just serve as a group of methods
return new Foo(some, args);
}
}
In C ++, questo si traduce in:
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
};
Freddo? Spesso, infatti. Ma poi, questo costringe l'utente a utilizzare solo l'allocazione dinamica. L'allocazione statica è ciò che rende complesso il C ++, ma è anche ciò che spesso lo rende potente. Inoltre, credo che esistano alcuni obiettivi (parola chiave: incorporato) che non consentono l'allocazione dinamica. E ciò non implica che agli utenti di quelle piattaforme piace scrivere OOP pulito.
Comunque, filosofia a parte: nel caso generale, non voglio costringere gli utenti della fabbrica a essere limitati all'allocazione dinamica.
2) Ritorno per valore
OK, quindi sappiamo che 1) va bene quando vogliamo un'allocazione dinamica. Perché non aggiungeremo l'allocazione statica?
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooInSomeWay() {
return Foo(some, args);
}
};
Che cosa? Non possiamo sovraccaricare il tipo restituito? Oh, certo che non possiamo. Quindi cambiamo i nomi dei metodi per riflettere quello. E sì, ho scritto l'esempio di codice non valido sopra solo per sottolineare quanto non mi piace la necessità di cambiare il nome del metodo, ad esempio perché non possiamo implementare correttamente un design di fabbrica indipendente dalla lingua, poiché dobbiamo cambiare i nomi - e ogni utente di questo codice dovrà ricordare quella differenza dell'implementazione dalla specifica.
class FooFactory {
public:
Foo* createDynamicFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooObjectInSomeWay() {
return Foo(some, args);
}
};
OK ... eccolo qui. È brutto, poiché dobbiamo cambiare il nome del metodo. È imperfetto, dal momento che dobbiamo scrivere due volte lo stesso codice. Ma una volta fatto, funziona. Destra?
Bene, di solito. Ma a volte no. Quando creiamo Foo, in realtà dipendiamo dal compilatore per fare l'ottimizzazione del valore di ritorno per noi, perché lo standard C ++ è abbastanza benevolo per i fornitori del compilatore da non specificare quando l'oggetto verrà creato sul posto e quando verrà copiato quando restituisce un oggetto temporaneo per valore in C ++. Quindi, se Foo è costoso da copiare, questo approccio è rischioso.
E se Foo non fosse affatto copiabile? Bene, doh. ( Si noti che in C ++ 17 con elision copia garantita, il non essere copiabile non è più un problema per il codice sopra )
Conclusione: fare una fabbrica restituendo un oggetto è davvero una soluzione per alcuni casi (come il vettore 2D precedentemente citato), ma non è ancora una sostituzione generale per i costruttori.
3) Costruzione bifase
Un'altra cosa che probabilmente verrebbe fuori è la separazione della questione dell'allocazione degli oggetti e della sua inizializzazione. Questo di solito si traduce in codice come questo:
class Foo {
public:
Foo() {
// empty or almost empty
}
// ...
};
class FooFactory {
public:
void createFooInSomeWay(Foo& foo, some, args);
};
void clientCode() {
Foo staticFoo;
auto_ptr<Foo> dynamicFoo = new Foo();
FooFactory factory;
factory.createFooInSomeWay(&staticFoo);
factory.createFooInSomeWay(&dynamicFoo.get());
// ...
}
Si potrebbe pensare che funzioni come un fascino. L'unico prezzo che paghiamo nel nostro codice ...
Da quando ho scritto tutto questo e lasciato questo come ultimo, devo anche non gradirmi. :) Perché?
Prima di tutto ... Non mi piace sinceramente il concetto di costruzione in due fasi e mi sento in colpa quando lo uso. Se progetto i miei oggetti con l'affermazione che "se esiste, è in uno stato valido", sento che il mio codice è più sicuro e meno soggetto a errori. Mi piace così.
Dover abbandonare quella convenzione E cambiare il design del mio oggetto solo allo scopo di farne una fabbrica è ... beh, ingombrante.
So che quanto sopra non convincerà molte persone, quindi lasciami dare alcuni argomenti più solidi. Utilizzando la costruzione a due fasi, non è possibile:
- inizializza
const
o fa riferimento alle variabili dei membri, - passare argomenti ai costruttori di classi di base e ai costruttori di oggetti membro.
E probabilmente potrebbero esserci altri svantaggi a cui non riesco a pensare in questo momento, e non mi sento nemmeno particolarmente obbligato poiché i punti elenco sopra citati mi convincono già.
Quindi: nemmeno vicino a una buona soluzione generale per l'implementazione di una fabbrica.
conclusioni:
Vogliamo avere un modo di creare un'istanza dell'oggetto che:
- consentire un'istanza uniforme indipendentemente dall'allocazione,
- dare nomi diversi e significativi ai metodi di costruzione (quindi non fare affidamento sul sovraccarico per argomento),
- non introdurre un significativo aumento delle prestazioni e, preferibilmente, un notevole aumento del codice, soprattutto sul lato client,
- essere generale, come in: possibile essere introdotto per qualsiasi classe.
Credo di aver dimostrato che i modi che ho citato non soddisfano tali requisiti.
Qualche suggerimento? Per favore, forniscimi una soluzione, non voglio pensare che questo linguaggio non mi permetta di implementare correttamente un concetto così banale.
delete
lo fai . Questo tipo di metodi va benissimo, purché sia "documentato" (il codice sorgente è la documentazione ;-)) che il chiamante assume la proprietà del puntatore (leggi: è responsabile dell'eliminazione quando appropriato).
unique_ptr<T>
invece di T*
.