¿Volver puntero a objetos compuestos viola la encapsulación

8

Cuando quiero crear un objeto que agregue otros objetos, quiero dar acceso a los objetos internos en lugar de revelar la interfaz a los objetos internos con funciones de paso a través.

Por ejemplo, digamos que tenemos dos objetos:

class Engine;
using EnginePtr = unique_ptr<Engine>;
class Engine
{
public:
    Engine( int size ) : mySize( 1 ) { setSize( size ); }
    int getSize() const { return mySize; }
    void setSize( const int size ) { mySize = size; }
    void doStuff() const { /* do stuff */ }
private:
    int mySize;
};

class ModelName;
using ModelNamePtr = unique_ptr<ModelName>;
class ModelName
{
public:
    ModelName( const string& name ) : myName( name ) { setName( name ); }
    string getName() const { return myName; }
    void setName( const string& name ) { myName = name; }
    void doSomething() const { /* do something */ }
private:
    string myName;
};

Y digamos que queremos tener un objeto Car que esté compuesto por un Engine y un ModelName (obviamente, está ideado). Una posible forma de hacerlo sería dar acceso a cada uno de estos

/* give access */
class Car1
{
public:
    Car1() : myModelName{ new ModelName{ "default" } }, myEngine{ new Engine{ 2 } } {}
    const ModelNamePtr& getModelName() const { return myModelName; }
    const EnginePtr& getEngine() const { return myEngine; }
private:
    ModelNamePtr myModelName;
    EnginePtr myEngine;
};

El uso de este objeto se vería así:

Car1 car1;
car1.getModelName()->setName( "Accord" );
car1.getEngine()->setSize( 2 );
car1.getEngine()->doStuff();

Otra posibilidad sería crear una función pública en el objeto Car para cada una de las funciones (deseadas) en los objetos internos, como esta:

/* passthrough functions */
class Car2
{
public:
    Car2() : myModelName{ new ModelName{ "default" } }, myEngine{ new Engine{ 2 } } {}
    string getModelName() const { return myModelName->getName(); }
    void setModelName( const string& name ) { myModelName->setName( name ); }
    void doModelnameSomething() const { myModelName->doSomething(); }
    int getEngineSize() const { return myEngine->getSize(); }
    void setEngineSize( const int size ) { myEngine->setSize( size ); }
    void doEngineStuff() const { myEngine->doStuff(); }
private:
    ModelNamePtr myModelName;
    EnginePtr myEngine;
};

El segundo ejemplo se usaría así:

Car2 car2;
car2.setModelName( "Accord" );
car2.setEngineSize( 2 );
car2.doEngineStuff();

Mi preocupación con el primer ejemplo es que viola la encapsulación OO al dar acceso directo a los miembros privados.

Mi preocupación con el segundo ejemplo es que, a medida que llegamos a niveles más altos en la jerarquía de clases, podríamos terminar con clases "divinas" que tienen interfaces públicas muy grandes (viola el "I" en SÓLIDO).

¿Cuál de los dos ejemplos representa un mejor diseño OO? ¿O ambos ejemplos demuestran una falta de comprensión de OO?

Matthew James Briggs
fuente

Respuestas:

5

Me encuentro con ganas de dar acceso a los objetos internos en lugar de revelar la interfaz a los objetos internos con funciones pasantes.

Entonces, ¿por qué es interno?

El objetivo no es "revelar la interfaz al objeto interno" sino crear una interfaz coherente, consistente y expresiva. Si la funcionalidad de un objeto interno necesita exponerse y un simple paso será suficiente, entonces pase. El objetivo es un buen diseño, no "evitar la codificación trivial".

Dar acceso a un objeto interno significa:

  • El cliente tiene que saber sobre esas partes internas para usarlas.
  • Lo anterior significa que la abstracción deseada es expulsada del agua.
  • Expone los otros métodos y propiedades públicas del objeto interno, lo que permite al cliente manipular su estado de forma no intencionada.
  • Acoplamiento significativamente mayor. Ahora corre el riesgo de romper el código del cliente si modifica el objeto interno, cambia la firma de su método o incluso reemplaza el objeto completo (cambie el tipo).
  • Todo esto es por qué tenemos la Ley de Deméter. Demeter no dice "bueno, si solo está de paso, entonces está bien ignorar este principio".
radarbob
fuente
Nota al margen: el motor es muy relevante para la interfaz MaintainableVehicle pero no es relevante para DrivableVehicle. El mecánico necesita saber sobre el motor (en todos sus detalles, probablemente), pero el conductor no. (Y los pasajeros no necesitan saber sobre el volante)
user253751
2

No creo que necesariamente viole la encapsulación para devolver referencias al objeto envuelto, particularmente si son constantes. Ambos std::stringy std::vectorhaz esto. Si puede comenzar a alterar las partes internas del objeto debajo de él sin pasar por su interfaz, eso es más cuestionable, pero si ya pudiera hacerlo de manera efectiva con los setters, la encapsulación era una ilusión de todos modos.

Los contenedores son especialmente difíciles de encajar en este paradigma; Es difícil imaginar una lista útil que no se pueda descomponer en cabeza y cola. Hasta cierto punto, puede escribir interfaces como las std::find()que son ortogonales al diseño interno de la estructura de datos. Haskell va más allá con clases como Plegable y Traversible. Pero en algún momento, terminaste diciendo que todo lo que querías romper con la encapsulación ahora está dentro de la barrera de encapsulación.

Davislor
fuente
Y particularmente si la referencia es a una clase abstracta que se extiende por la implementación concreta que usa su clase; entonces no está revelando la implementación, sino simplemente proporcionando una interfaz que la implementación tiene que soportar.
Jules