Hay una cosa en C ++ que me ha estado haciendo sentir incómoda durante bastante tiempo, porque honestamente no sé cómo hacerlo, aunque parezca simple:
¿Cómo implemento Factory Method en C ++ correctamente?
Objetivo: hacer posible que el cliente pueda crear instancias de algún objeto utilizando métodos de fábrica en lugar de los constructores del objeto, sin consecuencias inaceptables y un impacto en el rendimiento.
Por "patrón de método de fábrica", me refiero a ambos métodos de fábrica estáticos dentro de un objeto o métodos definidos en otra clase, o funciones globales. En general, "el concepto de redirigir la forma normal de creación de instancias de la clase X a cualquier otro lugar que no sea el constructor".
Permítanme leer algunas posibles respuestas en las que he pensado.
0) No hagas fábricas, haz constructores.
Esto suena bien (y de hecho a menudo es la mejor solución), pero no es un remedio general. En primer lugar, hay casos en que la construcción de objetos es una tarea lo suficientemente compleja como para justificar su extracción a otra clase. Pero incluso dejando de lado ese hecho, incluso para objetos simples que usan solo constructores a menudo no funcionan.
El ejemplo más simple que conozco es una clase de vectores 2D. Tan simple, pero complicado. Quiero poder construirlo tanto desde coordenadas cartesianas como polares. Obviamente, no puedo hacer:
struct Vec2 {
Vec2(float x, float y);
Vec2(float angle, float magnitude); // not a valid overload!
// ...
};
Mi forma natural de pensar es entonces:
struct Vec2 {
static Vec2 fromLinear(float x, float y);
static Vec2 fromPolar(float angle, float magnitude);
// ...
};
Lo que, en lugar de los constructores, me lleva al uso de métodos estáticos de fábrica ... lo que esencialmente significa que estoy implementando el patrón de fábrica, de alguna manera ("la clase se convierte en su propia fábrica"). Esto se ve bien (y sería adecuado para este caso en particular), pero falla en algunos casos, que voy a describir en el punto 2. Siga leyendo.
otro caso: tratar de sobrecargar por dos tipos de letra opacos de alguna API (como GUID de dominios no relacionados, o un GUID y un campo de bits), tipos semánticamente totalmente diferentes (por lo tanto, en teoría, sobrecargas válidas) pero que en realidad resultan ser lo mismo, como entradas sin signo o punteros nulos.
1) La forma de Java
Java lo tiene simple, ya que solo tenemos objetos asignados dinámicamente. Hacer una fábrica es tan trivial como:
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);
}
}
En C ++, esto se traduce en:
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
};
¿Frio? A menudo, de hecho. Pero entonces, esto obliga al usuario a usar solo la asignación dinámica. La asignación estática es lo que hace que C ++ sea complejo, pero también es lo que a menudo lo hace poderoso. Además, creo que existen algunos objetivos (palabra clave: incrustado) que no permiten la asignación dinámica. Y eso no implica que a los usuarios de esas plataformas les guste escribir OOP limpio.
De todos modos, dejando de lado la filosofía: en el caso general, no quiero obligar a los usuarios de la fábrica a restringir la asignación dinámica.
2) Retorno por valor
OK, entonces sabemos que 1) es genial cuando queremos una asignación dinámica. ¿Por qué no agregaremos asignación estática además de eso?
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooInSomeWay() {
return Foo(some, args);
}
};
¿Qué? ¿No podemos sobrecargar por el tipo de retorno? Oh, por supuesto que no podemos. Así que cambiemos los nombres de los métodos para reflejar eso. Y sí, he escrito el ejemplo de código no válido anterior solo para enfatizar cuánto me disgusta la necesidad de cambiar el nombre del método, por ejemplo, porque ahora no podemos implementar un diseño de fábrica independiente del idioma, ya que tenemos que cambiar los nombres, y Todos los usuarios de este código deberán recordar esa diferencia de la implementación de la especificación.
class FooFactory {
public:
Foo* createDynamicFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooObjectInSomeWay() {
return Foo(some, args);
}
};
OK ... ahí lo tenemos. Es feo, ya que necesitamos cambiar el nombre del método. Es imperfecto, ya que necesitamos escribir el mismo código dos veces. Pero una vez hecho, funciona. ¿Correcto?
Bueno, por lo general. Pero a veces no. Al crear Foo, en realidad dependemos del compilador para hacer la optimización del valor de retorno para nosotros, porque el estándar C ++ es lo suficientemente benevolente para que los proveedores del compilador no especifiquen cuándo se creará el objeto en el lugar y cuándo se copiará al devolver un objeto temporal por valor en C ++. Entonces, si Foo es costoso de copiar, este enfoque es arriesgado.
¿Y si Foo no es copiable en absoluto? Bueno doh. ( Tenga en cuenta que en C ++ 17 con elisión de copia garantizada, no ser copiable ya no es un problema para el código anterior )
Conclusión: Hacer una fábrica devolviendo un objeto es de hecho una solución para algunos casos (como el vector 2-D mencionado anteriormente), pero aún no es un reemplazo general para los constructores.
3) construcción de dos fases
Otra cosa que probablemente se le ocurrirá a alguien es separar el tema de la asignación de objetos y su inicialización. Esto generalmente da como resultado un código como este:
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());
// ...
}
Uno puede pensar que funciona como un encanto. El único precio que pagamos en nuestro código ...
Como he escrito todo esto y lo he dejado como el último, tampoco me gusta. :) ¿Por qué?
En primer lugar ... me disgusta sinceramente el concepto de construcción en dos fases y me siento culpable cuando lo uso. Si diseño mis objetos con la afirmación de que "si existe, está en estado válido", siento que mi código es más seguro y menos propenso a errores. Me gusta de esa forma.
Tener que abandonar esa convención Y cambiar el diseño de mi objeto solo con el propósito de fabricarlo es ... bueno, difícil de manejar.
Sé que lo anterior no convencerá a muchas personas, así que permítanme dar algunos argumentos más sólidos. Usando la construcción de dos fases, no puede:
- inicializar
const
o referenciar las variables miembro, - pasar argumentos a constructores de clase base y constructores de objetos miembros.
Y probablemente podría haber algunos inconvenientes más en los que no puedo pensar en este momento, y ni siquiera me siento particularmente obligado ya que los puntos anteriores ya me convencen.
Entonces: ni siquiera cerca de una buena solución general para implementar una fábrica.
Conclusiones:
Queremos tener una forma de instanciación de objetos que:
- permitir una instanciación uniforme independientemente de la asignación,
- dar nombres diferentes y significativos a los métodos de construcción (por lo tanto, no depender de la sobrecarga de argumentos),
- no introducir un impacto significativo en el rendimiento y, preferiblemente, un impacto significativo en el aumento de código, especialmente en el lado del cliente,
- ser general, como en: posible ser introducido para cualquier clase.
Creo que he demostrado que las formas que he mencionado no cumplen esos requisitos.
¿Alguna pista? Por favor, proporciónenme una solución. No quiero pensar que este lenguaje no me permita implementar adecuadamente un concepto tan trivial.
delete
. Este tipo de métodos están perfectamente bien, siempre y cuando esté "documentado" (el código fuente es documentación ;-)) que la persona que llama toma posesión del puntero (léase: es responsable de eliminarlo cuando sea apropiado).
unique_ptr<T>
lugar de T*
.