Opaque objects: an interface alternative

 

"your class should be an omnipotent monolith"

2012 by Arnaud Sintès

 

Keywords: architecture, c++, interface, multiple-inheritance, template specialization

 

As an introduction note, let’s thanks Nicolas Combe for the specialized template interfaces concept described in this paper, as long as he is the one who pops out the idea first.

 

 

 



Sometimes, when dealing with complex architectures, you will have to consider a “big object” class…

You know, the one you always want to avoid because it’s doing a lot of complex things in itself and has to handle unexpected strong parent/child dependencies at the same time.

 

Imagine for example a computation class:

- created (owned) by a parent one

- with a genuine public interfaced access

- with multiple child dependencies and upward communication (e.g. sub-computations)

 

 

 

In some situation you can split the big object into smaller ones, but adding some more internal dependencies and indirections may not always be the best solution (and you can expect some performance drawback).

 

So, sometimes you have to do that “big object”, and as long as you’re a smart developer always doing defensive programming (J) you don’t want it to become a public open-bar and you want to avoid the use of any friend statement for this situation (C++ is not  facebook, kids).

 

The best architecture approach to solve this problem seems to be a smart partitioning of the big class to make some parts opaque to some other components.

 

A good solution to hide the inner parts of a class will be the use of some opaque pointers and deal only with the public class to handle the stuff, but this is a pure “C” solution and it solves only half the problem as long as you cannot make any difference between the parent owning, the external public access and the child access.

 

Object languages offer a classic pattern to indirect the access of an object: the interface.

You already know all of these; make your class inherit of a public interface class and overrides/seals the public methods in protected/private to make the code itself opaque to the user.

 

It’s a really nice and pure C++ solution, which offers some smart and clear interfaces to the user (and nothing else) as well as an opaque implementation of your class code.

 

So, let’s start from this interface partitioning approach and extend it with some other basic C++ concepts to solve the problem and get our “opaque objects”.

 

 

For the record, please note that the __interface keyword used in the following snippets:

// object interface

__interface IObject {

        void Dummy();

};

 

will just simplify the code by being equivalent to something like:

// object interface

class IObject abstract {

public:

        virtual void Dummy() abstract;

 

protected:

        IObject() {}

        virtual ~IObject() {}

};

 

in a Microsoft™/Visual Studio™ specific environment, the rest of the code is fully C++03 compliant.

 

 

First of all, we have to consider that our object will have three distinct interfaced accesses:

-          a parent access, to handle construction/destruction and maybe some other parent stuff for descending communication

-          a public access, to let external objects interact with the object

-          child accesses, to handle upward communication from child to the object

 

So, the cleverest move now will be to already write down all of these interfaces as long as the visible methods remain the functional parts of your architecture.

 

Because there will be different interfaces on the same object, the template keyword may have pop somewhere in your mind at this point, for good reasons.

The use of specialized template interfaces is really a smart position when dealing with multiple interfaces:

-          you avoid yourself the “how the fuck can I name this interface” part -> it’s now trivial as long as it will be a template interface specialized for the concerned object

-          you make your code cleaner and fully partitioned

-          you lock the use of your interface for a given object -> someone else will always be able to use a specialized interface where it should not (or worse: dynamic_cast the object), but everyone will know about the trick just by quickly look at the code syntax (i.e. if an object “A” use the interface of “B” to access the object, there’s obviously something wrong)

 

Basically, if we consider the three interfaced accesses described above, we will get:

-          IObject< ParentObject > to be the parent access interface

-          IObject<> to be the public interface (not specialized, by defaulting the template to “void” which became implicit when not specified)

-          IObject< AChildObject > to be a child access interface

 

IObject is obviously the interface of the Object class.

 

This can be transcribed in C++ the following way :

 

“iobject.h” file

#pragma once

 

// ----------

// forwards

class ParentObject;

class Child1Object;

class Child2Object;

 

 

// ----------

// object interface template

template< class T = void > __interface IObject {};

 

 

// ----------

// object public interface declaration

template<> __interface IObject<> {

        void PublicMethod();

};

 

 

// ----------

// shared pointer of an object interface relative to its parent

typedef QSharedPointer< IObject< ParentObject > > IObjectPPtr;

 

 

// ----------

// object parent interface declaration

template<> __interface IObject< ParentObject > {

        // note: no "operator IObject<> & () const;" possible in an interface declaration

        IObject<> & AsPublic() const;

        void ParentMethod();

};

 

 

// ----------

// object child 1 interface declaration

template<> __interface IObject< Child1Object > {

        void Child1Method();

};

 

 

// ----------

// object child 2 interface declaration

template<> __interface IObject< Child2Object > {

        void Child2Method();

};

 

 

We all know that we will have to reuse this in the future, so let’s systematize and simplify it a bit with some macros.

 

“opaque_interfaces.h” generic file

#pragma once

 

// declare a public interface

#define decl_public_interface( _IOBJ_ )\

        template< class T = void > __interface _IOBJ_ {};\

        template<> __interface _IOBJ_<>

 

// declare a child interface

#define decl_child_interface( _IOBJ_, _CHILD_ )\

        class _CHILD_;\

        template<> __interface _IOBJ_< _CHILD_ >

 

// declare a parent interface

#define decl_parent_interface( _IOBJ_, _PARENT_ )\

        class _PARENT_;\

        typedef QSharedPointer< _IOBJ_< _PARENT_ > > _IOBJ_##PPtr;\

        template<> __interface _IOBJ_< _PARENT_ >

 

 

Final “iobject.h” file

#pragma once

#include "opaque_interfaces.h"

 

// ----------

// object public interface declaration

decl_public_interface( IObject ) {

        void PublicMethod();

};

       

// ----------

// object parent interface declaration

decl_parent_interface( IObject, ParentObject ) {

        // note: no "operator IObject<> & () const;" possible in an interface declaration

        IObject<> & AsPublic() const;

        void ParentMethod();

};

 

// ----------

// object child 1 interface declaration

decl_child_interface( IObject, Child1Object ) {

        void Child1Method();

};

 

// ----------

// object child 2 interface declaration

decl_child_interface( IObject, Child2Object ) {

        void Child2Method();

};

 

Notes:

-          in this example, only the parent will be able to give a public access interface to our object (through the AsPublic() method)

-          the public access may have to be done through a cast operator (from IObject< ParentObject > to an IObject<> interface), but this is only possible with a classic abstract class declaration and not with the __interface keywords which implicitly forbids any ctor/dtor/operator declaration (and I’m pretty sure the C++0x standard will do the same with a native interface keyword)

-          an implicit smart pointer to a parent interface casted object was provided to avoid the use of pointers / explicit destruction of object by the owner

 

 

So we can now sums up our architecture the following way:

 

To write the object itself, what we just have to consider is that the class will be fully opaque (only private methods) with all accesses performed through specialized interfaces.

So we have to make our class inherits from all specialized interfaces and overrides/seals the method in partitioned areas.

 

To be more nazi, the object itself will not be able to be constructed or destructed explicitly but will be created indirectly through a static creator which returns a smart pointer on the object (so the destruction will be implicit) casted into a specialized interface for its owner (so no one except the designed parent will handle new objects).

 

“object.h”

#pragma once

#include "iobject.h"

#include "child1.h"

#include "child2.h"

       

// ----------

// object

class Object sealed

        : public IObject<>

        , public IObject< ParentObject >

        , public IObject< Child1Object >

        , public IObject< Child2Object >

{

public: // public static methods

        static IObjectPPtr CreateInstance();

 

private: // ctor/dtor

        Object();

 

private: // IObject< ParentObject >

        IObject<> & AsPublic() const sealed;

        void ParentMethod() sealed;

 

private: // IObject<>

        void PublicMethod() sealed;

 

private: // IObject< Child1Object >

        void Child1Method() sealed;

 

private: // IObject< Child2Object >

        void Child2Method() sealed;

 

private: // private members

        Child1Object m_child1;

        Child2Object m_child2;

};

 

“object.cpp” file

#include "object.h"

 

 

// static instance creator

IObjectPPtr Object::CreateInstance() {

 

        // return a smart pointer on a new object

        return IObjectPPtr( new Object() );

}

 

 

// ctor

Object::Object()

        : m_child1( *this )

        , m_child2( *this )

{}

 

 

// public interface getter

IObject<> & Object::AsPublic() const {

 

        // IObject< ParentObject > & {override} Object & {upcast} IObject<> &

        return const_cast< Object & >( *this );

}

 

 

// dummy method to be called by parent

void Object::ParentMethod() {}

 

 

// dummy method to be called through public interface

void Object::PublicMethod() {}

 

 

// dummy method to be called by child 1

void Object::Child1Method() {}

 

 

// dummy method to be called by child 2

void Object::Child2Method() {}

 

Note that the specialized interfaces are given to the child during their construction by an implicit cast of the current instance, as well as the public interface (except for the const casting).

 

Now, if we consider one of the object’s children, the upward communication became trivial as long as the concerned child keeps a reference on its parent through a dedicated interface (which can easily be given on the construction).

 

“child1.h” file

#pragma once

#include "iobject.h"

 

// ----------

// child 1 object

class Child1Object sealed

{

public: // ctor/dtor

        Child1Object( IObject< Child1Object > & _parent );

 

private: // private methods

        void _Dummy();

 

private: // private members

        IObject< Child1Object > & m_parent;

};

 

 

“child1.cpp” file

#include "child1.h"

 

 

// ctor

Child1Object::Child1Object( IObject< Child1Object > & _parent )

        : m_parent( _parent )

{}

 

 

// dummy method

void Child1Object::_Dummy() {

 

        // just call a parent's method

        m_parent.Child1Method();

}

 

Of course, another child will work exactly on the same model, just by dealing with its specialized interface.

 

“child2.h” file

...

class Child2Object sealed

{

public: // ctor/dtor

        Child2Object( IObject< Child2Object > & _parent );

...

private: // private members

        IObject< Child2Object > & m_parent;

};

 

 

“child2.cpp” file

...

Child2Object::Child2Object( IObject< Child2Object > & _parent )

        : m_parent( _parent )

 

...

void Child2Object::_Dummy() {

        m_parent.Child2Method();

}

 

On the same model, as long as the parent can only register the object through the static creator, it’s only possible to call object’s methods through a smart pointer and then again through its dedicated specialized interface.

 

“parent.h” file

#pragma once

#include "iobject.h"

 

// ----------

// parent object

class ParentObject sealed

{

public: // ctor/dtor

        ParentObject();

 

public: // public methods

        IObject<> & GetObj() const;

 

private: // private methods

        void _Dummy();

 

private: // public members

        IObjectPPtr m_obj;

};

 

 

“parent.cpp” file

#include "parent.h"

 

 

// ctor

ParentObject::ParentObject()

        : m_obj( Object::CreateInstance() )

{}

 

 

// get object with a public interface visibility

// note: then, someone will be able to call "parent.GetObj().PublicMethod();"

IObject<> & ParentObject::GetObj() const {

 

        // convert object as public

        return m_obj->AsPublic();

}

 

 

// dummy method

void ParentObject::_Dummy() {

 

        // just call object's method

        m_obj->ParentMethod();

}

 


That’s all folks, hope this help!