Most of Object Oriented guides revolve around the word encapsulation: we should hide implementation details of a class and reduce coupling between objects as much as possible. But in C++ (and also in other compiled languages) we also need to care about the physical side of the design. How can we do that?
Here is a 'common' style of class declaration:
Assume the logic design is good. What are the problems related to physical side of such class?
First thing: what if we add a method in the Engine class? Not only the Car.cpp will have to be compiled again (this is expected of course), but clients of the Car class as well!
Second thing: what if we change/add some private method or a private field of the Car class? That way clients also have to be recompiled.
What is the point? Why is it important? For small projects there is almost no difference between good and bad physical design. But when a project is large the need for recompilation means long (very long and even longer) building time.
Do clients of a class need to be recompiled every time? Do they need to know its internal guts - private fields and methods?
Additionally, although clients can assume that the (public) interface of a class will not change, it is not the same as with the compiler. It depends on all information in header files, so even if a private implementation of a module is changed, than every dependent module is needed to be compiled again.
The tool that could help us in improving the physical side the design is called insulation, see below the result of applying that.
Isn't that a bit better?
Or even:
Can you see the difference? Will it compile/build faster?
This article is a result of skimming (and some reading) of the book called "Large Scale C++ Software Design", by John Lakos. This is an advanced C++ book, but touches a unique topic in the area: how to design code not only from a logical perspective but from the physical side as well.
Sometimes the writing style is a bit 'academic' and hard to understand quickly. But all in all it is a great source for everyone involved in creating something bigger than, let's say, 10,000 lines of code.
Main flaw: it is a bit old: from 1996, and it even uses Booch diagrams... I wish there is a second, newer edition. But still a lot of theories are so general that even now we can use them.
So what is this insulation? Let us see what author says about that:
What can we do to reduce this physical coupling then?
When we declare an argument in a method: void func(A a, B b); the compiler has to know the details of A and B. So the simplest way is to include A.h and B.h at the top of our header. But actually we need such details only in the implementation of func, it is not needed in (pre)declaration.
It is better to use const pointers or const references for such arguments (of course for built-in types there is no such need). When the compiler sees const A *a it has to know only the name, not the full definition of a particular type.
As with methods' parameters, we can use a similar technique regarding class members. It can be easily seen in our Car class - the second version. This time we need only predeclarations of particular classes.
Unfortunately this comes at a price: we need to allocate those members somewhere on the heap in the implementation file (in constructor for instance). For large members it is even better, but for small classes (for instance like Vector3) it can be too tricky.
One of the most radical methods is to use the pImpl pattern. It can be seen in the third version of the Car class. Everything private goes to a class named pImpl that is declared and implemented in the cpp file. That way the client of Car does not see its guts, and we do not have any additional includes and physical coupling.
When we see a class and its private: block, we can assume how it works. It is better to diminish this block and move every method to an implementation file. How? It is not that easy unfortunately.
We can create a static free function in cpp file, but that way we have no access to private members of the class. It is pointless to create a static member function of the class because it is almost the same as the original method (we need to declare this in header file as well).
Sometimes it is good to go through all private methods and check if they really should be in this class? Maybe they should be removed or moved to some helper class, or even made a free function?
Insulation is not a silver bullet for everything. Is it good to insulate a small Point2D class? Probably not! We would cause more harm actually. Below there is a list of issues to consider before doing full, hardcore insulation:
It is just a tip of an ice berg! As I mentioned earlier I did not cover include topic and some other stuff like: enums, protocol class, handles and opaque pointers. But I hope that this gives you motivation to look at your code from a slightly different perspective.
Reposted with permission from Bartłomiej Filipek's blog
logo comes from: openclipart
Example
Here is a 'common' style of class declaration:
#include "Engine.h" #include "Gearbox.h" #include "..." #include "Wheels.h" class Car { private: Engine m_engine; Gearbox m_gearbox; Wheels m_wheels; public: Car() { } virtual ~Car() { } void drive() { } private: void checkInternals(); }
Assume the logic design is good. What are the problems related to physical side of such class?
First thing: what if we add a method in the Engine class? Not only the Car.cpp will have to be compiled again (this is expected of course), but clients of the Car class as well!
Second thing: what if we change/add some private method or a private field of the Car class? That way clients also have to be recompiled.
What is the point? Why is it important? For small projects there is almost no difference between good and bad physical design. But when a project is large the need for recompilation means long (very long and even longer) building time.
Do clients of a class need to be recompiled every time? Do they need to know its internal guts - private fields and methods?
Additionally, although clients can assume that the (public) interface of a class will not change, it is not the same as with the compiler. It depends on all information in header files, so even if a private implementation of a module is changed, than every dependent module is needed to be compiled again.
The tool that could help us in improving the physical side the design is called insulation, see below the result of applying that.
Isn't that a bit better?
class Engine; class Gearbox; class Wheels; class Car { private: std::unique_ptr<Engine> m_engine; std::unique_ptr<Gearbox> m_gearbox; std::unique_ptr<Wheels> m_wheels; public: Car(); virtual ~Car(); void drive(); private: void checkInternals(); }
Or even:
class Car { private: std::unique_ptr<class CarImpl> m_pImpl; public: Car(); virtual ~Car(); void drive(); // private methods 'moved' to source file... }
Can you see the difference? Will it compile/build faster?
About the book
This article is a result of skimming (and some reading) of the book called "Large Scale C++ Software Design", by John Lakos. This is an advanced C++ book, but touches a unique topic in the area: how to design code not only from a logical perspective but from the physical side as well.
Sometimes the writing style is a bit 'academic' and hard to understand quickly. But all in all it is a great source for everyone involved in creating something bigger than, let's say, 10,000 lines of code.
Main flaw: it is a bit old: from 1996, and it even uses Booch diagrams... I wish there is a second, newer edition. But still a lot of theories are so general that even now we can use them.
Insulation definition
So what is this insulation? Let us see what author says about that:
A contained implementation detail (type, data, or function) that can be altered, added or removed without forcing clients to recompile is said to be insulated.
What can we do to reduce this physical coupling then?
Arguments
When we declare an argument in a method: void func(A a, B b); the compiler has to know the details of A and B. So the simplest way is to include A.h and B.h at the top of our header. But actually we need such details only in the implementation of func, it is not needed in (pre)declaration.
It is better to use const pointers or const references for such arguments (of course for built-in types there is no such need). When the compiler sees const A *a it has to know only the name, not the full definition of a particular type.
Members
As with methods' parameters, we can use a similar technique regarding class members. It can be easily seen in our Car class - the second version. This time we need only predeclarations of particular classes.
Unfortunately this comes at a price: we need to allocate those members somewhere on the heap in the implementation file (in constructor for instance). For large members it is even better, but for small classes (for instance like Vector3) it can be too tricky.
pImpl
One of the most radical methods is to use the pImpl pattern. It can be seen in the third version of the Car class. Everything private goes to a class named pImpl that is declared and implemented in the cpp file. That way the client of Car does not see its guts, and we do not have any additional includes and physical coupling.
Private methods
Keep private stuff private.
When we see a class and its private: block, we can assume how it works. It is better to diminish this block and move every method to an implementation file. How? It is not that easy unfortunately.
We can create a static free function in cpp file, but that way we have no access to private members of the class. It is pointless to create a static member function of the class because it is almost the same as the original method (we need to declare this in header file as well).
Sometimes it is good to go through all private methods and check if they really should be in this class? Maybe they should be removed or moved to some helper class, or even made a free function?
Problems
Insulation is not a silver bullet for everything. Is it good to insulate a small Point2D class? Probably not! We would cause more harm actually. Below there is a list of issues to consider before doing full, hardcore insulation:
- Lower performance: when doing insulation lots of variables are placed on the heap, for small pieces of data it can be harmful.
- It is not that easy to fully insulate a class. We need time (and skills) to do that.
- Maybe our class is not used that often, maybe there will be no difference in compilation time whatsoever?
Sum up
It is just a tip of an ice berg! As I mentioned earlier I did not cover include topic and some other stuff like: enums, protocol class, handles and opaque pointers. But I hope that this gives you motivation to look at your code from a slightly different perspective.
Reposted with permission from Bartłomiej Filipek's blog
logo comes from: openclipart