C ++: ¿Debería la clase poseer u observar sus dependencias?

15

Supongamos que tengo una clase Foobar que usa (depende de) la clase Widget . En los buenos tiempos, Widget podría ser declarado como un campo en Foobar , o tal vez como un puntero inteligente si se necesita el 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 en día, los niños de Java se reirán de nosotros una vez que lo vean, y con razón, ya que juntan Foobar y Widget juntos. La solución es aparentemente simple: aplique la inyección de dependencia para eliminar la construcción de dependencias de la clase Foobar . Pero entonces, C ++ nos obliga a pensar en la propiedad de las dependencias. Tres soluciones vienen a la mente:

puntero único

class Foobar {
    std::unique_ptr<Widget> widget;
    public:
    Foobar(std::unique_ptr<Widget> &&w) : widget(w) {}
    (…)
}

Foobar reclama la propiedad exclusiva de Widget que se le pasa. Esto tiene las siguientes ventajas:

  1. El impacto en el rendimiento es insignificante.
  2. Es seguro, ya que Foobar controla la vida útil de su Widget , por lo que se asegura de que Widget no desaparezca repentinamente.
  3. Es seguro que Widget no se filtrará y se destruirá adecuadamente cuando ya no sea necesario.

Sin embargo, esto tiene un costo:

  1. Establece restricciones sobre cómo se pueden usar Widget instancias, por ejemplo. no se puede usar Widgets asignado a la pila, no se puede compartir Widget .

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 a Java y otros lenguajes recolectados. Ventajas:

  1. Más universal, ya que permite compartir dependencias.
  2. Mantiene la seguridad (puntos 2 y 3) de la solución unique_ptr .

Desventajas:

  1. Desperdicia recursos cuando no se trata de compartir.
  2. Aún requiere la asignación de almacenamiento dinámico y no permite objetos asignados a la pila.

puntero de observación llano ol '

class Foobar {
    Widget *widget;
    public:
    Foobar(Widget *w) : widget(w) {}
    (…)
}

Coloque el puntero en bruto dentro de la clase y transfiera la carga de propiedad a otra persona. Pros:

  1. Tan simple como puede ser.
  2. Universal, acepta cualquier Widget .

Contras:

  1. Ya no es seguro.
  2. Introduce otra entidad que es responsable de la propiedad de Foobar y Widget .

Una metaprogramación de plantillas locas

La única ventaja en la que puedo pensar es que podría leer todos los libros para los que no encontré tiempo mientras mi software está funcionando;)

Me inclino por la tercera solución, ya que es la más universal, algo tiene que administrar Foobars de todos modos, por lo que administrar Widgets es un simple cambio. Sin embargo, usar punteros en bruto me molesta, por otro lado, la solución de puntero inteligente me parece mal, ya que hacen que el consumidor de dependencia restrinja cómo se crea esa dependencia.

¿Me estoy perdiendo algo? ¿O simplemente la inyección de dependencia en C ++ no es trivial? ¿Debería la clase poseer sus dependencias o simplemente observarlas?

    
pregunta el.pescado 07.09.2015 - 22:41
fuente

5 respuestas

2

Quise escribir esto como un comentario, pero resultó ser demasiado largo.

  

¿Cómo sé que Foobar es el único propietario? En el caso antiguo es simple. Pero el problema con la DI, como lo veo, es que al separar a la clase de la construcción de sus dependencias, también la desvincula de la propiedad de esas dependencias (ya que la propiedad está ligada a la construcción). En entornos de recolección de basura, como Java, eso no es un problema. En C ++, esto es.

Si debes usar std::unique_ptr<Widget> o std::shared_ptr<Widget> , eso depende de ti decidir y viene de tu funcionalidad.

Supongamos que tiene un Utilities::Factory , que es responsable de la creación de sus bloques, como Foobar . Siguiendo el principio DI, necesitará la instancia Widget , para inyectarla usando el constructor de Foobar , es decir, dentro de uno de los métodos de Utilities::Factory , por ejemplo, createWidget(const std::vector<std::string>& params) , usted crea el Widget y lo inyecta. en el objeto Foobar .

Ahora tienes un método Utilities::Factory , que creó el objeto Widget . ¿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ás desarrollando una aplicación que tendrá múltiples ventanas. Cada ventana se representa mediante la clase Foobar , por lo que, de hecho, Foobar actúa como un controlador.

El controlador probablemente usará algunos de tus Widget s y debes preguntarte:

Si voy a 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 están compartidos.

std::shared_ptr<Widget> es el camino a seguir.

También tiene una ventana de aplicación, donde hay un Widget específicamente vinculado a esta única ventana, lo que significa que no se mostrará en ningún otro lugar. Entonces, si cierras la ventana, ya no necesitas el Widget en ninguna parte de tu 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. Llamar a std::move en std::unique_ptr transfiere completamente la propiedad, por lo que incluso si crea un object A en un método y lo pasa a otro object B como una dependencia, el object B ahora será responsable del recurso de object A y lo eliminará correctamente, cuando object B quede fuera del alcance.

    
respondido por el Andy 08.09.2015 - 08:46
fuente
2

Yo usaría un puntero de observación en forma de una referencia. Da una sintaxis mucho mejor cuando la usas y tiene la ventaja semántica de que no implica propiedad, lo que puede hacer un puntero simple.

El mayor problema con este enfoque es la vida útil. 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 el almacenamiento de dependencias y en todas las clases que dependen de él, la opción 2 anterior) puede eliminar este problema, pero también presenta el problema de las dependencias circulares que también es no trivial y, en mi opinión, menos obvio y por lo tanto más difícil Detectar antes que cause problemas. Es por eso que prefiero no hacerlo de forma automática y manual, la vida útil 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 fue 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 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.

    
respondido por el Dominic McDonnell 08.09.2015 - 01:14
fuente
1

En primer lugar, esto es C ++, no Java, y aquí muchas cosas son diferentes. La gente de Java no tiene esos problemas sobre la propiedad, ya que existe una recolección automática de basura que los resuelve.

Segundo: no hay una respuesta general a esta pregunta, ¡depende de cuáles sean los requisitos!

¿Cuál es el problema con el acoplamiento de FooBar y Widget? FooBar quiere usar un Widget, y si cada instancia de FooBar siempre tendrá su propio Widget de todos modos, déjelo acoplado ...

En C ++, incluso puedes hacer cosas "extrañas" que simplemente no existen en Java, e. sol. tener un constructor de plantillas variadic (bueno, existe una ... notación en Java, que también se puede usar en constructores, pero eso es solo azúcar sintáctica para ocultar una matriz de objetos, que en realidad no tiene nada que ver con plantillas variadic reales. ) - categoría 'metaprogramación de algunas plantillas locas':

namespace WidgetFactory
{
    Widget* create(int, int)
    {
        return 0;
    }
    Widget* create(int, bool, long)
    {
        return 0;
    }
}
class FooBar
{
public:
    template < typename ...Arguments >
    FooBar(Arguments... arguments)
        : mWidget(WidgetFactory::create(arguments...))
    {
    }
    ~FooBar()
    {
        delete mWidget;
    }
private:
    Widget* mWidget;
};

FooBar foobar1(10, 12);
FooBar foobar2(51, true, 54L);

¿Te gusta?

Por supuesto, hay razones por las que desea o necesita desacoplar ambas clases - e. sol. si necesita crear los widgets mucho antes de que exista una instancia de FooBar, si desea o necesita 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 respuesta general. Debe decidir cuál es la solución más adecuada para el problema real . Me gusta el enfoque de referencia de DominicMcDonnell, pero solo se puede aplicar si FooBar no toma la propiedad (bueno, en realidad, podría, pero eso implica un código muy, muy sucio ...). Aparte de eso, me sumo a la respuesta de David Packer (la que debía escribirse como comentario, pero de todos modos es una buena respuesta).

    
respondido por el Aconcagua 10.09.2015 - 03:09
fuente
1

Faltan al menos dos opciones más disponibles en C ++:

Uno 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 permite la inyección de dependencia en 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 usando 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 tipo para hacer que los tipos de valor se comporten de forma polimórfica.

La opción más apropiada realmente depende de su caso de uso, es difícil dar una respuesta general. Si solo necesitas polimorfismo estático, diría que las plantillas son la mejor manera de ir a C ++.

    
respondido por el mattnewport 10.09.2015 - 23:32
fuente
0

Usted ignoró una cuarta respuesta posible, que combina el primer código que publicó (los "buenos días" de almacenar un miembro por valor) con la inyección de dependencia:

class Foobar {
    Widget widget;
public:
    Foobar(Widget w) // pass w by value
     : widget{ std::move(w) } {}
};

El código del cliente puede escribir:

Widget w;
Foobar f1{ w }; // default: copy w into f1
Foobar f2{ std::move(w) }; // move w into f2

La representación del acoplamiento entre objetos no se debe realizar (puramente) en 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" en comparación con "un automóvil utiliza las cuatro ruedas que el conductor tiene que 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 están limitadas).

    
respondido por el utnapistim 08.09.2015 - 11:00
fuente

Lea otras preguntas en las etiquetas