8

When I want to create an object which aggregates other objects, I find myself wanting to give access to the internal objects instead of revealing the interface to the internal objects with passthrough functions.

For example, say we have two objects:

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

And lets say we want to have a Car object which is composed of both an Engine and a ModelName (this is contrived obviously). One possible way to do so would be to give access to each of these

/* 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;
};

Using this object would look like this:

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

Another possibility would be to create a public function on the Car object for each of the (desired) functions on the internal objects, like this:

/* 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;
};

The second example would be used like this:

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

My concern with the first example is that it violates OO encapsulation by giving direct access to the private members.

My concern with the second example is that, as we get to higher levels in the class hierarchy, we could wind up with "god-like" classes that have very large public interfaces (violates the "I" in SOLID).

Which of the two examples represents better OO design? Or do both examples demonstrate a lack of OO comprehension?

2 Answers 2

5

I find myself wanting to give access to the internal objects instead of revealing the interface to the internal objects with passthrough functions.

So, why then is it internal?

The objective is not to "reveal the interface to the internal object" but to create a coherent, consistent, expressive interface. If an internal object's functionality needs to be exposed and a simple pass-through will do, then pass-through. Good design is the goal, not "avoid trivial coding."

Giving access to an internal object means:

  • The client has to know about those internals to use them.
  • The above means desired abstraction is blown out of the water.
  • You expose the internal object's other public methods and properties, allowing the client to manipulate your state in unintended ways.
  • Significantly increased coupling. Now you are at risk of breaking client code should you modify the internal object, change it's method signature, or even replace the whole object (change the type).
  • All of this is why we have the Law of Demeter. Demeter does not say "well, if it's just passing through, then it's ok to ignore this principle."
1
  • Side note: the engine is very relevant to the MaintainableVehicle interface but it's not relevant to DrivableVehicle. The mechanic needs to know about the engine (in all its detail, probably), but the driver doesn't. (And passengers don't need to know about the steering wheel)
    – user20574
    Commented Feb 24, 2020 at 16:02
2

I don’t think it necessarily violates encapsulation to return references to the wrapped object, particularly if they’re const. Both std::string and std::vector do this. If you can start altering the object’s internals from under it without going through its interface, that’s more questionable, but if you could already effectively do that with setters, encapsulation was an illusion anyway.

Containers are especially hard to fit into this paradigm; it’s difficult to imagine a useful list that can’t be decomposed into a head and tail. To some extent, you can write interfaces such as std::find() that are orthogonal to the internal layout of the data structure. Haskell goes further with classes such as Foldable and Traversible. But at some point, you’ve ended up saying that everything you wanted to break encapsulation to do is now inside the encapsulation barrier.

1
  • And particularly if the reference is to an abstract class that is extended by the concrete implementation your class uses; then you are not revealing the implementation, but just providing an interface that the implementation has to support.
    – Jules
    Commented Sep 27, 2015 at 13:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.