Insulation
Insulation is the process of avoiding or removing unnecessary compile-time coupling.
Insulation is a physical design issue whereas encapsulation refers to logical design.
If a component can be modified without forcing clients to recompile is said to be insulated.
The following sections list techniques to minimize excessive compile-time dependencies:
Remove Private Inheritance
Private Inheritance exposes some but not all functions in private base class to clients of derived class.
// base.h
#ifndef _INCLUDED_BASE
#define _INCLUDED_BASE
class Base
{
public:
Base() {}
~Base() {}
void f1(int);
int f1() const;
};
#endif//_INCLUDED_BASE
|
// myclass.h
#ifndef _INCLUDED_MYCLASS
#define _INCLUDED_MYCLASS
#ifndef _INCLUDED_BASE
#include "base.h"
#endif//_INCLUDED_BASE
class MyClass : private Base
{
public:
MyClass() {}
Base::f1; // access declaration
};
#endif//_INCLUDED_MYCLASS
|
However, the same logical interface can be achieved without exposing clients to details of the class.
// myclass.h
#ifndef _INCLUDED_MYCLASS
#define _INCLUDED_MYCLASS
class Base;
class MyClass
{
public:
MyClass();
MyClass(const MyClass& c);
~MyClass();
MyClass& operator=(const MyClass& c);
void f1(int i);
int f1() const;
private:
Base* p_base;
};
#endif//_INCLUDED_MYCLASS
|
Instead of privately deriving from Base, an implementation now holds an opaque pointer to Base;
New member functions of MyClass are defined (out-of-line) to forward calls to functions in Base.
// my_class.c
#include "myclass.h"
#include "base.h"
MyClass::MyClass() : p_base(new Base) {}
MyClass::MyClass(const MyClass& c) : p_base(new Base(*c.p_base)) {}
MyClass::~MyClass()
{
delete p_base;
}
MyClass& MyClass::operator=(const MyClass& c)
{
if (this != &c)
{
delete p_base;
p_base = new Base(*c.p_base);
}
return *this;
}
void MyClass::f1(int i)
{
p_base->f1(i);
}
int MyClass::f1() const
{
return p_base->f1();
}
|
Remove Embedded Data Members
Insulate clients from an individual implementation class:
- Convert all embedded instances of that implementation class to pointers (or references)
- Manage those pointers explicitly in constructors, destructors and assignment operators
That is, convert HasA relationship to HoldsA to improve insulation.
Here, compare before insulating clients of class to after insulation.
// myclass_before.h
#ifndef _INCLUDED_MYCLASS_BEFORE
#define _INCLUDED_MYCLASS_BEFORE
#ifndef _INCLUDED_YOURCLASS
#include "yourclass.h"
#endif//_INCLUDED_YOURCLASS
class MyClass
{
public:
MyClass();
~MyClass();
int getValue() const;
int getCount() const
{
return count;
}
private:
YourClass yours;
int count;
};
#endif//_INCLUDED_MYCLASS_BEFORE
// my_class_before.c
#include "myclass_before.h"
MyClass::MyClass()
{
}
MyClass::~MyClass()
{
}
int MyClass::getValue() const
{
return yours.getValue();
}
|
// myclass_after.h
#ifndef _INCLUDED_MYCLASS_AFTER
#define _INCLUDED_MYCLASS_AFTER
class YourClass;
class MyClass
{
public:
MyClass();
~MyClass();
int getValue() const;
int getCount() const
{
return count;
}
private:
YourClass* p_yours;
int count;
};
#endif//_INCLUDED_MYCLASS_AFTER
// my_class_after.c
#include "myclass_after.h"
#include "yourclass.h"
MyClass::MyClass()
{
p_yours = new YourClass;
}
MyClass::~MyClass()
{
delete p_yours;
}
int MyClass::getValue() const
{
return p_yours->getValue();
}
|
Remove Private Member Functions
Private member functions, although encapsulated, are part of the component's physical interface.
Instead of making functions private, make them static free (non-member) declared at file scope.
Compare an original class, modified with only private static functions then with static free functions:
// myclass_before.h
#ifndef _INCLUDED_MYCLASS_BEFORE
#define _INCLUDED_MYCLASS_BEFORE
class MyClass
{
public:
int getValue() const;
private:
int value() const;
};
#endif//_INCLUDED_MYCLASS_BEFORE
|
Step One: convert each private member function to a private static member:
// myclass_after1.h
#ifndef _INCLUDED_MYCLASS_AFTER1
#define _INCLUDED_MYCLASS_AFTER1
class MyClass
{
public:
int getValue() const;
private:
static int value(const MyClass&);
};
#endif//_INCLUDED_MYCLASS_AFTER1
// myclass_after1.c
#include "myclass_after1.h"
int MyClass::getValue() const
{
return value(*this);
}
int MyClass::value(const MyClass& myclass)
{
return 0;
}
|
Step Two: remove function declaration from header file and member notation from definition file:
// myclass_after2.h
#ifndef _INCLUDED_MYCLASS_AFTER2
#define _INCLUDED_MYCLASS_AFTER2
class MyClass
{
public:
int getValue() const;
};
#endif//_INCLUDED_MYCLASS_AFTER2
// myclass_after2.c
#include "myclass_after2.h"
int MyClass::getValue() const
{
return value(*this);
}
static int value(const MyClass& myClass)
{
return 0;
}
|
Remove Private Member Data
Removing private static data is easy for inline member functions that do not require direct access:
Move static member data into a static variable defined at file scope in the component's .c file.
Compare original class with private static member data and modified class with static file-scope data:
// myclass_before.h
#ifndef _INCLUDED_MYCLASS_BEFORE
#define _INCLUDED_MYCLASS_BEFORE
class MyClass
{
public:
private:
static int s_count;
};
#endif//_INCLUDED_MYCLASS_BEFORE
// my_class_before.c
#include "myclass_before.h"
int MyClass::s_count;
|
// myclass_after.h
#ifndef _INCLUDED_MYCLASS_AFTER
#define _INCLUDED_MYCLASS_AFTER
class MyClass
{
public:
private:
};
#endif//_INCLUDED_MYCLASS_AFTER
// my_class_after.c
#include "myclass_after.h"
static int s_count;
|
Remove Compiler-Generated Functions
The compiler will auto-generate the following functions if not found: constructor, copy constructor, assignment operator and/or destructor. However, truly insulating class must define these explicitly.
Remove Include Directives
Unnecessary #include directives can cause compile-time coupling where none would otherwise exist: Move all unnecessary #include directives from a header file to .c file with forward class declarations.
// bank.h
class USD; // class declaration instead of #include
class CAD; // class declaration instead of #include
class NZD; // class declaration instead of #include
class Bank
{
public:
USD getUSD() const;
CAD getCAD() const;
NZD getNZD() const;
};
|
An alternative is to place redundant guards around each #include directive in every header file.
Although unpleasant, this may significantly reduce compile times in much larger C++ projects.
// bank.h
#ifndef _INCLUDED_BANK
#define _INCLUDED_BANK
#ifndef _INCLUDED_USD
#include "usd.h"
#endif//_INCLUDED_USD
#ifndef _INCLUDED_CAD
#include "cad.h"
#endif//_INCLUDED_CAD
#ifndef _INCLUDED_NZD
#include "nzd.h"
#endif//_INCLUDED_NZD
class Bank {};
#endif//_INCLUDED_BANK
|
Remove Enumerations
Judicious use of enumerations, typedefs, + other constructs with internal linkage achieve good insulation: Move enumerations and typedefs to .c file and replace them as private static const member of the class.
Summary
Generally if a component is used widely throughout the system, its interface should be insulated.
However, insulated interfaces are not always practical, e.g. lightweight, reusuable components.
Techniques
| Remove Private Inheritance | Convert WasA relationship to HoldsA |
| Remove Embedded Data Members | Convert HasA relationship to HoldsA |
| Remove Private Member Functions | Make them static at file scope and move into .c file |
| Remove Private Member Data | Extract a protocol and/or move static data to .c file |
| Remove Compiler Functions | Explicitly define these functions |
| Remove Include Directives | Remove include directives or replace with class declarations |
| Remove Enumerations | Relocate to .c file and replace as const static class member data |
Conclusion
In conclusion, two important techniques can be used to mitigate cyclic dependencies in C++: Levelization can reduce link-time dependencies and Insulation can minimize compile-time dependencies. Awesome J
