Preface
Have a problem with extensive use of pointers? Totally abhor macros? Do you find the following way of interface declaration totally despicable?
ABSTRACT_CLASS(IDynamicCtrl)
ABSTRACT_METHOD(void, Draw, CDC*);
ABSTRACT_METHOD(BOOL, Edit);
ABSTRACT_METHOD(BOOL, IsEditing);
ABSTRACT_METHOD(void, EndEditing);
ABSTRACT_METHOD(void, SetCtrlType, int);
ABSTRACT_METHOD(void, SetDisplayInfo, LPCTSTR pszInfo);
ABSTRACT_METHOD(BOOL, AddData, LPARAM);
ABSTRACT_METHOD(void, ResetData);
ABSTRACT_METHOD(void, UpdateGUI);
END_ABSTRACT_CLASS()
Then don't bother :). This article is not for you. Otherwise, please carry on..
Introduction
Changing a comment in the header makes you recompile an entire project, containing nearly 1 million lines (maybe a little exaggeration) and taking almost half of your working hours - if you ever had to put up with this situation, then come onboard, let's share some ideas on how to get rid of such a disturbing effect on the mind.
Here recompilation is an indication of how tightly or loosely your classes are coupled; It gives some insight about how flexible your design is and how easily you can accommodate changes in your modules - such an addition of an extra class or deletion of an existing class.
As the GOF has described in design patterns, the more you make your classes loosely coupled, the easier it is for you to accommodate changes (one example of loose coupling can be, instead of declaring a member variable directly in the header, keep a pointer and instantiate in the CPP).
Whether you do without loose coupling or tight coupling, one thing you cannot live without in the modern world is interface, or in other words implementation of interface. We all know how we do it in C++, we'll have an abstract
class containing pure virtual methods and we implement those methods in the derived classes. This means, even a single line change in the header of an abstract
class (interface), even if it is a comment ends up compiling a lot of classes derived from it. Although I am talking about interfaces, sometimes people who are programming in C++ make their life a bit easier by declaring some member variables in the abstract
classes which are supposed to be interfaces. I cannot comment whether it's good or bad, but sometimes due to short deadlines, we have to do it (giving up to the dreaded multiple inheritance - the horror in modern programming paradigm). So, in these cases even if a member variable name is changed, or new members are added, you still end up compiling a lot of classes which inherited from these abstract
classes.
Those of us, who are not running a super computer, need a solution for this in big projects. Let me again re-iterate: recompilation problem solving is not the main objective here, the main objective is to achieve loose coupling and very light level of dependencies among classes.
So How Do We Plan to Meet Our Objective?
First of all, let me tell you - even I, myself, wouldn't be using these techniques I mention here, for now; at least not in my professional projects yet. Maybe, after 100 years, when(?) this article gets a noble prize or something similar for its concept, then some of you people might start using the techniques I describe here (given C++ remains the ultimate programming language even then).
As the gurus of the design world always say, aggregation is a better concept than inheritance -but what about interface implementation which is also done through multiple inheritance in C++. This is where I came up with this idea of applying the concept of aggregation in interface implementation.
Here the main concept is the user will have an interface aggregated inside the aggregator, but invoking of the interface method will end up calling the method of the aggregator class.
Speaking more visually, instead of declaring something like:
class ConcreteClassA : public AbstractClassA
{
public:
virtual void CallFunction1();
};
We'd declare:
class ConcreateClassA
{
public:
AbstractClassA* m_pAbstractClassAPointer;
void CallFunction1();
};
You can imagine what AbstractClassA
looks like, right?
class AbstractClassA
{
public:
virtual void CallFunction1() = 0 ; };
And somehow through a pointer of the AbstractClassA
, we'd invoke the ConcreteClassA
method CallClassFunction1()
;
Quite amazing, don't you think?. We will call the overridden virtual function of the interface AbstractClassA
by a pointer of AbstractClassA
type only, but that too, without inheritance. So, how?
Yes!! You guessed right. Through function pointers; to be more precise: method pointers. Now, AbstractClassA
which used to look like this:
class AbstractClassA
{
public:
virtual void CallFunction1() = 0 ; };
will now look like what you see below:
(To quote a famous lyricist Bob Dylan : "It used to be like that, now it goes like this..")
class AbstractClassA
{
public:
typedef void (AbstractClassA::*__CallFunction1)();
__CallFunction1 CallFunction1;
};
Ok, now we managed to have the method pointer inside AbstractClassA
but how to invoke the actual method of ConcreteClassA
through it? And also, you need an object to invoke any method pointer. And this object cannot be any object, this has to be the instance of ConcreteClassA
which holds AbstractClassA
inside it.
So, the solution I came up with was - I would make sure my AbstractClassA
is properly initialized, i.e., its function pointer has a valid value and it contains the pointer to the Concrete
class in void
pointer form. Unethical? This is nothing compared to instantiating the abstract AbstractClassA
in the constructor of ConcreteClassA
. But we are here for a noble cause, isn't it? And that is to implement interface without inheritance; so we have to play dirty a little. Just tell your fellow programmers not to directly instantiate AbstractClassA
anywhere else, ok? It will only be instantiated in the constructor of those concrete classes who treat it as their interface to the outside world.
Going forward with this inventive(!) thinking, this is how we'd create and initialize an instance of AbstractClassA
in the constructor of ConcreteClassA
:
ConcreteClassA::ConcreteClassA()
{
AbstractClassA* q = new AbstractClassA();
if(q)
{
q->SetObj(this);
q->CallFunction1 = (AbstractClassA::__CallFunction1)(&ConcreteClassA::CallFunction1);
m_pAbstractClassAPointer = q;
}
....
}
Here the main initialization involves the assigning of the function, i.e., concrete class's method to the abstract
class's method pointer and the setting of a reference of the concrete class in void
pointer form inside the abstract
class.
To make provision for the void
pointer which is actually the address of the concrete class, we have to add methods like SetObj
, GetObj
and a relevant member variable in AbstractClassA
making the complete picture of our abstract
class looking similar to what follows:
class AbstractClassA
{
public:
typedef void (AbstractClassA::*__CallFunction1)();
__CallFunction1 CallFunction1;
void SetObj(void* pObj){m_pObj = pObj;}
AbstractClassA* GetObj(){ return m_pObj; }
protected:
void* m_pObj;
};
And this is how we'd invoke the actual method CallFunction1
of ConcreteClassA
if we have a pointer to AbstractClassA
named pAbstractClassA
:
( (pAbstractClassA->GetObj())->*AbstractClassA->CallFunction1 )();
Notice the clever typecasting of the void
pointer to AbstractClassA
* through GetObj()
. This allows us to pass the this
pointer of ConcreteClassA
to the CallFunction1
method through AbstractClassA
's method pointer. It's not a problem that we have typecasted the concrete class pointer to a completely different data type named AbstractClassA
; It is due to the fact that a method pointer only requires a valid address of a proper object to be passed (as this
pointer) when the method gets invoked irrespective of its type. We are casting the void
pointer to AbstractClassA*
only to get access to the function pointer inside it.
Since the code to invoke the method looks a bit clumsy, I came up with some useful macros for it. Also, to declare an abstract
class which will always have some common methods such as GetObj()
, SetObj
- I came up with some more macros.
How It All Looks With the Advent of Macros
Before I start giving an example (with macros) of an interface implemented through aggregation techniques, let me talk about my interface and the implementor of the interface a little.
The name of my interface here is IDynamicCtrl
. The implementor class is CDynamicCtrl
which looks and acts like an edit box by default, but it's outlook and behaviour changes to a combo box when its interface method SetCtrlType
is invoked. In short, IDynamicCtrl
is an interface of a GUI control which can change dynamically its type in runtime.
Following is the structure of my interface IDynamicCtrl
with macros:
ABSTRACT_CLASS(IDynamicCtrl)
ABSTRACT_METHOD(void, Draw, CDC*);
ABSTRACT_METHOD(BOOL, Edit);
ABSTRACT_METHOD(BOOL, IsEditing);
ABSTRACT_METHOD(void, EndEditing);
ABSTRACT_METHOD(void, SetCtrlType, int);
ABSTRACT_METHOD(void, SetDisplayInfo, LPCTSTR pszInfo);
ABSTRACT_METHOD(BOOL, AddData, LPARAM);
ABSTRACT_METHOD(void, ResetData);
ABSTRACT_METHOD(void, UpdateGUI);
END_ABSTRACT_CLASS()
I have taken all the intricacies of the interface declaration and its method pointer declarations inside the macros named ABSTRACT_CLASS
, END_ABSTRACT_CLASS
and ABSTRACT_METHOD
.
So, to me it looks a little better and more readable with macros.
This is how the implementor class CDynamicCtrl
aggregates the interface IDynamicCtrl
:
class CDynamicCtrl : public CStatic
{
DECLARE_DYNAMIC(CDynamicCtrl)
ABSTRACT_CLASS_AGGREGATION_SUPPORT(CDynamicCtrl)
DECLARE_ABSTRACT_CLASS(IDynamicCtrl)
public:
CDynamicCtrl();
virtual ~CDynamicCtrl();
protected:
DECLARE_MESSAGE_MAP()
afx_msg void OnChildKillFocus();
void Draw(CDC* pDC);
BOOL Edit();
BOOL IsEditing();
void EndEditing();
void DrawCombo(CDC* pDC);
BOOL EditCombo();
void SetCtrlType(int nType);
void SetDisplayInfo(LPCTSTR pszInfo);
BOOL AddData(LPARAM lp);
void ResetData();
void UpdateGUI();
CStringArray m_arrStrings;
BOOL m_bFocused;
public:
afx_msg void OnPaint();
afx_msg void OnLButtonDblClk(UINT nFlags, CPoint point);
afx_msg void OnKeyUp(UINT nChar, UINT nRepCnt, UINT nFlags);
afx_msg void OnSetFocus(CWnd* pOldWnd);
afx_msg void OnKillFocus(CWnd* pNewWnd);
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
};
The macros required for defining the structure of an interface and its aggregation in concrete classes are declared in "abstract_class_decl.h" header of the uploaded source code.
Now, let's have a look at initialization of the interface class IDynamicCtrl
and the mapping of concrete class CDynamicCtrl
functions with the abstract
class's function pointers (Since we are dealing with C++, I am using the terms interface class and abstract
class interchangeably):
IMPLEMENT_ABSTRACT_CLASS(CDynamicCtrl, IDynamicCtrl)
CDynamicCtrl::CDynamicCtrl()
{
BEGIN_ABSTRACT_METHOD_MAP(CDynamicCtrl, IDynamicCtrl)
ABSTRACT_METHOD_MAP(Draw)
ABSTRACT_METHOD_MAP(Edit)
ABSTRACT_METHOD_MAP(IsEditing)
ABSTRACT_METHOD_MAP(EndEditing)
ABSTRACT_METHOD_MAP(SetCtrlType)
ABSTRACT_METHOD_MAP(SetDisplayInfo)
ABSTRACT_METHOD_MAP(AddData)
ABSTRACT_METHOD_MAP(ResetData)
ABSTRACT_METHOD_MAP(UpdateGUI)
END_ABSTRACT_METHOD_MAP()
...
}
The macros related to the implementation (instantiation and method mapping) of the abstract
class in concrete classes is declared in "abstract_class_impl.h" header file of the uploaded source code.
And finally and most importantly, let's have a look at the macro related to the invoking of a CDynamicCtrl
method through IDynamicCtrl
interface (see if it suits you, because that's the most readable version of invoking a concrete class method I could come up with):
IDynamicCtrl* pCtrl = m_pInfoDisplayer;
if(nViewType == 1)
{
if(pCtrl)
{
(CONCRETE(pCtrl)->SetCtrlType)(nViewType);
}
}
If (CONCRETE
(pCtrl
)->SetCtrlType
)(nViewType
) does not suit you, you can use INVOKE
(pCtrl
, SetCtrlType
, nViewType
) macro. Both CONCRETE
and INVOKE
macro is defined in "abstract_class_impl.h"
header file.
Background Behind the Aberration
The whole thing about implementing interface through aggregation rather than inheritance came to picture, because I work in a big project which has a lot of interface implementation through inheritance in several modules and sometimes writing comments on the header files ends up compiling the whole project. While thinking about the recompilation problem, the thought of removing one of the tightest form of coupling (bearing the name interfaces) came to mind. The article is an end result of my contemplative thinking upon inheritance and interfacing and whether it would be a nice thing to have in the C++ language itself if it really supported interfacing through aggregated classes.
Practically speaking, there were certain benefits I found when I ended up writing a sample application for this article.
Constructive Benefits from this Design
First and foremost, this concept reduces appreciably the possible ambiguity factor which might be introduced due to multiple inheritance. Like I mentioned before, working as C++ programmers under tight deadlines, you end up using the abstract
class concept (which has variables) more than the interface concept (interface shouldn't have a single non static
member variable). Over time, this increases ambiguity because of similar method and variable names in some of the parent classes. This technique here encourages the usage of composition of multiple objects rather than multiple inheritance and in return solves the problem related to ambiguity completely.
The other benefit is, you can change dynamically the run time behaviour of certain interface methods. For example, to give a combo look and combo behaviour to the CDynamicCtrl
when the control type changes, I just remap the Draw
and Edit
method pointers of IDynamicCtrl
to the DrawCombo
and EditCombo
methods of CDynamicCtrl
, achieving greater flexibility and manageability in the entire process. If you'd have used inheritance instead, probably you'd end up creating another class to encapsulate different behaviours and use the correct class to display proper behaviour given the control type following the footsteps of strategy pattern; or in the worst case, you'd be using if else
conditions increasing the complexity of the code - when you know very well, one of the reasons design patterns and modern techniques were introduced, were to minimize the usage of if else
conditions and spaghetti codes.
The downside of this design is the way I implemented the macros, it's not possible to support polymorphic features such as function overloading as of now. However, writing the macros more cleverly and using an array of function pointers can solve this problem, but that is beyond the scope of this article.
The horror of this design, as some people may point out, is the use of void
pointer, complete disregard for type safety, and breaking several ethics of OOP design; but all the hideous things mentioned above are hidden underneath the carpet of macros and the discretion of the user of macros is requested so that he may not have a peek at what's inside :).
However, like I pointed out before, if C++ supported something of this sort, the theory in practice could have been real elegant. We could ponder upon dynamic binding of an interface to a concrete class in runtime even. I know, most people are not wary of adding extra classes to encapsulate behaviour for runtime behaviour changing, but in my view if we could reduce some overheads in some cases, then why not?
During runtime, if I could write codes such as:
ISortable& sortableData = _bind(CDataTable, ISortable) { _connect(Sort, bSortByID ? SortByID : SortByName)
}
And then invoke:
Sort(sortableData)
where The Sort
function takes a reference of the ISortable
interface - maybe this dynamic binding of an interface (ISortable)
with a concrete class (CDataTable)
could have been really useful in some cases.
A Decade Later (Updated on Dec 20, 2020)
With the cool C++17 features it is now possible to achieve the holy grail of decoupling, using std::any and lamdas.
The famous shape example of C++, where you inherit rect and circle from shape can now be modified to decouple shape from rect and circle thusly:
Decoupling
Acknowledgements
- Kaisar Ul Haque: Who helped me in many ways to understand the complex things about method pointers, the this pointer and virtual tables through his blogs.
History
- 6th October, 2010: Article uploaded
- 20th December, 2020: Article updated