How far do you go to encapsulate?

Started by
6 comments, last by ApochPiQ 12 years, 1 month ago
So we all know that encapsulation is a good thing in OO design. Encapsulation increases maintainability and insulates clients from changes to implementation details. But at the same time, there ain't no such thing as a free lunch: there is an upfront cost (in terms of programmer time) to writing a class in an encapsulated manner.

So the question of how much you should encapsulate is, like most engineering decisions, a tradeoff: how much effort do you want to spend now to ease future development and maintenance?

I want to give a concrete example here: let's say you have a class Gadget, whose purpose is to maintain, update, and mutate an internal collection of Widgets. The internal representation of this collection isn't part of Gadget's interface and is an implementation detail - so ideally you want to hide this from clients of the Gadget class. The Gadget class needs to allow its clients to somehow observe its collection of Widgets, without exposing too much of its implementation details. And ideally, only forward and backward iteration of Widgets should be exposed to clients: that way we can use vector, list, set, or any other container in the implementation without affecting client code.

The simplest solution would be to just return a reference to the internal collection:
[source]
class Gadget {
vector<Widget> widgets;

public:
const vector<Widget>& GetWidgets() const { return widgets; }
}
[/source]

This hides very little: client code knows that you use a vector and must take a dependency on that. You also cannot change the implementation (eg. can't change a vector to a list or whatever) without modifying your interface.

A second option would be to provide an index accessor like this:

[source]
class Gadget {
vector<Widget> widgets;

public:
size_t GetWidgetCount() const;
const Widget& GetWidget(size_t i) const;
}
[/source]

This allows you to change the type of [font=courier new,courier,monospace]widgets[/font] without affecting client code, but with a caveat: you've allowed random access to the underlying Widgets collection. In this case, you wouldn't be able to change [font=courier new,courier,monospace]widgets[/font] to map or list or something else that doesn't support random access.

A similar, but different option would be to expose iterators to the data:

[source]
class Gadget {
vector<widget> widgets;

public:
typedef vector<Widget>::const_iterator WidgetIterator;

WidgetIterator WidgetBegin() const { return widgets.begin(); }
WidgetIterator WidgetEnd() const { return widgets.end(); }
}
[/source]

But that's still not perfect: typedefs are are not actually separate types so it doesn't stop anyone from accidentally doing stuff like this:

[font=courier new,courier,monospace]vector<Widget>::const_iterator it = gadget.WidgetBegin(); // leaky encapsulation[/font]
[font=courier new,courier,monospace]size_t num = gadget.WidgetEnd() - gadget.WidgetBegin(); // only works if random access iterator[/font]

And finally, probably the most encapsulated solution would be to actually define your own iterators:

[source]
class Gadget {
vector<Widget> widgets;

public:
struct WidgetIterator : iterator<bidirectional_iterator_tag, Widget> {
// ...
}

WidgetIterator WidgetBegin() const { ... }
WidgetIterator WidgetEnd() const { ... }
}
[/source]

This is the most encapsulated solution. It exposes exactly nothing more than it needs to, and allows complete freedom for you to change the internals of Gadget without affecting client code. You can even make [font=courier new,courier,monospace]widgets[/font] an associative data structure like map (whose iterator iterates over key/value pairs), and write your custom iterator to discard the key and return only the value. But it's also the most difficult and costly to write: custom iterators aren't entirely trivial to write and the extra code to implement the custom iterator is itself a maintenance burden.

So there are many ways to achieve the same thing, with varying degrees of complexity and encapsulation. So my question is: in your projects, which option do you usually choose? If you were tasked with implementing the Gadget class, would you go with the simple and cheap option, the complex but safe option, or something entirely different?

edit: fix angle brackets
NextWar: The Quest for Earth available now for Windows Phone 7.
Advertisement
The editor ate all my angle brackets. :(
NextWar: The Quest for Earth available now for Windows Phone 7.
The single responsibility principle probably applies best here.

An object or class should have a single responsibility.

A utility class, perhaps RandUtils, should have a single responsibility of generating random numbers.
A seed spawner class should have a single responsibility of spawning seeds.
A vector3 class should have a single responsibility of 3x1 array of numbers.
A time class should have a single responsibility of knowing the clock.

A Widget class should only have stuff dealing directly with a widget. It should not have any particular knowledge of gadgets. It should not have any knowledge of a collection of widgets. It should only do one thing, and do it well. It should be the single point of interface for all things widget related.

The single responsibility principle probably applies best here.

An object or class should have a single responsibility.

A utility class, perhaps RandUtils, should have a single responsibility of generating random numbers.
A seed spawner class should have a single responsibility of spawning seeds.
A vector3 class should have a single responsibility of 3x1 array of numbers.
A time class should have a single responsibility of knowing the clock.

A Widget class should only have stuff dealing directly with a widget. It should not have any particular knowledge of gadgets. It should not have any knowledge of a collection of widgets. It should only do one thing, and do it well. It should be the single point of interface for all things widget related.


Yes, that's correct but I don't think that's really the issue here. The Gadget is just an example, and whose sole purpose is to manage a collection of Widgets and to allow for read-only enumeration of Widgets by clients. You can call it "WidgetCollection" instead of "Gadget" if you wish, but the question was more around how to implement it and making the choice between complexity vs. encapsulation.
NextWar: The Quest for Earth available now for Windows Phone 7.
Implementation details, like degree of encapsulation, depend not only what a class does, but also on why it exists. Why does your WidgetCollection exist? What problem is it intended to solve?
Right.
Reading the code example in the first post, the obvious encapsulation issue is not what's the best way for class Gadget to expose its vector<Widget>, but why the hell does Gadget allow client code to see and modify its precious Widgets in the first place.

A serious class, as opposed to a trivial example, would have meaningful operations like void replaceWidget(const Widget& obsoleteScrap, const Widget& improvedReplacement) or void deleteRustedWidgets().
On the other hand, if you want a basic collection of widgets there is nothing wrong with putting a vanilla std::vector<Widget>, std::vector<Widget*> or the like as a private or protected member in the classes that keepcollections of Widgets..

Omae Wa Mou Shindeiru

Providing a function to return a const reference to the widget collection is the level of encapsulation I usually settle on. Controlling changes to the underlying container by requiring people to use your implementation of addWidget() rather than vector push_back(), is likely to mitigate most of the problems you'd face when changing your implementation in the future. Most of the time classes containing collections will perform some logic behind the scenes when the user adds or removes something, so not exposing a mutable collection object is just thinking ahead.

But really, you can skip all the headache and just use a map. Expose find, insert and delete methods, and bidirectional iterators for traversal. If you provide the flexibility of maps from the beginning, you don't have to worry about breaking the implementation if you find out you need them in the future. Plus there are several avenues of optimisation of this structure without breaking the interface, should the situation warrant. (You could switch to hash_maps, etc)

Implementation details, like degree of encapsulation, depend not only what a class does, but also on why it exists. Why does your WidgetCollection exist? What problem is it intended to solve?


This, a thousand times.

Trying to extract general design principles from a contrived example is about as useful as trying to learn how to drive by staring at a steering wheel.

Wielder of the Sacred Wands
[Work - ArenaNet] [Epoch Language] [Scribblings]

This topic is closed to new replies.

Advertisement