CleanCodeDeveloper SOLID
CleanCodeDeveloper SOLID
In Motivational Pictures
https://lostechies.com/derickbailey/2009/02/11/solid-development-principles-in-motivational-pictures/
By Derick Bailey
I found the Motivator this morning. It lets you create your own motivational pictures. So,
here’s my first run at creating the SOLID software development principles in motivational
picture form. I ‘borrowed’ the images from google image search results. I hope you find them
to be as fun as I do! I have them all hanging up in my team room, already. :)
SOLID
There’s been a lot of request to re-use these images, so I’m going to release them under a
Creative Commons license.
Single Responsibility Principle
Just because you can, doesn’t mean you should.
Open Closed Principle
Open chest surgery is not needed when putting on a coat.
Liskov Substitution Principle
If it looks like a duck, quacks like a duck, but needs batteries – you probably have the wrong
abstraction
Interface Segregation Principle
You want me to plug this in, where?
Dependency Inversion Principle
Would you solder a lamp directly to the electrical wiring in a wall?
________________________
________________________
9
SRP:
The Single Responsibility Principle
None but Buddha himself must take the responsibility of giving out occult secrets...
— E. Cobham Brewer 1810–1897.
Dictionary of Phrase and Fable. 1898.
This principle was described in the work of Tom DeMarco1 and Meilir Page-Jones2. They
called it cohesion. As we’ll see in Chapter 21, we have a more specific definition of cohe-
sion at the package level. However, at the class level the definition is similar.
Consider the bowling game from Chapter 6. For most of its development the Game class
was handling two separate responsibilities. It was keeping track of the current frame, and
it was calculating the score. In the end, RCM and RSK separated these two responsibilities
into two classes. The Game kept the responsibility to keep track of frames, and the Scorer
got the responsibility to calculate the score. (see page 85.)
1. [DeMarco79], p310
2. [PageJones88], Chapter 6, p82.
149
150
Why was it important to separate these two responsibilities into separate classes?
Because each responsibility is an axis of change. When the requirements change, that
change will be manifest through a change in responsibility amongst the classes. If a class
assumes more than one responsibility, then there will be more than one reason for it to
change.
If a class has more then one responsibility, then the responsibilities become coupled.
Changes to one responsibility may impair or inhibit the class’ ability to meet the others.
This kind of coupling leads to fragile designs that break in unexpected ways when
changed.
For example, consider the design in Figure 9-1. The Rectangle class has two meth-
ods shown. One draws the rectangle on the screen, the other computes the area of the rect-
angle.
Computational Rectangle
Graphical
Geometry
+ draw() Application
Application
+ area() : double
GUI
Figure 9-1
More than one responsibility
Two different applications use the Rectangle class. One application does computa-
tional geometry. It uses Rectangle to help it with the mathematics of geometric shapes.
It never draws the rectangle on the screen. The other application is graphical in nature. It
may also do some computational geometry, but it definitely draws the rectangle on the
screen.
This design violates the SRP. The Rectangle class has two responsibilities. The first
responsibility is to provide a mathematical model of the geometry of a rectangle. The sec-
ond responsibility is to render the rectangle on a graphical user interface.
The violation of SRP causes several nasty problems. Firstly, we must include the GUI
in the computational geometry application. If this were a C++ application, the GUI would
have to be linked in, consuming link time, compile time, and memory footprint. In a Java
application, the .class files for the GUI have to be deployed to the target platform.
Secondly, if a change to the GraphicalApplication causes the Rectangle to
change for some reason, that change may force us to rebuild, retest, and redeploy the
ComputationalGeometryApplication. If we forget to do this, that application may
break in unpredictable ways.
151 Chapter 9: SRP: The Single Responsibility Principle
A better design is to separate the two responsibilities into two completely different
classes as shown in Figure 9-2. This design moves the computational portions of
Rectangle into the GeometricRectangle class. Now changes made to the way rectan-
gles are rendered cannot affect the ComputationalGeometryApplication.
Computational
Graphical
Geometry
Application
Application
Geometric Rectangle
Rectangle
GUI
+ area() : double
+ area() : double
Figure 9-2
Separated Responsibilities
What is a Responsibility?
Listing 9-1
Modem.java -- SRP Violation
interface Modem
{
public void dial(String pno);
public void hangup();
public void send(char c);
public char recv();
}
However, there are two responsibilities being shown here. The first responsibility is
connection management. The second is data communication. The dial and hangup func-
tions manage the connection of the modem, while the send and recv functions communi-
cate data.
Should these two responsibilities be separated? Almost certainly they should. The
two sets of functions have almost nothing in common. They’ll certainly change for differ-
ent reasons. Moreover, they will be called from completely different parts of the applica-
tions that use them. Those different parts will change for different reasons as well.
152
Therefore the design in Figure 9-3 is probably better. It separates the two responsibil-
ities into two separate interfaces3. This, at least, keeps the client applications from cou-
pling the two responsibilities.
«interface» «interface»
Data
Connection
Channel
Modem
Implementation
Figure 9-3
Separated Modem Interface
However, notice that I have recoupled the two responsibilities into a single
ModemImplementation class. This is not desirable, but it may be necessary. There are
often reasons, having to do with the details of the hardware or OS, that force us to couple
things that we’d rather not couple. However, by separating their interfaces we have decou-
pled the concepts as far as the rest of the application is concerned.
We may view the ModemImplementation class is a kludge, or a wart; however,
notice that all dependencies flow away from it. Nobody need depend upon this class.
Nobody except main needs to know that it exists. Thus, we’ve put the ugly bit behind a
fence. It’s ugliness need not leak out and pollute the rest of the application.
Conclusion
The SRP is one of the simplest of the principle, and one of the hardest to get right. Con-
joining responsibilities is something that we do naturally. Finding and separating those
responsibilities from one another is much of what software design is really about. Indeed,
the rest of the principles we will discuss come back to this issue in one way or another.
3. We’ll see more of this in Chapter 13, when we study the Interface Segregation Principle (ISP).
153 Chapter 9: SRP: The Single Responsibility Principle
Bibliography
[PageJones88]: The Practical Guide to Structured Systems Design, 2d. ed., Meilir Page-
Jones, Yourdon Press Computing Series, 1988
154
The Open-Closed Principle
This is the first of my Engineering Notebook columns for The C++ Report. The articles
that will appear in this column will focus on the use of C++ and OOD, and will address
issues of software engineering. I will strive for articles that are pragmatic and directly use-
ful to the software engineer in the trenches. In these articles I will make use of Booch’s
notation for documenting object oriented designs. The sidebar provides a brief lexicon of
Booch’s notation.
There are many heuristics associated with object oriented design. For example, “all
member variables should be private”, or “global variables should be avoided”, or “using
run time type identification (RTTI) is dangerous”. What is the source of these heuristics?
What makes them true? Are they always true? This column investigates the design princi-
ple that underlies these heuristics -- the open-closed principle.
As Ivar Jacobson said: “All systems change during their life cycles. This must be
borne in mind when developing systems expected to last longer than the first version.”1
How can we create designs that are stable in the face of change and that will last longer
than the first version? Bertrand Meyer2 gave us guidance as long ago as 1988 when he
coined the now famous open-closed principle. To paraphrase him:
Description
Modules that conform to the open-closed principle have two primary attributes.
1. Object Oriented Software Engineering a Use Case Driven Approach, Ivar Jacobson, Addison
Wesley, 1992, p 21.
2. Object Oriented Software Construction, Bertrand Meyer, Prentice Hall, 1988, p 23
1
Description 2
In C++, using the principles of object oriented design, it is possible to create abstractions
that are fixed and yet represent an unbounded group of possible behaviors. The abstrac-
tions are abstract base classes, and the unbounded group of possible behaviors is repre-
sented by all the possible derivative classes. It is possible for a module to manipulate an
abstraction. Such a module can be closed for modification since it depends upon an
abstraction that is fixed. Yet the behavior of that module can be extended by creating new
derivatives of the abstraction.
Figure 1 shows a simple design that does not conform to the open-closed principle.
Both the Client and Server classes are concrete. There is no guarantee that the mem-
ber functions of the Server class are virtual. The Client class uses the Server class.
If we wish for a Client object to use a different server object, then the Client class
must be changed to name the new server class.
Client Server
Figure 1
Closed Client
Figure 2 shows the corresponding design that conforms to the open-closed principle.
In this case, the AbstractServer class is an abstract class with pure-virtual member
functions. the Client class uses this abstraction. However objects of the Client class
will be using objects of the derivative Server class. If we want Client objects to use a
different server class, then a new derivative of the AbstractServer class can be cre-
ated. The Client class can remain unchanged.
3 : The Open-Closed Principle
Abstract
Client
Server
Server
Figure 2
Open Client
Listing 1
Procedural Solution to the Square/Circle Problem
enum ShapeType {circle, square};
struct Shape
{
ShapeType itsType;
};
struct Circle
{
ShapeType itsType;
double itsRadius;
Point itsCenter;
};
The Shape Abstraction 4
Listing 1 (Continued)
Procedural Solution to the Square/Circle Problem
struct Square
{
ShapeType itsType;
double itsSide;
Point itsTopLeft;
};
//
// These functions are implemented elsewhere
//
void DrawSquare(struct Square*)
void DrawCircle(struct Circle*);
case circle:
DrawCircle((struct Circle*)s);
break;
}
}
}
The function DrawAllShapes does not conform to the open-closed principle
because it cannot be closed against new kinds of shapes. If I wanted to extend this function
to be able to draw a list of shapes that included triangles, I would have to modify the func-
tion. In fact, I would have to modify the function for any new type of shape that I needed
to draw.
Of course this program is only a simple example. In real life the switch statement in
the DrawAllShapes function would be repeated over and over again in various func-
tions all over the application; each one doing something a little different. Adding a new
shape to such an application means hunting for every place that such switch statements
(or if/else chains) exist, and adding the new shape to each. Moreover, it is very
unlikely that all the switch statements and if/else chains would be as nicely struc-
tured as the one in DrawAllShapes. It is much more likely that the predicates of the if
5 : The Open-Closed Principle
statements would be combined with logical operators, or that the case clauses of the
switch statements would be combined so as to “simplify” the local decision making.
Thus the problem of finding and understanding all the places where the new shape needs
to be added can be non-trivial.
Listing 2 shows the code for a solution to the square/circle problem that conforms to
the open-closed principle. In this case an abstract Shape class is created. This abstract
class has a single pure-virtual function called Draw. Both Circle and Square are
derivatives of the Shape class.
Listing 2
OOD solution to Square/Circle problem.
class Shape
{
public:
virtual void Draw() const = 0;
};
Strategic Closure
It should be clear that no significant program can be 100% closed. For example, consider
what would happen to the DrawAllShapes function from Listing 2 if we decided that
all Circles should be drawn before any Squares. The DrawAllShapes function is
not closed against a change like this. In general, no matter how “closed” a module is, there
will always be some kind of change against which it is not closed.
Since closure cannot be complete, it must be strategic. That is, the designer must
choose the kinds of changes against which to close his design. This takes a certain amount
of prescience derived from experience. The experienced designer knows the users and the
industry well enough to judge the probability of different kinds of changes. He then makes
sure that the open-closed principle is invoked for the most probable changes.
How could we close the DrawAllShapes function against changes in the ordering
of drawing? Remember that closure is based upon abstraction. Thus, in order to close
DrawAllShapes against ordering, we need some kind of “ordering abstraction”. The
specific case of ordering above had to do with drawing certain types of shapes before other
types of shapes.
An ordering policy implies that, given any two objects, it is possible to discover which
ought to be drawn first. Thus, we can define a method of Shape named Precedes that
takes another Shape as an argument and returns a bool result. The result is true if the
Shape object that receives the message should be ordered before the Shape object
passed as the argument.
In C++ this function could be represented by an overloaded operator< function.
Listing 3 shows what the Shape class might look like with the ordering methods in place.
Now that we have a way to determine the relative ordering of two Shape objects, we
can sort them and then draw them in order. Listing 4 shows the C++ code that does this.
This code uses the Set, OrderedSet and Iterator classes from the Components
category developed in my book3 (if you would like a free copy of the source code of the
Components category, send email to [email protected]).
This gives us a means for ordering Shape objects, and for drawing them in the appro-
priate order. But we still do not have a decent ordering abstraction. As it stands, the indi-
vidual Shape objects will have to override the Precedes method in order to specify
ordering. How would this work? What kind of code would we write in Circle::Pre-
cedes to ensure that Circles were drawn before Squares? Consider Listing 5.
3. Designing Object Oriented C++ Applications using the Booch Method, Robert C. Martin, Pren-
tice Hall, 1995.
7 : The Open-Closed Principle
Listing 3
Shape with ordering methods.
class Shape
{
public:
virtual void Draw() const = 0;
virtual bool Precedes(const Shape&) const = 0;
Listing 4
DrawAllShapes with Ordering
void DrawAllShapes(Set<Shape*>& list)
{
// copy elements into OrderedSet and then sort.
OrderedSet<Shape*> orderedList = list;
orderedList.Sort();
Listing 5
Ordering a Circle
bool Circle::Precedes(const Shape& s) const
{
if (dynamic_cast<Square*>(s))
return true;
else
return false;
}
It should be very clear that this function does not conform to the open-closed princi-
ple. There is no way to close it against new derivatives of Shape. Every time a new deriv-
ative of Shape is created, this function will need to be changed.
Closure of the derivatives of Shape can be achieved by using a table driven approach
that does not force changes in every derived class. Listing 6 shows one possibility.
By taking this approach we have successfully closed the DrawAllShapes function
against ordering issues in general and each of the Shape derivatives against the creation
of new Shape derivatives or a change in policy that reorders the Shape objects by their
type. (e.g. Changing the ordering so that Squares are drawn first.)
Strategic Closure 8
Listing 6
Table driven type ordering mechanism
#include <typeinfo.h>
#include <string.h>
enum {false, true};
typedef int bool;
class Shape
{
public:
virtual void Draw() const = 0;
virtual bool Precedes(const Shape&) const;
char* Shape::typeOrderTable[] =
{
“Circle”,
“Square”,
0
};
Listing 6 (Continued)
Table driven type ordering mechanism
if ((argOrd > 0) && (thisOrd > 0))
done = true;
}
else // table entry == 0
done = true;
}
return thisOrd < argOrd;
}
The only item that is not closed against the order of the various Shapes is the table
itself. And that table can be placed in its own module, separate from all the other modules,
so that changes to it do not affect any of the other modules.
This isn’t the end of the story. We have managed to close the Shape hierarchy, and
the DrawAllShapes function against ordering that is dependent upon the type of the
shape. However, the Shape derivatives are not closed against ordering policies that have
nothing to do with shape types. It seems likely that we will want to order the drawing of
shapes according to some higher level structure. A complete exploration of these issues is
beyond the scope of this article; however the ambitious reader might consider how to
address this issue using an abstract OrderedObject class contained by the class
OrderedShape, which is derived from both Shape and OrderedObject.
This is one of the most commonly held of all the conventions of OOD. Member variables
of classes should be known only to the methods of the class that defines them. Member
variables should never be known to any other class, including derived classes. Thus they
should be declared private, rather than public or protected.
In light of the open-closed principle, the reason for this convention ought to be clear.
When the member variables of a class change, every function that depends upon those
variables must be changed. Thus, no function that depends upon a variable can be closed
with respect to that variable.
Heuristics and Conventions 10
In OOD, we expect that the methods of a class are not closed to changes in the mem-
ber variables of that class. However we do expect that any other class, including sub-
classes are closed against changes to those variables. We have a name for this expectation,
we call it: encapsulation.
Now, what if you had a member variable that you knew would never change? Is there
any reason to make it private? For example, Listing 7 shows a class Device that has a
bool status variable. This variable contains the status of the last operation. If that
operation succeeded, then status will be true; otherwise it will be false.
Listing 7
non-const public variable
class Device
{
public:
bool status;
};
We know that the type or meaning of this variable is never going to change. So why
not make it public and let client code simply examine its contents? If this variable really
never changes, and if all other clients obey the rules and only query the contents of sta-
tus, then the fact that the variable is public does no harm at all. However, consider
what happens if even one client takes advantage of the writable nature of status, and
changes its value. Suddenly, this one client could affect every other client of Device.
This means that it is impossible to close any client of Device against changes to this one
misbehaving module. This is probably far too big a risk to take.
On the other hand, suppose we have the Time class as shown in Listing 8. What is the
harm done by the public member variables in this class? Certainly they are very unlikely
to change. Moreover, it does not matter if any of the client modules make changes to the
variables, the variables are supposed to be changed by clients. It is also very unlikely that
a derived class might want to trap the setting of a particular member variable. So is any
harm done?
Listing 8
class Time
{
public:
int hours, minutes, seconds;
Time& operator-=(int seconds);
Time& operator+=(int seconds);
bool operator< (const Time&);
bool operator> (const Time&);
bool operator==(const Time&);
bool operator!=(const Time&);
};
11 : The Open-Closed Principle
One complaint I could make about Listing 8 is that the modification of the time is not
atomic. That is, a client can change the minutes variable without changing the hours
variable. This may result in inconsistent values for a Time object. I would prefer it if there
were a single function to set the time that took three arguments, thus making the setting of
the time atomic. But this is a very weak argument.
It would not be hard to think of other conditions for which the public nature of
these variables causes some problems. In the long run, however, there is no overriding rea-
son to make these variables private. I still consider it bad style to make them public,
but it is probably not bad design. I consider it bad style because it is very cheap to create
the appropriate inline member functions; and the cheap cost is almost certainly worth the
protection against the slight risk that issues of closure will crop up.
Thus, in those rare cases where the open-closed principle is not violated, the proscrip-
tion of public and protected variables depends more upon style than on substance.
The argument against global variables is similar to the argument against pubic member
variables. No module that depends upon a global variable can be closed against any other
module that might write to that variable. Any module that uses the variable in a way that
the other modules don’t expect, will break those other modules. It is too risky to have
many modules be subject to the whim of one badly behaved one.
On the other hand, in cases where a global variable has very few dependents, or can-
not be used in an inconsistent way, they do little harm. The designer must assess how
much closure is sacrificed to a global and determine if the convenience offered by the glo-
bal is worth the cost.
Again, there are issues of style that come into play. The alternatives to using globals
are usually very inexpensive. In those cases it is bad style to use a technique that risks even
a tiny amount of closure over one that does not carry such a risk. However, there are cases
where the convenience of a global is significant. The global variables cout and cin are
common examples. In such cases, if the open-closed principle is not violated, then the
convenience may be worth the style violation.
RTTI is Dangerous.
Listing 9
RTTI violating the open-closed principle.
class Shape {};
Listing 10
RTTI that does not violate the open-closed Principle.
class Shape
{
public:
virtual void Draw() cont = 0;
};
Listing 10 (Continued)
RTTI that does not violate the open-closed Principle.
{
for (Iterator<Shape*>i(ss); i; i++)
{
Square* s = dynamic_cast<Square*>(*i);
if (s)
s->Draw();
}
}
ever, nothing changes in Listing 10 when a new derivative of Shape is created. Thus,
Listing 10 does not violate the open-closed principle.
As a general rule of thumb, if a use of RTTI does not violate the open-closed princi-
ple, it is safe.
Conclusion
There is much more that could be said about the open-closed principle. In many ways this
principle is at the heart of object oriented design. Conformance to this principle is what
yeilds the greatest benefits claimed for object oriented technology; i.e. reusability and
maintainability. Yet conformance to this principle is not achieved simply by using an
object oriented programming language. Rather, it requires a dedication on the part of the
designer to apply abstraction to those parts of the program that the designer feels are going
to be subject to change.
This article is an extremely condensed version of a chapter from my new book: The
Principles and Patterns of OOD, to be published soon by Prentice Hall. In subsequent
articles we will explore many of the other principles of object oriented design. We will
also study various design patterns, and their strenghts and weaknesses with regard to
implementation in C++. We will study the role of Booch’s class categories in C++, and
their applicability as C++ namespaces. We will define what “cohesion” and “coupling”
mean in an object oriented design, and we will develop metrics for measuring the quality
of an object oriented design. And, after that, many other interesting topics.
A A
B B
Contains - by value. This indicates that A Uses. This indicates that the name of class
and B have identical lifetimes. When A B is used within the source code of class A.
is destroyed, B will be destroyed too.
class A
class A {
{ public:
private: void F(const B&);
B itsB; };
};
A B
B
D
Contains - by reference. This is used to
indicate that A and B have dissimilar Inheritance. This indicates that B is a public
lifetimes. B may outlive A. base class of D.
class A class D : public B {...};
{
private:
B* itsB;
}; A
P 0..n
A V
B
M B
*
Set
B
Using a container class. In this case a
template for a "Set".
Invoking a member function.
class A
void A::F(B& theB)
{
{
private:
P p;
Set<B*> itsBs;
V v = theB.M(p);
};
}
14
The Liskov Substitution Principle
This is the second of my Engineering Notebook columns for The C++ Report. The articles
that will appear in this column will focus on the use of C++ and OOD, and will address
issues of software engineer-
ing. I will strive for articles Sidebar: Unified Notation 0.8
that are pragmatic and
directly useful to the soft- Had by
Reference
ware engineer in the
Used Base Class
trenches. In these articles I
will make use of Booch’s
and Rumbaugh’s new uni- Had By Value
Introduction
My last column (Jan, 96) talked about the Open-Closed principle. This principle is the
foundation for building code that is maintainable and reusable. It states that well designed
code can be extended without modification; that in a well designed program new features
are added by adding new code, rather than by changing old, already working, code.
The primary mechanisms behind the Open-Closed principle are abstraction and poly-
morphism. In statically typed languages like C++, one of the key mechanisms that sup-
ports abstraction and polymorphism is inheritance. It is by using inheritance that we can
create derived classes that conform to the abstract polymorphic interfaces defined by pure
virtual functions in abstract base classes.
What are the design rules that govern this particular use of inheritance? What are the
characteristics of the best inheritance hierarchies? What are the traps that will cause us to
create hierarchies that do not conform to the Open-Closed principle? These are the ques-
tions that this article will address.
1
The Liskov Substitution Principle 2
The above is a paraphrase of the Liskov Substitution Principle (LSP). Barbara Liskov first
wrote it as follows nearly 8 years ago1:
The importance of this principle becomes obvious when you consider the conse-
quences of violating it. If there is a function which does not conform to the LSP, then that
function uses a pointer or reference to a base class, but must know about all the derivatives
of that base class. Such a function violates the Open-Closed principle because it must be
modified whenever a new derivative of the base class is created.
One of the most glaring violations of this principle is the use of C++ Run-Time Type
Information (RTTI) to select a function based upon the type of an object. i.e.:
void DrawShape(const Shape& s)
{
if (typeid(s) == typeid(Square))
DrawSquare(static_cast<Square&>(s));
else if (typeid(s) == typeid(Circle))
DrawCircle(static_cast<Circle&>(s));
}
[Note: static_cast is one of the new cast operators. In this example it works
exactly like a regular cast. i.e. DrawSquare((Square&)s);. However the new syn-
tax has more stringent rules that make is safer to use, and is easier to locate with tools such
as grep. It is therefore preferred.]
Clearly the DrawShape function is badly formed. It must know about every possible
derivative of the Shape class, and it must be changed whenever new derivatives of
Shape are created. Indeed, many view the structure of this function as anathema to
Object Oriented Design.
1. Barbara Liskov, “Data Abstraction and Hierarchy,” SIGPLAN Notices, 23,5 (May, 1988).
3 : The Liskov Substitution Principle
However, there are other, far more subtle, ways of violating the LSP. Consider an
application which uses the Rectangle class as described below:
class Rectangle
{
public:
void SetWidth(double w) {itsWidth=w;}
void SetHeight(double h) {itsHeight=w;}
double GetHeight() const {return itsHeight;}
double GetWidth() const {return itsWidth;}
private:
double itsWidth;
double itsHeight;
};
Imagine that this application works well, and is installed in
many sites. As is the case with all successful software, as its Figure 1.
users’ needs change, new functions are needed. Imagine that
one day the users demand the ability to manipulate squares in Rectangle
addition to rectangles.
It is often said that, in C++, inheritance is the ISA relation-
ship. In other words, if a new kind of object can be said to fulfill
the ISA relationship with an old kind of object, then the class of
the new object should be derived from the class of the old
object.
Clearly, a square is a rectangle for all normal intents and Square
purposes. Since the ISA relationship holds, it is logical to
model the Square class as being derived from Rectangle.
(See Figure 1.)
This use of the ISA relationship is considered by many to be one of the fundamental
techniques of Object Oriented Analysis. A square is a rectangle, and so the Square class
should be derived from the Rectangle class. However this kind of thinking can lead to
some subtle, yet significant, problems. Generally these problem are not foreseen until we
actually try to code the application.
Our first clue might be the fact that a Square does not need both itsHeight and
itsWidth member variables. Yet it will inherit them anyway. Clearly this is wasteful.
Moreover, if we are going to create hundreds of thousands of Square objects (e.g. a
CAD/CAE program in which every pin of every component of a complex circuit is drawn
as a square), this waste could be extremely significant.
However, let’s assume that we are not very concerned with memory efficiency. Are
there other problems? Indeed! Square will inherit the SetWidth and SetHeight
functions. These functions are utterly inappropriate for a Square, since the width and
height of a square are identical.”. This should be a significant clue that there is a problem
The Liskov Substitution Principle 4
with the design. However, there is a way to sidestep the problem. We could override Set-
Width and SetHeight as follows:
void Square::SetWidth(double w)
{
Rectangle::SetWidth(w);
Rectangle::SetHeight(w);
}
void Square::SetHeight(double h)
{
Rectangle::SetHeight(h);
Rectangle::SetWidth(h);
}
Now, when someone sets the width of a Square object, its height will change corre-
spondingly. And when someone sets its height, the width will change with it. Thus, the
invariants of the Square remain intact. The Square object will remain a mathematically
proper square.
Square s;
s.SetWidth(1); // Fortunately sets the height to 1 too.
s,SetHeight(2); // sets width and heigt to 2, good thing.
But consider the following function:
void f(Rectangle& r)
{
r.SetWidth(32); // calls Rectangle::SetWidth
}
If we pass a reference to a Square object into this function, the Square object will
be corrupted because the height won’t be changed. This is a clear violation of LSP. The f
function does not work for derivatives of its arguments. The reason for the failure is that
SetWidth and SetHeight were not declared virtual in Rectangle.
We can fix this easily. However, when the creation of a derived class causes us to
make changes to the base class, it often implies that the design is faulty. Indeed, it violates
the Open-Closed principle. We might counter this with argument that forgetting to make
SetWidth and SetHeight virtual was the real design flaw, and we are just fixing it
now. However, this is hard to justify since setting the height and width of a rectangle are
exceedingly primitive operations. By what reasoning would we make them virtual if
we did not anticipate the existence of Square.
Still, let’s assume that we accept the argument, and fix the classes. We wind up with
the following code:
class Rectangle
{
public:
virtual void SetWidth(double w) {itsWidth=w;}
virtual void SetHeight(double h) {itsHeight=h;}
double GetHeight() const {return itsHeight;}
double GetWidth() const {return itsWidth;}
5 : The Liskov Substitution Principle
private:
double itsHeight;
double itsWidth;
};
class Square : public Rectangle
{
public:
virtual void SetWidth(double w);
virtual void SetHeight(double h);
};
void Square::SetWidth(double w)
{
Rectangle::SetWidth(w);
Rectangle::SetHeight(w);
}
void Square::SetHeight(double h)
{
Rectangle::SetHeight(h);
Rectangle::SetWidth(h);
}
At this point in time we have two classes, Square and Rectangle, that appear to work.
No matter what you do to a Square object, it will remain consistent with a mathematical
square. And regardless of what you do to a Rectangle object, it will remain a mathe-
matical rectangle. Moreover, you can pass a Square into a function that accepts a pointer
or reference to a Rectangle, and the Square will still act like a square and will remain
consistent.
Thus, we might conclude that the model is now self consistent, and correct. However,
this conclusion would be amiss. A model that is self consistent is not necessarily consis-
tent with all its users! Consider function g below.
void g(Rectangle& r)
{
r.SetWidth(5);
r.SetHeight(4);
assert(r.GetWidth() * r.GetHeight()) == 20);
}
This function invokes the SetWidth and SetHeight members of what it believes
to be a Rectangle. The function works just fine for a Rectangle, but declares an
assertion error if passed a Square. So here is the real problem: Was the programmer who
wrote that function justified in assuming that changing the width of a Rectangle leaves
its height unchanged?
Clearly, the programmer of g made this very reasonable assumption. Passing a
Square to functions whose programmers made this assumption will result in problems.
Therefore, there exist functions that take pointers or references to Rectangle objects,
The Liskov Substitution Principle 6
but cannot operate properly upon Square objects. These functions expose a violation of
the LSP. The addition of the Square derivative of Rectangle has broken these func-
tion; and so the Open-Closed principle has been violated.
This leads us to a very important conclusion. A model, viewed in isolation, can not be
meaningfully validated. The validity of a model can only be expressed in terms of its cli-
ents. For example, when we examined the final version of the Square and Rectangle
classes in isolation, we found that they were self consistent and valid. Yet when we looked
at them from the viewpoint of a programmer who made reasonable assumptions about the
base class, the model broke down.
Thus, when considering whether a particular design is appropriate or not, one must
not simply view the solution in isolation. One must view it in terms of the reasonable
assumptions that will be made by the users of that design.
So what happened? Why did the apparently reasonable model of the Square and Rect-
angle go bad. After all, isn’t a Square a Rectangle? Doesn’t the ISA relationship
hold?
No! A square might be a rectangle, but a Square object is definitely not a Rectan-
gle object. Why? Because the behavior of a Square object is not consistent with the
behavior of a Rectangle object. Behaviorally, a Square is not a Rectangle! And it
is behavior that software is really all about.
The LSP makes clear that in OOD the ISA relationship pertains to behavior. Not
intrinsic private behavior, but extrinsic public behavior; behavior that clients depend upon.
For example, the author of function g above depended on the fact that Rectangles
behave such that their height and width vary independently of one another. That indepen-
dence of the two variables is an extrinsic public behavior that other programmers are likely
to depend upon.
In order for the LSP to hold, and with it the Open-Closed principle, all derivatives
must conform to the behavior that clients expect of the base classes that they use.
Design by Contract
There is a strong relationship between the LSP and the concept of Design by Contract as
expounded by Bertrand Meyer2. Using this scheme, methods of classes declare precondi-
tions and postconditions. The preconditions must be true in order for the method to exe-
cute. Upon completion, the method guarantees that the postcondition will be true.
...when redefining a routine [in a derivative], you may only replace its
precondition by a weaker one, and its postcondition by a stronger one.
In other words, when using an object through its base class interface, the user knows
only the preconditions and postconditions of the base class. Thus, derived objects must not
expect such users to obey preconditions that are stronger then those required by the base
class. That is, they must accept anything that the base class could accept. Also, derived
classes must conform to all the postconditions of the base. That is, their behaviors and out-
puts must not violate any of the constraints established for the base class. Users of the base
class must not be confused by the output of the derived class.
Clearly, the postcondition of Square::SetWidth(double w) is weaker than
the postcondition of Rectangle::SetWidth(double w) above, since it does not
conform to the base class clause “(itsHeight == old.itsHeight)”. Thus,
Square::SetWidth(double w) violates the contract of the base class.
Certain languages, like Eiffel, have direct support for preconditions and postcondi-
tions. You can actually declare them, and have the runtime system verify them for you.
C++ does not have such a feature. Yet, even in C++ we can manually consider the precon-
ditions and postconditions of each method, and make sure that Meyer’s rule is not vio-
lated. Moreover, it can be very helpful to document these preconditions and postconditions
in the comments for each method.
A Real Example.
Figure 2: Container Hierarchy
Motivation
BoundedSet ThirdParty
BoundedSet
third parties. I did not want my application code to be horribly dependent upon these con-
tainers because I felt that I would want to replace them with better classes later. Thus I
wrapped the third party containers in my own abstract interface. (See Figure 2)
I had an abstract class called Set which presented pure virtual Add, Delete, and
IsMember functions.
template <class T>
class Set
{
public:
virtual void Add(const T&) = 0;
virtual void Delete(const T&) = 0;
virtual bool IsMember(const T&) const = 0;
};
This structure unified the Unbounded and Bounded varieties of the two third party
sets and allowed them to be accessed
through a common interface. Thus template <class T>
some client could accept an argument void {
PrintSet(const Set<T>& s)
of type Set<T>& and would not care for (Iterator<T>i(s); i; i++)
whether the actual Set it worked on cout << (*i) << endl;
was of the Bounded or }
Unbounded variety. (See the
PrintSet function listing.)
This ability to neither know nor care the type of Set you are operating on is a big
advantage. It means that the programmer can decide which kind of Set is needed in each
particular instance. None of the client functions will be affected by that decision. The pro-
grammer may choose a BoundedSet when memory is tight and speed is not critical, or
the programmer may choose an UnboundedSet when memory is plentiful and speed is
critical. The client functions will manipulate these objects through the interface of the base
class Set, and will therefore not know or care which kind of Set they are using.
derived from the abstract base class PersistentObject. I created the hierarchy
shown in Figure 3.
On the surface of it, this might look all right. However there is an implication that is
rather ugly. When a client is adding members to the base class Set, how is that client sup-
posed to ensure that it only adds derivatives of PersistentObject if the Set happens
to be a PersistentSet?
Consider the code for PersistentSet::Add:
template <class T>
void PersistentSet::Add(const T& t)
{
PersistentObject& p =
dynamic_cast<PersistentObject&>(t); // throw bad_cast
itsThirdPartyPersistentSet.Add(p);
}
This code makes it clear that if any client tries to add an object that is not derived from
the class PersistentObject to my PersistentSet, a runtime error will ensue.
The dynamic_cast will throw bad_cast (one of the standard exception objects).
None of the existing clients of the abstract base class Set expect exceptions to be thrown
on Add. Since these functions will be confused by a derivative of Set, this change to the
hierarchy violates the LSP.
Is this a problem? Certainly. Functions that never before failed when passed a deriva-
tive of Set, will now cause runtime errors when passed a PersistentSet. Debugging
this kind of problem is relatively difficult since the runtime error occurs very far away
from the actual logic flaw. The logic flaw is either the decision to pass a Persistent-
Set into the failed function, or it is the decision to add an object to the Persistent-
Set that is not derived from PersistentObject. In either case, the actual decision
might be millions of instructions away from the actual invocation of the Add method.
Finding it can be a bear. Fixing it can be worse.
How do we solve this problem? Several years ago, I solved it by convention. Which is
to say that I did not solve it in source code. Rather I instated a convention whereby Per-
sistentSet and PersistentObject were not known to the application as a whole.
They were only known to one particular module. This module was responsible for reading
and writing all the containers. When a container needed to be written, its contents were
copied into PersistentObjects and then added to PersistentSets, which were
then saved on a stream. When a container needed to be read from a stream, the process
was inverted. A PersistentSet was read from the stream, and then the Persisten-
tObjects were removed from the PersistentSet and copied into regular (non-per-
sistent) objects which were then added to a regular Set.
A Real Example. 10
This solution may seem overly restrictive, but it was the only way I could think of to
prevent PersistentSet objects from appearing at the interface of functions that would want
to add non-persistent objects to them. Moreover it broke the dependency of the rest of the
application upon the whole notion of persistence.
Did this solution work? Not really. The convention was violated in several parts of the
application by engineers who did not understand the necessity for it. That is the problem
with conventions, they have to be continually re-sold to each engineer. If the engineer does
not agree, then the convention will be violated. And one violation ruins the whole struc-
ture.
Conclusion
The Open-Closed principle is at the heart of many of the claims made for OOD. It is when
this principle is in effect that applications are more maintainable, reusable and robust. The
Liskov Substitution Principle (A.K.A Design by Contract) is an important feature of all
programs that conform to the Open-Closed principle. It is only when derived types are
completely substitutable for their base types that functions which use those base types can
be reused with impunity, and the derived types can be changed with impunity.
This article is an extremely condensed version of a chapter from my new book: Pat-
terns and Advanced Principles of OOD, to be published soon by Prentice Hall. In subse-
quent articles we will explore many of the other principles of object oriented design. We
will also study various design patterns, and their strengths and weaknesses with regard to
implementation in C++. We will study the role of Booch’s class categories in C++, and
their applicability as C++ namespaces. We will define what “cohesion” and “coupling”
mean in an object oriented design, and we will develop metrics for measuring the quality
of an object oriented design. And, after that, many other interesting topics.
The Interface Segregation
Principle
This is the fourth of my Engineering Notebook columns for The C++ Report. The articles
that appear in this column focus on the use of C++ and OOD, and address issues of soft-
ware engineering. I strive
for articles that are prag- Sidebar: Unified Notation 0.8
matic and directly useful to
the software engineer in the Had by
trenches. In these articles I Reference
Used Base Class
make use of Booch’s and
Rumbaugh’s new unified
Modeling Langage (UML Had By Value
Introduction
In my last column (May 96) I discussed the principle of Dependency Inversion (DIP). This
principle states that modules that encapsulate high level policy should not depend upon
modules that implement details. Rather, both kinds of modules should depend upon
abstractions. To be more succinct and simplistic, abstract classes should not depend upon
concrete classes; concrete classes should depend upon abstract classes. A good example of
this principle is the TEMPLATE METHOD pattern from the GOF1 book. In this pattern, a
high level algorithm is encoded in an abstract base class and makes use of pure virtual
functions to implement its details. Derived classes implement those detailed virtual func-
tions. Thus, the class containing the details depend upon the class containing the abstrac-
tion.
In this article we will examine yet another structural principle: the Interface Segrega-
tion Principle (ISP). This principle deals with the disadvantages of “fat” interfaces.
Classes that have “fat” interfaces are classes whose interfaces are not cohesive. In other
words, the interfaces of the class can be broken up into groups of member functions. Each
group serves a different set of clients. Thus some clients use one group of member func-
tions, and other clients use the other groups.
The ISP acknowledges that there are objects that require non-cohesive interfaces;
however it suggests that clients should not know about them as a single class. Instead, cli-
ents should know about abstract base classes that have cohesive interfaces. Some lan-
guages refer to these abstract base classes as “interfaces”, “protocols” or “signatures”.
In this article we will discuss the disadvantages of “fat” or “polluted” interfacse. We
will show how these interfaces get created, and how to design classes which hide them.
Finally we will present a case study in which the a “fat” interface naturally occurs, and we
will employ the ISP to correct it.
Interface Pollution
Consider a security system. In this system there are Door objects that can be locked and
unlocked, and which know whether they are open or closed. (See Listing 1).
Listing 1
Security Door
class Door
{
public:
virtual void Lock() = 0;
virtual void Unlock() = 0;
virtual bool IsDoorOpen() = 0;
};
This class is abstract so that clients can use objects that conform to the Door interface,
without having to depend upon particular implementations of Door.
Now consider that one such implementation. TimedDoor needs to sound an alarm
when the door has been left open for too long. In order to do this the TimedDoor object
communicates with another object called a Timer. (See Listing 2.)
Listing 2
Timer
class Timer
{
public:
void Regsiter(int timeout, TimerClient* client);
};
class TimerClient
{
public:
virtual void TimeOut() = 0;
};
3 : The Interface Segregation Principle
When an object wishes to be informed about a timeout, it calls the Register function
of the Timer. The arguments of this function are the time of the timeout, and a pointer to a
TimerClient object whose TimeOut function will be called when the timeout expires.
How can we get the TimerClient class to communicate with the TimedDoor class so
that the code in the TimedDoor can be notified of the timeout? There are several alterna-
tives. Figure 1 shows a common solution. We force Door, and therefore TimedDoor, to
inherit from TimerClient. This ensures that TimerClient can register itself with the Timer
and receive the TimeOut message.
Figure 1
TimerClient at top of hierarchy
TimerClient
Door
TimedDoor
Although this solution is common, it is not without problems. Chief among these is
that the Door class now depends upon TimerClient. Not all varieties of Door need timing.
Indeed, the original Door abstraction had nothing whatever to do with timing. If timing-
free derivatives of Door are created, those derivatives will have to provide nil implementa-
tions for the TimeOut method. Moreover, the applications that use those derivatives will
have to #include the definition of the TimerClient class, even though it is not used.
Figure 1 shows a common syndrome of object oriented design in statically typed lan-
guates like C++. This is the syndrome of interface pollution. The interface of Door has
been polluted with an interface that it does not require. It has been forced to incorporate
this interface solely for the benefit of one of its subclasses. If this practice is pursued, then
every time a derivative needs a new interface, that interface will be added to the base class.
This will further pollute the interface of the base class, making it “fat”.
Moreover, each time a new interface is added to the base class, that interface must be
implemented (or allowed to default) in derived classes. Indeed, an associated practice is to
add these interfaces to the base class as nil virtual functions rather than pure virtual func-
tions; specifically so that derived classes are not burdened with the need to implement
them. As we learned in the second article of this column, such a practice violates the
Liskov Substitution Principle (LSP), leading to maintenance and reusability problems.
Interface Pollution 4
Door and TimerClient represent interfaces that are used by complely different clients.
Timer uses TimerClient, and classes that manipulate doors use Door. Since the clients are
separate, the interfaces should remain separate too. Why? Because, as we will see in the
next section, clients exert forces upon their server interfaces.
When we think of forces that cause changes in software, we normally think about how
changes to interfaces will affect their users. For example, we would be concerned about
the changes to all the users of TimerClient, if the TimerClient interface changed. However,
there is a force that operates in the other direction. That is, sometimes it is the user that
forces a change to the interface.
For example, some users of Timer will register more than one timeout request. Con-
sider the TimedDoor. When it detects that the Door has been opened, it sends the Register
message to the Timer, requesting a timeout. However, before that timeout expires the door
closes; remains closed for awhile, and then opens again. This causes us to register a new
timeout request before the old one has expired. Finally, the first timeout request expires
and the TimeOut function of the TimedDoor is invoked. And the Door alarms falsely.
We can correct this situation by using the convention shown in Listing 3. We include a
unique timeOutId code in each timeout registration, and repeat that code in the TimeOut
call to the TimerClient. This allows each derivative of TimerClient to know which timeout
request is being responded to.
Listing 3
Timer with ID
class Timer
{
public:
void Regsiter(int timeout,
int timeOutId,
TimerClient* client);
};
class TimerClient
{
public:
virtual void TimeOut(int timeOutId) = 0;
};
Clearly this change will affect all the users of TimerClient. We accept this since the
lack of the timeOutId is an oversight that needs correction. However, the design in Figure
1 will also cause Door, and all clients of Door to be affected (i.e. at least recompiled) by
this fix! Why should a bug in TimerClient have any affect on clients of Door derivatives
that do not require timing? It is this kind of strange interdependency that chills customers
5 : The Interface Segregation Principle
and managers to the bone. When a change in one part of the program affects other com-
pletely unerlated parts of the program, the cost and repercussions of changes become
unpredictable; and the risk of fallout from the change increases dramatically.
True. But recompiles can be very expensive for a number of reasons. First of all, they take
time. When recompiles take too much time, developers begin to take shortcuts. They may
hack a change in the “wrong” place, rather than engineer a change in the “right” place;
because the “right” place will force a huge recompilation. Secondly, a recompilation
means a new object module. In this day and age of dynamically linked libraries and incre-
mental loaders, generating more object modules than necessary can be a significant disad-
vantage. The more DLLs that are affected by a change, the greater the problem of
distributing and managing the change.
When clients are forced to depend upon interfaces that they don’t use, then those clients
are subject to changes to those interfaces. This results in an inadvertent coupling between
all the clients. Said another way, when a client depends upon a class that contains inter-
faces that the client does not use, but that other clients do use, then that client will be
affected by the changes that those other clients force upon the class. We would like to
avoid such couplings where possible, and so we want to separate the interfaces where pos-
sible.
Consider the TimedDoor again. Here is an object which has two separate interfaces used
by two separate clients; Timer, and the users of Door. These two interfaces must be imple-
mented in the same object since the implementation of both interfaces manipulates the
same data. So how can we conform to the ISP? How can we separate the interfaces when
they must remain together?
The answer to this lies in the fact that clients of an object do not need to access it
through the interface of the object. Rather, they can access it through delegation, or
through a base class of the object.
The Interface Segregation Principle (ISP) 6
We can employ the object form of theADAPTER2 pattern to the TimedDoor problem.
The solution is to create an adapter object that derives from TimerClient and delegates to
the TimedDoor. Figure 2 shows this solution.
When the TimedDoor wants to register a timeout request with the Timer, it creates a
DoorTimerAdapter and registers it with the Timer. When the Timer sends the TimeOut
message to the DoorTimerAdapter, the DoorTimerAdapter delegates the message back to
the TimedDoor.
Figure 2
Door Timer Adapter
Door TimerClient
Abstract Abstract
DoorTimer
TimedDoor Adapter
This solution conforms to the ISP and prevents the coupling of Door clients to Timer.
Even if the change to Timer shown in Listing 3 were to be made, none of the users of Door
would be affected. Moreover, TimedDoor does not have to have the exact same interface
as TimerClient. The DoorTimerAdapter can translate the TimerClient interface into the
TimedDoor interface. Thus, this is a very general purpose solution. (See Listing 4)
Listing 4
Object Form of Adapter Pattern
class TimedDoor : public Door
{
public:
virtual void DoorTimeOut(int timeOutId);
};
class DoorTimerAdapter : public TimerClient
{
public:
DoorTimerAdapter(TimedDoor& theDoor)
: itsTimedDoor(theDoor)
{}
virtual void TimeOut(int timeOutId)
{itsTimedDoor.DoorTimeOut(timeOutId);}
private:
TimedDoor& itsTimedDoor;
};
However, this solution is also somewhat inelegant. It involves the creation of a new
object every time we wish to register a timeout. Moreover the delegation requires a very
small, but still non-zero, amount of runtime and memory. There are application domains,
such as embedded real time control systems, in which runtime and memory are scarce
enough to make this a concern.
Figure 3 and Listing 5 show how Multiple Inheritance can be used, in the class form of the
ADAPTER pattern, to achieve the ISP. In this model, TimedDoor inherits from both Door
and TimerClient. Although clients of both base classes can make use of TimedDoor, nei-
ther actually depend upon the TimedDoor class. Thus, they use the same object through
separate interfaces.
Figure 3
Multiply Inherited Timed Door
Door TimerClient
TimedDoor
Listing 5
Class Form of Adapter Pattern
class TimedDoor : public Door, public TimerClient
{
public:
virtual void TimeOut(int timeOutId);
};
This solution is my normal preference. Multiple Inheritance does not frighten me.
Indeed, I find it quite useful in cases such as this. The only time I would choose the solu-
tion in Figure 2 over Figure 3 is if the translation performed by the DoorTimerAdapter
object were necessary, or if different translations were needed at different times.
The ATM User Interface Example 8
ATM UI
Abstract
Consider also that each different transaction that the ATM can perform is encasulated
as a derivative of the class Transaction. Thus we might have classes such as DepositTrans-
action, WithdrawlTransaction, TransferTransaction, etc. Each of these objects issues mes-
sage to the UI. For example, the DepositTransaction object calls the
RequestDepositAmount member function of the UI class. Whereas the TransferTransac-
tion object calls the RequestTransferAmount member function of UI. This corresponds to
the diagram in Figure 5.
Notice that this is precicely the situation that the ISP tells us to avoid. Each of the
transactions is using a portion of the UI that no other object uses. This creates the possibil-
ity that changes to one of the derivatives of Transaction will force coresponding change to
the UI, thereby affecting all the other derivatives of Transaction, and every other class that
depends upon the UI interface.
This unfortunate coupling can be avoided by segregating the UI interface into indu-
vidual abstract base classes such as DepositUI, WithdrawUI and TransferUI. These
abstract base classes can then be multiply inherited into the final UI abstract class. Figure
6 and Listing 6 show this model.
It is true that, whenever a new derivative of the Transaction class is created, a core-
sponding base class for the abstract UI class will be needed. Thus the UI class and all its
derivatives must change. However, these classes are not widely used. Indeed, they are
probably only used by main, or whatever process boots the system and creates the con-
crete UI instance. So the impact of adding new UI base classes is contained.
9 : The Interface Segregation Principle
Figure 5
ATM Transaction Hierarchy
Transaction
Abstract
UI
Abstract
Figure 6
Segregated ATM UI Interface
UI
Abstract
Listing 6
Segregated ATM Interfaces
class DepositUI
{
public:
virtual void RequestDepositAmount() = 0;
};
The ATM User Interface Example 10
, public WithdrawlUI,
, public TransferUI
{
public:
virtual void RequestDepositAmount();
virtual void RequestWithdrawlAmount();
virtual void RequestTransferAmount();
};
A careful examination of Listing 6 will show one of the issues with ISP conformance
that was not obvious from the TimedDoor example. Note that each transaction must some-
how know about its particular version of the UI. DepositTransaction must know about
DepositUI; WithdrawTransaction must know about WithdrawUI, etc. In Listing 6 I have
addressed this issue by forcing each transaction to be constructed with a reference to its
particular UI. Note that this allows me to employ the idom in Listing 7.
Listing 7
Interface Initialization Idiom
UI Gui; // global object;
void f()
{
DepositTransaction dt(Gui);
}
This is handy, but also forces each transaction to contain a reference member to its UI.
Another way to address this issue is to create a set of global constants as shown in Listing
8. As we discovered when we discussed the Open Closed Principle in the January 96 issue,
global variables are not always a symptom of a poor design. In this case they provide the
distinct advantage of easy access. And since they are references, it is impossible to change
them in any way, therefore they cannot be manipulated in a way that would surprise other
users.
Listing 8
Seperate Global Pointers
// in some module that gets linked in
// to the rest of the app.
static UI Lui; // non-global object;
DepositUI& GdepositUI = Lui;
WithdrawlUI& GwithdrawlUI = Lui;
TransferUI& GtransferUI = Lui;
GwithdrawlUI.RequestWithdrawlAmount();
...
}
};
One might be tempted to put all the globals in Listing 8 into a single class in order to
prevent pollution of the global namespace. Listing 9 shows such an approach. This, how-
ever, has an unfortunate effect. In order to use UIGlobals, you must #include ui_globals.h.
This, in turn, #includes depositUI.h, withdrawUI.h, and transferUI.h. This means that any
module wishing to use any of the UI interfaces transitively depends upon all of them;
exactly the situation that the ISP warns us to avoid. If a change is made to any of the UI
interfaces, all modules that #include ui_globals.h are forced to recompile. The UIGlobals
class has recombined the interfaces that we had worked so hard to segregate!
Listing 9
Wrapping the Globals in a class
// in ui_globals.h
#include “depositUI.h”
#include “withdrawlUI.h”
#include “transferUI.h”
class UIGlobals
{
public:
static WithdrawlUI& withdrawl;
static DepositUI& deposit;
static TransferUI& transfer
};
// in ui_globals.cc
static UI Lui; // non-global object;
DepositUI& UIGlobals::deposit = Lui;
WithdrawlUI& UIGlobals::withdrawl = Lui;
TransferUI& UIGlobals::transfer = Lui;
Consider a function ‘g’ that needs access to both the DepositUI and the TransferUI. Con-
sider also that we wish to pass the UIs into this function. Should we write the function pro-
totype like this: void g(DepositUI&, TransferUI&);? Or should we write it like this:
void g(UI&);?
The temptation to write the latter (monadic) form is strong. After all, we know that in
the former (polyadic) form, both arguments will refer to the same object. Moreover, if we
were to use the polyadic form, its invocation would look like this: g(ui, ui); Somehow
this seems perverse.
Perverse or not, the polyadic form is preferable to the monadic form. The monadic
form forces ‘g’ to depend upon every interface included in UI. Thus, when WithdrawUI
13 : The Interface Segregation Principle
changed, ‘g’ and all clients of ‘g’ would have to recompile. This is more perverse than
g(ui,ui);! Moreover, we cannot be sure that both arguments of ‘g’ will always refer to
the same object! In the future, it may be that the interface objects are separated for some
reason. From the point of view of function ‘g’, the fact that all interfaces are combined
into a single object is information that ‘g’ does not need to know. Thus, I prefer the poly-
adic form for such functions.
Conclusion
In this article we have discussed the disadvantages of “fat interfaces”; i.e. interfaces that
are not specific to a single client. Fat interfaces lead to inadvertent couplings beween cli-
ents that ought otherwise to be isolated. By making use of the ADAPTER pattern, either
through delegation (object form) or multiple inheritance (class form), fat interfaces can be
segregated into abstract base classes that break the unwanted coupling between clients.
This article is an extremely condensed version of a chapter from my new book: Pat-
terns and Advanced Principles of OOD, to be published soon by Prentice Hall. In subse-
quent articles we will explore many of the other principles of object oriented design. We
will also study various design patterns, and their strengths and weaknesses with regard to
implementation in C++. We will study the role of Booch’s class categories in C++, and
their applicability as C++ namespaces. We will define what “cohesion” and “coupling”
mean in an object oriented design, and we will develop metrics for measuring the quality
of an object oriented design. And after that, we will discuss many other interesting topics.
The Dependency Inversion
Principle
This is the third of my Engineering Notebook columns for The C++ Report. The articles
that will appear in this column will focus on the use of C++ and OOD, and will address
issues of software engi-
neering. I will strive for Sidebar: Unified Notation 0.8
articles that are pragmatic
and directly useful to the Had by
software engineer in the Reference
Used Base Class
trenches. In these articles I
will make use of Booch’s
and Rumbaugh’s new uni- Had By Value
Introduction
My last article (Mar, 96) talked about the Liskov Substitution Principle (LSP). This princi-
ple, when applied to C++, provides guidance for the use of public inheritance. It states that
every function which operates upon a reference or pointer to a base class, should be able to
operate upon derivatives of that base class without knowing it. This means that the virtual
member functions of derived classes must expect no more than the corresponding member
functions of the base class; and should promise no less. It also means that virtual member
functions that are present in base classes must also be present in the derived classes; and
they must do useful work. When this principle is violated, the functions that operate upon
pointers or references to base classes will need to check the type of the actual object to
make sure that they can operate upon it properly. This need to check the type violates the
Open-Closed Principle (OCP) that we discussed last January.
In this column, we discuss the structural implications of the OCP and the LSP. The
structure that results from rigorous use of these principles can be generalized into a princi-
ple all by itself. I call it “The Dependency Inversion Principle” (DIP).
1
What goes wrong with software? 2
Have you ever presented a software design, that you were especially proud of, for review
by a peer? Did that peer say, in a whining derisive sneer, something like: “Why’d you do it
that way?”. Certainly this has happened to me, and I have seen it happen to many other
engineers too. Clearly the disagreeing engineers are not using the same criteria for defin-
ing what “bad design” is. The most common criterion that I have seen used is the TNTWI-
WHDI or “That’s not the way I would have done it” criterion.
But there is one set of criteria that I think all engineers will agree with. A piece of
software that fulfills its requirements and yet exhibits any or all of the following three
traits has a bad design.
1. It is hard to change because every change affects too many other parts of the sys-
tem. (Rigidity)
2. When you make a change, unexpected parts of the system break. (Fragility)
3. It is hard to reuse in another application because it cannot be disentangled from
the current application. (Immobility)
Moreover, it would be difficult to demonstrate that a piece of software that exhibits
none of those traits, i.e. it is flexible, robust, and reusable, and that also fulfills all its
requirements, has a bad design. Thus, we can use these three traits as a way to unambigu-
ously decide if a design is “good” or “bad”.
What is it that makes a design rigid, fragile and immobile? It is the interdependence of the
modules within that design. A design is rigid if it cannot be easily changed. Such rigidity
is due to the fact that a single change to heavily interdependent software begins a cascade
of changes in dependent modules. When the extent of that cascade of change cannot be
3 : The Dependency Inversion Principle
predicted by the designers or maintainers, the impact of the change cannot be estimated.
This makes the cost of the change impossible to predict. Managers, faced with such unpre-
dictability, become reluctant to authorize changes. Thus the design becomes officially
rigid.
Fragility is the tendency of a program to break in many places when a single change is
made. Often the new problems are in areas that have no conceptual relationship with the
area that was changed. Such fragility greatly decreases the credibility of the design and
maintenance organization. Users and managers are unable to predict the quality of their
product. Simple changes to one part of the application lead to failures in other parts that
appear to be completely unrelated. Fixing those problems leads to even more problems,
and the maintenance process begins to resemble a dog chasing its tail.
A design is immobile when the desirable parts of the design are highly dependent
upon other details that are not desired. Designers tasked with investigating the design to
see if it can be reused in a different application may be impressed with how well the
design would do in the new application. However if the design is highly interdependent,
then those designers will also be daunted by the amount of work necessary to separate the
desirable portion of the design from the other portions of the design that are undesirable.
In most cases, such designs are not reused because the cost of the separation is deemed to
be higher than the cost of redevelopment of the design.
1. See: The Practical Guide To Structured Systems Design, by Meilir Page-Jones, Yourdon Press,
1988
Dependency Inversion 4
Dependency Inversion
One way to characterize the problem above is to notice that the module containing the
high level policy, i.e. the Copy() module, is dependent upon the low level detailed modules
that it controls. (i.e. WritePrinter() and ReadKeyboard()). If we could find a way to make
the Copy() module independent of the details that it controls, then we could reuse it freely.
We could produce other programs which used this module to copy characters from any
5 : The Dependency Inversion Principle
By now, some of you are probably saying to yourselves that you could get the same bene-
fits by writing Copy() in C, using the device independence inherent to stdio.h; i.e.
getchar and putchar (See Listing 4). If you consider Listings 3 and 4 carefully, you
will realize that the two are logically equivalent. The abstract classes in Figure 3 have been
replaced by a different kind of abstraction in Listing 4. It is true that Listing 4 does not use
classes and pure virtual functions, yet it still uses abstraction and polymorphism to achieve
its ends. Moreover, it still uses dependency inversion! The Copy program in Listing 4 does
not depend upon any of the details it controls. Rather it depends upon the abstract facilities
The Dependency Inversion Principle 6
One might question why I use the word “inversion”. Frankly, it is because more traditional
software development methods, such as Structured Analysis and Design, tend to create
software structures in which high level modules depend upon low level modules, and in
which abstractions depend upon details. Indeed one of the goals of these methods is to
define the subprogram hierarchy that describes how the high level modules make calls to
the low level modules. Figure 1 is a good example of such a hierarchy. Thus, the depen-
dency structure of a well designed object oriented program is “inverted” with respect to
the dependency structure that normally results from traditional procedural methods.
Consider the implications of high level modules that depend upon low level modules.
It is the high level modules that contain the important policy decisions and business mod-
els of an application. It is these models that contain the identity of the application. Yet,
when these modules depend upon the lower level modules, then changes to the lower level
modules can have direct effects upon them; and can force them to change.
This predicament is absurd! It is the high level modules that ought to be forcing the
low level modules to change. It is the high level modules that should take precedence over
the lower level modules. High level modules simply should not depend upon low level
modules in any way.
Moreover, it is high level modules that we want to be able to reuse. We are already
quite good at reusing low level modules in the form of subroutine libraries. When high
level modules depend upon low level modules, it becomes very difficult to reuse those
high level modules in different contexts. However, when the high level modules are inde-
pendent of the low level modules, then the high level modules can be reused quite simply.
7 : The Dependency Inversion Principle
Policy Layer
Layering
Mechanism
According to Booch2, “...all well Layer
structured object-oriented architec-
tures have clearly-defined layers,
with each layer providing some Utility Layer
coherent set of services though a
well-defined and controlled inter-
face.” A naive interpretation of this statement might lead a designer to produce a structure
similar to Figure 3. In this diagram the high level policy class uses a lower level Mecha-
nism; which in turn uses a detailed level utility class. While this may look appropriate, it
has the insidious characteristic that the Policy Layer is sensitive to changes all the way
down in the Utility Layer. Dependency is transitive. The Policy Layer depends upon
something that depends
upon the Utility Layer, thus
the Policy Layer transi-
Figure 4: Abstract Layers
tively depends upon the
Utility Layer. This is very Policy Layer Mechanism
unfortunate. Interface
Abstract
Figure 4 shows a more
appropriate model. Each of
the lower level layers are
represented by an abstract
class. The actual layers are Mechanism Utility Interface
Layer
then derived from these
Abstract
abstract classes. Each of
the higher level classes
uses the next lowest layer
through the abstract inter-
face. Thus, none of the lay- Utility Layer
ers depends upon any of
the other layers. Instead,
the layers depend upon abstract classes. Not only is the transitive dependency of Policy
Layer upon Utility Layer broken, but even the direct dependency of Policy Layer upon
Mechanism Layer is broken.
Using this model, Policy Layer is unaffected by any changes to Mechanism Layer or
Utility Layer. Moreover, Policy Layer can be reused in any context that defines lower level
modules that conform to the Mechanism Layer interface. Thus, by inverting the dependen-
cies, we have created a structure which is simultaneously more flexible, durable, and
mobile.
One might complain that the structure in Figure 3 does not exhibit the dependency, and
transitive dependency problems that I claimed. After all, Policy Layer depends only upon
the interface of Mechanism Layer. Why would a change to the implementation of Mecha-
nism Layer have any affect at all upon Policy Layer?
In some object oriented language, this would be true. In such languages, interface is
separated from implementation automatically. In C++ however, there is no separation
between interface and implementation. Rather, in C++, the separation is between the defi-
nition of the class and the definition of its member functions.
In C++ we generally separate a class into two modules: a .h module and a .cc mod-
ule. The .h module contains the definition of the class, and the .cc module contains the
definition of that class’s member functions. The definition of a class, in the .h module,
contains declarations of all the member functions and member variables of the class. This
information goes beyond simple interface. All the utility functions and private variables
needed by the class are also declared in the .h module. These utilities and private vari-
ables are part of the implementation of the class, yet they appear in the module that all
users of the class must depend upon. Thus, in C++, implementation is not automatically
separated from interface.
This lack of separation between interface and implementation in C++ can be dealt
with by using purely abstract classes. A purely abstract class is a class that contains noth-
ing but pure virtual functions. Such a class is pure interface; and its .h module contains
no implementation. Figure 4 shows such a structure. The abstract classes in Figure 4 are
meant to be purely abstract so that each of the layers depends only upon the interface of
the subsequent layer.
A Simple Example
Dependency Inversion can be applied wherever one class sends a message to another. For
example, consider the case of the Button object and the Lamp object.
The Button object senses the external environment. It can determine whether or not a
user has “pressed” it. It doesn’t matter what the sensing mechanism is. It could be a button
icon on a GUI, a physical button being pressed by a human finger, or even a motion detec-
9 : The Dependency Inversion Principle
tor in a home security system. The Button object detects that a user has either activated or
deactivated it. The lamp object affects the external environment. Upon receiving a TurnOn
message, it illuminates a light of some kind. Upon receiving a TurnOff message it extin-
guishes that light. The physical mechanism is unimportant. It could be an LED on a com-
puter console, a mercury vapor
lamp in a parking lot, or even
the laser in a laser printer. Figure 5: Naive Button/Lamp Model
How can we design a sys- TurnOn
tem such that the Button object Button Lamp
controls the Lamp object? Fig- TurnOff
ure 5 shows a naive model. The
Button object simply sends the
TurnOn and TurnOff message
to the Lamp. To facilitate this,
the Button class uses a “con- Button Lamp
tains” relationship to hold an
instance of the Lamp class.
Listing 5 shows the C++ Listing 5: Naive Button/Lamp Code
code that results from this --------------lamp.h----------------
class Lamp
model. Note that the Button {
class depends directly upon the public:
Lamp class. In fact, the but- void TurnOn();
void TurnOff();
ton.cc module #includes };
the lamp.h module. This -------------button.h---------------
dependency implies that the class Lamp;
class Button
button class must change, or at {
very least be recompiled, public:
whenever the Lamp class Button(Lamp& l) : itsLamp(&l) {}
void Detect();
changes. Moreover, it will not private:
be possible to reuse the Button Lamp* itsLamp;
class to control a Motor object. };
-------------button.cc--------------
Figure 5, and Listing 5 #include “button.h”
violate the dependency inver- #include “lamp.h”
sion principle. The high level void Button::Detect()
policy of the application has {
not been separated from the bool buttonOn = GetPhysicalState();
if (buttonOn)
low level modules; the abstrac- itsLamp->TurnOn();
tions have not been separated else
from the details. Without such itsLamp->TurnOff();
}
a separation, the high level pol-
icy automatically depends
upon the low level modules, and the abstractions automatically depend upon the details.
A Simple Example 10
What is the high level policy? It is the abstractions that underlie the application, the
truths that do not vary when the details are changed. In the Button/Lamp example, the
underlying abstraction is to detect an on/off gesture from a user and relay that gesture to a
target object. What mechanism is used to detect the user gesture? Irrelevant! What is the
target object? Irrelevant! These are details that do not impact the abstraction.
To conform to the principle of
Figure 6: Inverted Button Model
dependency inversion, we must
isolate this abstraction from the
details of the problem. Then we
must direct the dependencies of Button ButtonClient
the design such that the details Abstract Abstract
depend upon the abstractions. Fig-
ure 6 shows such a design.
In Figure 6, we have isolated
the abstraction of the Button class,
from its detailed implementation. Button Lamp
Listing 6 shows the corresponding Implementation
code. Note that the high level pol-
icy is entirely captured within the
abstract button class3. The Button class knows nothing of the physical mechanism for
detecting the user’s gestures; and it knows nothing at all about the lamp. Those details are
isolated within the concrete derivatives: ButtonImplementation and Lamp.
The high level policy in Listing 6 is reusable with any kind of button, and with any
kind of device that needs to be controlled. Moreover, it is not affected by changes to the
low level mechanisms. Thus it is robust in the presence of change, flexible, and reusable.
Once could make a legitimate complaint about the design in Figure/Listing 6. The device
controlled by the button must be derived from ButtonClient. What if the Lamp class comes
from a third party library, and we cannot modify the source code.
Figure 7 demonstrates how the Adapter pattern can be used to connect a third party
Lamp object to the model. The LampAdapter class simply translates the TurnOn and Turn-
Off message inherited from ButtonClient, into whatever messages the Lamp class needs to
see.
Button ButtonClient
Conclusion
The principle of dependency inversion is at the root of many of the benefits claimed for
object-oriented technology. Its proper application is necessary for the creation of reusable
frameworks. It is also critically important for the construction of code that is resilient to
3. Aficionados of Patterns will recognize the use of the Template Method pattern in the Button Hier-
archy. The member function: Button::Detect() is the template that makes use of the pure virtual
function: Button::GetState(). See: Design Patterns, Gamma, et. al., Addison Wesley, 1995
Conclusion 12
change. And, since the abstractions and details are all isolated from each other, the code is
much easier to maintain.
This article is an extremely condensed version of a chapter from my new book: Pat-
terns and Advanced Principles of OOD, to be published soon by Prentice Hall. In subse-
quent articles we will explore many of the other principles of object oriented design. We
will also study various design patterns, and their strengths and weaknesses with regard to
implementation in C++. We will study the role of Booch’s class categories in C++, and
their applicability as C++ namespaces. We will define what “cohesion” and “coupling”
mean in an object oriented design, and we will develop metrics for measuring the quality
of an object oriented design. And after that, we will discuss many other interesting topics.