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


16

Digamos que tengo una clase Foobarque usa (depende de) la clase Widget. En los viejos tiempos, Widgetwolud 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 Foobary Widgetjuntos. La solución es aparentemente simple: aplique la inyección de dependencias para eliminar la construcción de dependencias fuera de Foobarclase. 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) {}
    (…)
}

Foobarreclama la propiedad exclusiva de lo Widgetque se le transfiere. Esto tiene las siguientes ventajas:

  1. El impacto en el rendimiento es insignificante.
  2. Es seguro, ya que Foobarcontrola su vida útil Widget, por lo que se asegura de que Widgetno desaparezca repentinamente.
  3. Es seguro que Widgetno tendrá fugas y se destruirá correctamente cuando ya no sea necesario.

Sin embargo, esto tiene un costo:

  1. Establece restricciones sobre cómo Widgetse pueden usar las instancias, por ejemplo, no se puede usar una pila asignada Widgets, no Widgetse 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:

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

Desventajas

  1. Malgasta recursos cuando no se trata de compartir.
  2. 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:

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

Contras:

  1. Ya no es seguro.
  2. Introduce otra entidad responsable de la propiedad de ambos Foobary Widget.

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 Foobarstodos modos hay que administrar algo , por lo que administrar Widgetses 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?


1
Desde el principio, si puede compilar en C ++ 11 y / o nunca, tener el puntero simple como una forma de manejar los recursos en una clase ya no es la forma recomendada. Si la clase Foobar es el único propietario del recurso y el recurso debe liberarse, cuando Foobar queda fuera del alcance, ese std::unique_ptres el camino a seguir. Puede usar std::move()para transferir la propiedad de un recurso desde el ámbito superior a la clase.
Andy

1
¿Cómo sé que Foobares 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.
el.pescado

1
@ el.pescado También existe el mismo problema en Java, la memoria no es el único recurso. También hay archivos y conexiones, por ejemplo. La forma habitual de resolver esto en Java es tener un contenedor que gestione el ciclo de vida de todos sus objetos contenidos. Por lo tanto, no tiene que preocuparse por eso en los objetos contenidos en un contenedor DI. Esto también podría funcionar para aplicaciones de c ++ si hay una manera para que el contenedor DI sepa cuándo cambiar a qué fase del ciclo de vida.
SpaceTrucker

3
Por supuesto, puede hacerlo a la antigua usanza sin toda la complejidad y tener un código que funcione y sea mucho más fácil de mantener. (Tenga en cuenta que los chicos Java pueden reír, pero siempre se va en alrededor de refactorización, por lo que la disociación no es exactamente ellos ayudar mucho)
gbjbaanb

3
Además, hacer que otras personas se rían de ti no es una razón para ser como ellos. Escuche sus argumentos (que no sean "porque se nos dijo que lo hiciéramos") e implemente DI si aporta un beneficio tangible a su proyecto. En este caso, piense en el patrón como una regla muy general, que debe aplicar de una manera específica para el proyecto: los tres enfoques (y todos los otros enfoques potenciales que aún no ha pensado) son válidos dados aportan un beneficio general (es decir, tienen más ventajas que desventajas).

Respuestas:


2

Tenía la intención de escribir esto como un comentario, pero resultó ser demasiado largo.

¿Cómo sé que ese Foobares el único dueño? 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.

Si debe usar std::unique_ptr<Widget>o std::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, como Foobar. Siguiendo el principio DI, necesitará la Widgetinstancia, para inyectarlo utilizando Foobarel constructor, es decir, dentro de uno de los Utilities::Factorymétodos, por ejemplo createWidget(const std::vector<std::string>& params), crea el widget y lo inyecta en el Foobarobjeto.

Ahora tiene un Utilities::Factorymétodo, que creó el Widgetobjeto. ¿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 Foobarclase, por lo que en realidad Foobaractúa como un controlador.

El controlador probablemente utilizará algunos de sus Widgetcorreos 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 seguir

También tiene una ventana de aplicación, donde hay Widgetuna 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á Widgetninguna 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::movepor std::unique_ptrcompleto transfiere la propiedad, por lo que incluso si crea un object Amétodo y lo pasa a otro object Bcomo una dependencia, object Bahora será responsable del recurso object Ay lo eliminará correctamente cuando esté object Bfuera de alcance.


La idea general de propiedad y punteros inteligentes es clara para mí. Simplemente no me parece jugar bien con la idea de DI. La inyección de dependencias, para mí, se trata de desacoplar la clase de sus dependencias, pero en este caso la clase todavía influye en cómo se crean sus dependencias.
el.pescado

Por ejemplo, el problema que tengo con las unique_ptrpruebas unitarias (DI se anuncia como amigable para las pruebas): quiero probar Foobar, así que creo Widget, lo paso Foobar, Foobarhago ejercicio y luego quiero inspeccionarlo Widget, pero a menos que de Foobaralguna manera lo exponga, puedo desde que ha sido reclamado por Foobar.
el.pescado

@ el.pescado Estás mezclando dos cosas juntas. Está mezclando accesibilidad y propiedad. El hecho de que sea Foobarpropietario del recurso no significa que nadie más deba usarlo. Podría implementar un Foobarmétodo como Widget* 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 la Widgetclase.
Andy

Buen punto, eso tiene sentido.
el.pescado

1
Si convirtió un shared_ptr en un unique_ptr, ¿cómo le gustaría garantizar unique_ness ???
Aconcagua

2

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_ptres la respuesta correcta.


1

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':

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);

¿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).


1
En C ++, no use classes 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.
Andy

@DavidPacker Hmm, claro, tienes razón: silenciosamente asumí que la clase tenía algunos elementos internos privados, pero eso no es visible ni mencionado en otra parte ...
Aconcagua

1

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 ++.


0

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:

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 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).


1
Esto impide el comportamiento polimórfico, que limita severamente el valor agregado.
el.pescado

Cierto. Sólo pensé hablar de ella, ya que es la forma que terminan utilizando los más (yo sólo uso mi herencia en la codificación para el comportamiento polimórfico - no la reutilización de código, por lo que no tengo muchos casos de necesidad comportamiento polimórfico) ..
Utnapistim
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.