Digamos que tengo una clase Foobar
que usa (depende de) la clase Widget
. En los viejos tiempos, Widget
wolud se declararía como un campo Foobar
, o tal vez como un puntero inteligente si fuera necesario un comportamiento polimórfico, y se inicializaría en el constructor:
class Foobar {
Widget widget;
public:
Foobar() : widget(blah blah blah) {}
// or
std::unique_ptr<Widget> widget;
public:
Foobar() : widget(std::make_unique<Widget>(blah blah blah)) {}
(…)
};
Y estaríamos listos y listos. Desafortunadamente, hoy, los niños de Java se reirán de nosotros una vez que lo vean, y con razón, como parejas Foobar
y Widget
juntos. La solución es aparentemente simple: aplique la inyección de dependencias para eliminar la construcción de dependencias fuera de Foobar
clase. Pero entonces, C ++ nos obliga a pensar en la propiedad de las dependencias. Se me ocurren tres soluciones:
Puntero único
class Foobar {
std::unique_ptr<Widget> widget;
public:
Foobar(std::unique_ptr<Widget> &&w) : widget(w) {}
(…)
}
Foobar
reclama la propiedad exclusiva de lo Widget
que se le transfiere. Esto tiene las siguientes ventajas:
- El impacto en el rendimiento es insignificante.
- Es seguro, ya que
Foobar
controla su vida útilWidget
, por lo que se asegura de queWidget
no desaparezca repentinamente. - Es seguro que
Widget
no tendrá fugas y se destruirá correctamente cuando ya no sea necesario.
Sin embargo, esto tiene un costo:
- Establece restricciones sobre cómo
Widget
se pueden usar las instancias, por ejemplo, no se puede usar una pila asignadaWidgets
, noWidget
se puede compartir.
Puntero compartido
class Foobar {
std::shared_ptr<Widget> widget;
public:
Foobar(const std::shared_ptr<Widget> &w) : widget(w) {}
(…)
}
Este es probablemente el equivalente más cercano de Java y otros lenguajes recolectados de basura. Ventajas:
- Más universal, ya que permite compartir dependencias.
- Mantiene la seguridad (puntos 2 y 3) de la
unique_ptr
solución.
Desventajas
- Malgasta recursos cuando no se trata de compartir.
- Todavía requiere la asignación del montón y no permite los objetos asignados a la pila.
Puntero de observación simple
class Foobar {
Widget *widget;
public:
Foobar(Widget *w) : widget(w) {}
(…)
}
Coloque el puntero en bruto dentro de la clase y transfiera la carga de la propiedad a otra persona. Pros:
- Tan simple como puede ser.
- Universal, acepta cualquiera
Widget
.
Contras:
- Ya no es seguro.
- Introduce otra entidad responsable de la propiedad de ambos
Foobar
yWidget
.
Alguna metaprogramación de plantillas locas
La única ventaja que se me ocurre es que sería capaz de leer todos esos libros para los que no encontré tiempo mientras mi software se está acumulando;)
Me inclino hacia la tercera solución, ya que es más universal, de Foobars
todos modos hay que administrar algo , por lo que administrar Widgets
es un cambio simple. Sin embargo, el uso de punteros sin formato me molesta, por otro lado, la solución de puntero inteligente me parece incorrecta, ya que hacen que el consumidor de dependencia restrinja cómo se crea esa dependencia.
¿Me estoy perdiendo de algo? ¿O simplemente la inyección de dependencia en C ++ no es trivial? ¿Debería la clase poseer sus dependencias o simplemente observarlas?
fuente
std::unique_ptr
es el camino a seguir. Puede usarstd::move()
para transferir la propiedad de un recurso desde el ámbito superior a la clase.Foobar
es el único propietario? En el viejo caso es simple. Pero el problema con DI, como lo veo, es que al desacoplar la clase de la construcción de sus dependencias, también la desacopla de la propiedad de esas dependencias (ya que la propiedad está vinculada a la construcción). En entornos de recolección de basura como Java, eso no es un problema. En C ++, esto es.Respuestas:
Tenía la intención de escribir esto como un comentario, pero resultó ser demasiado largo.
Si debe usar
std::unique_ptr<Widget>
ostd::shared_ptr<Widget>
, eso depende de usted decidir y proviene de su funcionalidad.Supongamos que tiene un
Utilities::Factory
, que es responsable de la creación de sus bloques, comoFoobar
. Siguiendo el principio DI, necesitará laWidget
instancia, para inyectarlo utilizandoFoobar
el constructor, es decir, dentro de uno de losUtilities::Factory
métodos, por ejemplocreateWidget(const std::vector<std::string>& params)
, crea el widget y lo inyecta en elFoobar
objeto.Ahora tiene un
Utilities::Factory
método, que creó elWidget
objeto. ¿Significa que el método debería ser responsable de su eliminación? Por supuesto no. Solo está ahí para hacerte la instancia.Imaginemos que está desarrollando una aplicación que tendrá múltiples ventanas. Cada ventana se representa usando la
Foobar
clase, por lo que en realidadFoobar
actúa como un controlador.El controlador probablemente utilizará algunos de sus
Widget
correos electrónicos y debe preguntarse:Si entro en esta ventana específica en mi aplicación, necesitaré estos Widgets. ¿Se comparten estos widgets entre otras ventanas de aplicaciones? Si es así, probablemente no debería crearlos de nuevo, porque siempre se verán iguales, porque se comparten.
std::shared_ptr<Widget>
es el camino a seguirTambién tiene una ventana de aplicación, donde hay
Widget
una ventana específicamente vinculada a esta única, lo que significa que no se mostrará en ningún otro lugar. Entonces, si cierra la ventana, ya no necesitaráWidget
ninguna parte de su aplicación, o al menos su instancia.Ahí es donde
std::unique_ptr<Widget>
viene a reclamar su trono.Actualizar:
Realmente no estoy de acuerdo con @DominicMcDonnell , sobre el problema de por vida. Invocar
std::move
porstd::unique_ptr
completo transfiere la propiedad, por lo que incluso si crea unobject A
método y lo pasa a otroobject B
como una dependencia,object B
ahora será responsable del recursoobject A
y lo eliminará correctamente cuando estéobject B
fuera de alcance.fuente
unique_ptr
pruebas unitarias (DI se anuncia como amigable para las pruebas): quiero probarFoobar
, así que creoWidget
, lo pasoFoobar
,Foobar
hago ejercicio y luego quiero inspeccionarloWidget
, pero a menos que deFoobar
alguna manera lo exponga, puedo desde que ha sido reclamado porFoobar
.Foobar
propietario del recurso no significa que nadie más deba usarlo. Podría implementar unFoobar
método comoWidget* getWidget() const { return this->_widget.get(); }
, que le devolverá el puntero sin formato con el que puede trabajar. Luego puede usar este método como entrada para sus pruebas unitarias, cuando desee probar laWidget
clase.Usaría un puntero de observación en forma de referencia. Ofrece una sintaxis mucho mejor cuando la usa y tiene la ventaja semántica de que no implica propiedad, lo que puede hacer un puntero simple.
El mayor problema con este enfoque son las vidas. Debe asegurarse de que la dependencia se construya antes y se destruya después de su clase dependiente. No es un problema simple. El uso de punteros compartidos (como almacenamiento de dependencia y en todas las clases que dependen de él, la opción 2 anterior) puede eliminar este problema, pero también introduce el problema de las dependencias circulares, que tampoco es trivial y, en mi opinión, es menos obvio y, por lo tanto, más difícil de resolver. detectar antes de que cause problemas. Es por eso que prefiero no hacerlo de forma automática y manual gestionar las vidas y el orden de construcción. También he visto sistemas que utilizan un enfoque de plantilla ligera que construyó una lista de objetos en el orden en que fueron creados y los destruyó en orden opuesto, no era infalible, pero hizo las cosas mucho más simples.
Actualizar
La respuesta de David Packer me hizo pensar un poco más sobre la pregunta. La respuesta original es cierta para las dependencias compartidas en mi experiencia, que es una de las ventajas de la inyección de dependencias, puede tener varias instancias utilizando la única instancia de una dependencia. Sin embargo, si su clase necesita tener su propia instancia de una dependencia particular, entonces
std::unique_ptr
es la respuesta correcta.fuente
En primer lugar, esto es C ++, no Java, y aquí muchas cosas son diferentes. La gente de Java no tiene esos problemas de propiedad, ya que existe una recolección automática de basura que los resuelve por ellos.
Segundo: no hay una respuesta general a esta pregunta, ¡depende de cuáles sean los requisitos!
¿Cuál es el problema sobre el acoplamiento de FooBar y Widget? FooBar quiere usar un widget, y si cada instancia de FooBar siempre tendrá su propio y el mismo widget, déjelo acoplado ...
En C ++, incluso puede hacer cosas "extrañas" que simplemente no existen en Java, por ejemplo, tener un constructor de plantilla variable (bueno, existe una ... notación en Java, que también se puede usar en constructores, pero eso es solo azúcar sintáctico para ocultar una matriz de objetos, ¡que en realidad no tiene nada que ver con plantillas variadas reales!) - categoría 'Alguna metaprogramación de plantilla loca':
¿Gusta?
Por supuesto, no son razones cuando se desea o necesita para desacoplar las dos clases - por ejemplo, si necesita crear los reproductores a la vez mucho antes de que exista cualquier instancia FooBar, si quieres o necesidad de reutilizar los widgets, ..., o simplemente porque para el problema actual es más apropiado (por ejemplo, si Widget es un elemento GUI y FooBar debe / puede / no debe ser uno).
Luego, volvemos al segundo punto: no hay una respuesta general. Debe decidir qué, para el problema real , es la solución más adecuada. Me gusta el enfoque de referencia de DominicMcDonnell, pero solo se puede aplicar si FooBar no tomará la propiedad (bueno, en realidad, podrías, pero eso implica un código muy, muy sucio ...). Aparte de eso, me uno a la respuesta de David Packer (la que debía escribirse como comentario, pero una buena respuesta, de todos modos).
fuente
class
es con nada más que métodos estáticos, incluso si es solo una fábrica como en su ejemplo. Eso viola los principios de OO. Use espacios de nombres en su lugar y agrupe sus funciones usándolos.Le faltan al menos dos opciones más que están disponibles en C ++:
Una es usar la inyección de dependencia 'estática' donde sus dependencias son parámetros de plantilla. Esto le da la opción de mantener sus dependencias por valor y al mismo tiempo permitir la inyección de dependencias de tiempo de compilación. Los contenedores STL utilizan este enfoque para asignadores y funciones de comparación y hash, por ejemplo.
Otra es tomar objetos polimórficos por valor utilizando una copia profunda. La forma tradicional de hacer esto es usar un método de clonación virtual, otra opción que se ha vuelto popular es usar el borrado de tipos para crear tipos de valores que se comporten polimórficamente.
La opción más apropiada realmente depende de su caso de uso, es difícil dar una respuesta general. Si solo necesita un polimorfismo estático, diría que las plantillas son la mejor opción en C ++.
fuente
Ignoraste una cuarta respuesta posible, que combina el primer código que publicaste (los "buenos días" de almacenar un miembro por valor) con inyección de dependencia:
El código del cliente puede escribir:
La representación del acoplamiento entre objetos no debe hacerse (puramente) según los criterios que usted enumeró (es decir, no se basa únicamente en "lo que es mejor para una gestión segura de por vida").
También puede usar criterios conceptuales ("un automóvil tiene cuatro ruedas" versus "un automóvil usa cuatro ruedas que el conductor debe llevar").
Puede tener criterios impuestos por otras API (si lo que obtiene de una API es un contenedor personalizado o un std :: unique_ptr, por ejemplo, las opciones en su código de cliente también son limitadas).
fuente