Tuesday, February 08, 2005

Creating Abstractions

To properly write testable code, you need to be able to test your objects in isolation. In practice, this means that your objects must only depend on abstractions that can be easily stubbed out.

Wagging the Dog

How do you know what to pull out into an abstract interface? Let's look at an example.
class Dog
{
void bark();
void setOwner( PetOwner* o );
PetOwner* getOwner();
}
You might want to just abstract the whole thing, like this:
struct IDog
{
virtual void bark() = 0;
virtual void setOwner( PetOwner* o ) = 0;
virtual PetOwner* getOwner() = 0;
}
This is often referred to as the pimpl idiom , but it has some problems.

- It presupposes that all clients want to deal with a Dog, forcing clients to write different implementations for cats and parakeets.
- It is harder to mock, requiring you to mock the whole beast rather than the one part that you care about.

We have achieve physical isolation of the Dog code, but have not logically decoupled anything.

Consider Your Clients When Creating Abstractions

When you create an abstraction, remember that you are defining an interface between two units of code. You need to take into account both classes, rather than just one. Sometimes, this can be tricky, but fortunately, TDD helps us to learn what the needs of both classes are.

And we have some rules of thumb to guide us. When we create abstractions, we may want to consider abstracting capabilities and roles.

Capabilities

A capability is an interface that, you guessed it, denotes the ability to do something. By convention, capability class names end in "-able", like ISerializable or Serializable.

By pulling a capability into its own interface, you make it easy for clients to get only what want.
struct IBarkable
{
virtual void bark() = 0;
}
It makes it possible for me to write a re-useable client, that works with anything that barks, and not just dogs.

void makeThemAllBark( std::vector& barkers );

Roles

A role is an interface that has a meaning to a particular audience. The same object may fulfill many different roles.

In our Dog example, having an owner is not a quality which is intrinsic to dogs. You can pull that out and say that this is a quality of a pet.
struct IPet
{
virtual void setOwner( IPetOwner* o ) = 0;
virtual IPetOwner* getOwner() = 0;
}
You can add this interface to any class that needs to support "pet ownership" semantics. You might see an opportunity to create one standard pet implementation which you can mix-in to different animal classes.
class PetImpl
: public IPet
{
virtual void setOwner( IPetOwner* o );
virtual IPetOwner* getOwner();
}
The Abstract Dog

Our final class looks like this:
class Dog
: public IBarkable,
public IPet,
public PetImpl
{
// IBarkable
virtual void bark();

// IPet is implemented by PetImpl
}