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:
class Gadget {
vector<Widget> widgets;
public:
const vector<Widget>& GetWidgets() const { return widgets; }
}
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:
class Gadget {
vector<Widget> widgets;
public:
size_t GetWidgetCount() const;
const Widget& GetWidget(size_t i) const;
}
This allows you to change the type of widgets 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 widgets 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:
class Gadget {
vector<widget> widgets;
public:
typedef vector<Widget>::const_iterator WidgetIterator;
WidgetIterator WidgetBegin() const { return widgets.begin(); }
WidgetIterator WidgetEnd() const { return widgets.end(); }
}
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:
vector<Widget>::const_iterator it = gadget.WidgetBegin(); // leaky encapsulation
size_t num = gadget.WidgetEnd() - gadget.WidgetBegin(); // only works if random access iterator
And finally, probably the most encapsulated solution would be to actually define your own iterators:
class Gadget {
vector<Widget> widgets;
public:
struct WidgetIterator : iterator<bidirectional_iterator_tag, Widget> {
// ...
}
WidgetIterator WidgetBegin() const { ... }
WidgetIterator WidgetEnd() const { ... }
}
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 widgets 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



















