This is a long post, you might want to grab a cup of coffee before reading.
Recently I've been exposed to
Miško Hevery's blog and his
Guide to Writing Testable Code.
Specifically, the following guideline:
Do not create collaborators in your constructor, but pass them in.
This is meant to make it easy to pass in fake objects to test your class. It's just basic dependency injection, and it helps make code reusable.
With languages like C# and Java, this is usually not a problem since objects are cleaned up by the garbage collector when they're no longer in use. In C++, we have an issue.
[ A ] Who is responsible for destroying the collaborators?
There are three options:
1. Whoever created the collaborators.
2. The object that depends on the collaborators.
3. Use smart pointers (either strict ownership or shared ownership).
Assume we don't have access to smart pointers, so I'll discard option 3 for now.
With option 1, whoever created the collaborators will have to exist for the duration of the lifetime of the object that depends on them. If the creator (let's say it's a Factory) gets destroyed while the Dependent object still exists, the Dependent will no longer have a valid handle to the collaborators.
With option 2, the Factory will have to trust that the Dependent object will properly dispose of the collaborators.
Additionally, C++ allows for stack-allocated and heap-allocated objects. We have to take that into account.
[ B ] RAII
Languages like C++ handle resource management using the
RAII idiom. It stands for
Resource Acquisition is Initialization. Essentially, we release all acquired resources in the destructor of an object, ensuring that when the object is destroyed, it closes open files, connections, etc.
[ C ] A Solution
I decided to go with a variation on option 2. Let the collaborators be passed in to the Dependent. The Dependent object will then store copies of the collaborators and ensure that they're tied to its own scope. In this way, an object can be passed out of the scope of the factory and still maintain valid handles to all its collaborators.
Basically, the Dependent object owns its collaborators and ensures they get destroyed.
Cost: we'll be making copies of things.
[ D ] Example
Consider an
Eater class that
eats something edible.
class Eater
{
public:
explicit Eater(const IEdible& edible);
~Eater();
void consumeEdible();
private:
IEdible* myEdible;
};
Where
IEdible is the following interface, it's implemented by many foods, such as
Apple:
class IEdible
{
public:
virtual ~IEdible() { }
virtual IEdible* clone() const = 0;
virtual void eat() const = 0;
private:
IEdible& operator=(const IEdible&);
};
I want to be able to write the following code:
int main (int argc, char **argv)
{
Apple myApple;
Eater myEater(myApple);
myEater.consumeEdible();
return 0;
}
Here, the Factory is my
main function. It creates the collaborator
myApple and passes it to the Eater. From that point on,
myEater owns a copy of
myApple that is independent of the original copy. In this way, it is not bound to the lifetime of the original.
Here's how this is accomplished:
Eater::Eater(const IEdible& edible)
: myEdible(edible.clone())
{ }
Eater::~Eater()
{
delete myEdible;
}
The key to all this is the
clone function. It is an implementation of the
Virtual Copy Constructor Idiom. It's defined in the
IEdible interface, so each edible food must have a
clone function. This function will simply return a copy of the edible food. Here's the implementation for
Apple:
Apple* Apple::clone() const
{
return new Apple(*this);
}
This is just a call to
Apple's copy constructor, which handles the heavy lifting.
[ E ] Thoughts
For me this mostly solves the problem. The
Eater depends on an interface, so different edible objects can be substituted into it. The interface defines the mechanism through which ownership is granted to the object. Using
RAII, the object manages the lifetime of its collaborators and destroys them deterministically (when it gets destroyed). This is all transparent from the outside, as demonstrated in
main.
Please forgive me for the wall of text. Let me know if this is useful. I'd like to read questions and comments. This was just the result of a bit of experimenting and I'm interested in knowing how it can be improved, or if you have some other cool solution.