A blog about in-depth analysis and understanding of programming, with a focus on C++.

Tuesday, December 04, 2007

Interface my Code

Once upon a time, I talked about Pimpl. Basically, its a way of hiding the implementation details from external code. The primary benefit is improved compile times, since header files do not have to include a bunch of code that nobody cares about, and only the internals of a system need recompiling when external code changes.

As the primary example for Pimpl, I used what I described as "entity from a game system". The example brings up a specific problem, one that is not unique to games. That is, of the "fat interface" object, the object that needs to know about everything, everywhere.

Here is that example again, pre-Pimpl:

class GameEntity
{
public:
GameEntity();
virtual ~GameEntity();

private:
boost::shared_ptr<Render::MeshClass> m_Mesh;
boost::shared_ptr<Anim::AnimSystem> m_AnimationSystem;
std::vector<boost::shared_ptr<Audio::Sounds> > m_SoundList;
std::vector<boost::shared_ptr<Coll::CollisionMesh> > m_CollList;
boost::shared_ptr<Physics::PhysicalObject> m_PhysicsObj;
};


Now, the Pimpl version does a great job of hiding the internals:

class GameEntityImpl;

class GameEntity
{
public:
GameEntity();
virtual ~GameEntity();

private:
boost::shared_ptr m_Impl;
};


Now, the only #include that the external system needs to do is with boost/shared_ptr. That's all well and good, but there's something missing... the interface. GameEntity isn't terribly useful without its interface. And while we no longer expose numerous classes from the internals, the interface would force us to do so:

class GameEntityImpl;

class GameEntity
{
public:
GameEntity();
virtual ~GameEntity();

void EnumerateCollisionVolumes(boost::function<void(boost::shared_ptr<const Physics::CollisionVolume>)>) const;
void EnumerateMeshes(boost::function<void(boost::shared_ptr<const Render::Mesh>)>) const;
boost::shared_ptr<Ai::AiController> GetAI();
boost::shared_ptr<const Ai::AiController> GetAI() const;
void PlayAnimation(boost::shared_ptr<const Anim::Animation> pTheAnim);

void SetPosition(const Position &thePos);
void SetOrientation(const Orientation &theOrientation);

private:
boost::shared_ptr<GameEntityImpl> m_Impl;
};


This is really no better than before. Oh, it still gets rid of certain implementation details, like the use of std::vector and such. But that's all; the user still needs the definition of a lot of classes.

Also, there is the problem of having fat interfaces. What I depicted is the tip of the iceberg; it is not uncommon for the core entity classes to have hundreds of member functions.

There is a way to solve all of these problems at once. Basically, you use your Pimpl differently.

Pimpl has a publicly available interface class, but also a private implementation class. The solution is to, instead of having one public interface class, have many. One for different uses of the class.

So, in this case, GameEntity objects would be created through some global function interface. The code that creates a GameEntity needs to know about all the different bits, so you can't avoid that one. But what is created is the private implementation, which is stored by boost::shared_ptr in a list accessible by a unique name. It would also create the primary public interface class, called GameEntity, which is returned to the user.

GameEntity would have a number of functions that would return different interfaces. So, there would be a physics interface, rendering interface, sound interface, etc. These all expose functions and classes that much of the game code can freely ignore. All of them have shared_ptrs back to the private implementation.

So, your GameEntity looks like:

class GameEntityImpl;
class PhysicsEntity;
class RenderEntity;
class AiEntity;
class AnimEntity;

class GameEntity
{
public:
virtual ~GameEntity();

//Interface retrieval
boost::shared_ptr<PhysicsEntity> GetPhysicsEntity();
boost::shared_ptr<const PhysicsEntity> GetPhysicsEntity() const;
boost::shared_ptr<RenderEntity> GetRenderEntity();
boost::shared_ptr<const RenderEntity> GetRenderEntity() const;
boost::shared_ptr<AiEntity> GetAiEntity();
boost::shared_ptr<const AiEntity> GetAiEntity() const;
boost::shared_ptr<AnimEntity> GetAnimEntity();
boost::shared_ptr<const AnimEntity> GetAnimEntity() const;

std::string GetEntityName() const;

void SetPosition(const Position &thePos);
void SetOrientation(const Orientation &theOrientation);

private:
GameEntity();

boost::shared_ptr m_Impl;

This works perfectly. It hides everything from prying eyes, while creating interfaces for specific kinds of tasks. Because all of the interfaces point to the same object, they can call any internal code they wish. So the generic GameEntity interface can have expose functions like SetPosition which operate on the physics object (the ultimate source of the position) without having to go to the PhysicsEntity to do so. The PhysicsEntity will expose it of course, but it will also expose other functions.

Also, if the necessary objects for a certain kind of interface don't exist for a particular GameEntity, then the interface retrieval function will simply return NULL. It forces the user to check if the GameEntity, for example, has an AI before calling AI function.

I call this "Interface-based Programming". The data is perfectly hidden behind specific views into that data, with each view being categorized for specific functions and purposes.

1 comment:

Morna said...

Good words.