Classification of design patterns in software development
Patterns can be classified in a number of ways. The book “Design Patterns” provides a good guideline.
Patterns can be classified in a number of ways. The best known are the patterns used in the books Design Patterns: Elements of Reusable Object-Oriented Software and Pattern-Oriented Software Architecture, Volume 1.
Rainer Grimm has been working as a software architect, team leader and training manager for many years. He likes to write articles on the programming languages C++, Python and Haskell, but also likes to speak frequently at specialist conferences. On his blog Modernes C++ he deals intensively with his passion for C++.
Design Patterns: Elements of Reusable Object-Oriented Software
The following table gives an initial overview of the 23 patterns presented in the book.
Studying the table, one can note two classifications. First: generation pattern, structure pattern and behavior pattern and second: class pattern and object pattern. The first classification is obvious, but not the second.
Generational patterns, structural patterns and behavioral patterns
- generation pattern deal with the creation of objects in a well-defined way,
- texture pattern provide mechanisms for organizing classes and objects for larger structures,
- behavior pattern deal with communication between objects.
The patterns that are in bold are the ones I’ve used a lot in my past. Consequently, I will write about them explicitly in future articles.
destruction pattern
There’s an asymmetry in this one. The book “Design Patterns: Elements of Reusable Object-Oriented Software” introduces creation patterns, but not destruction patterns. What to do?
- Design Patterns book co-author Jon Vlissides wrote about the destruction of a singleton in his book Pattern Hatching: Design Patterns Applied (1998).
- One can read Andrei Alexandrescu’s stunning book “Modern C++ Design: Generic Programming and Design Principle Applied” (2001) to learn how to destroy a singleton.
- The excellent book “Small Memory Software: Patterns for systems with limited memory” (2000) by James Noble and Charles Weir devotes an entire chapter to allocation.
Now I come to the not-so-obvious classification.
The scope of a pattern can be distinguished.
class pattern and object pattern
In my Design Pattern courses, I refer to class patterns and object patterns as metapatterns. I have two meta-patterns in mind when trying to solve a design challenge: inheritance versus composition. All 23 Design Patterns are variations on these two key principles. More specifically, inheritance is a class pattern and composition is an object pattern.
- class pattern apply classes and their subclasses. They use the separation of interface and implementation and runtime dispatch with virtual function calls. Their functions are hardcoded and available at compile time. They offer less flexibility and dynamic behavior than the object patterns.
- object pattern use the relationship of objects. You build an abstraction by assembling it from basic building blocks. This composition can be done at runtime. Consequently, object patterns are more flexible and defer the decision to the runtime of the program.
To be honest, inheritance is used far too often. Most of the time, composition is the better choice.
composition
In 2006 I held my first design pattern courses for the German automotive industry. To motivate the composition, I designed a generic car:
#include <iostream>
#include <memory>
#include <string>
#include <utility>
struct CarPart{
virtual int getPrice() const = 0;
};
struct Wheel: CarPart{
int getPrice() const override = 0;
};
struct Motor: CarPart{
int getPrice() const override = 0;
};
struct Body: CarPart{
int getPrice() const override = 0;
};
// Trabi
struct TrabiWheel: Wheel{
int getPrice() const override{
return 30;
}
};
struct TrabiMotor: Motor{
int getPrice() const override{
return 350;
}
};
struct TrabiBody: Body{
int getPrice() const override{
return 550;
}
};
// VW
struct VWWheel: Wheel{
int getPrice() const override{
return 100;
}
};
struct VWMotor: Motor{
int getPrice() const override{
return 500;
}
};
struct VWBody: Body{
int getPrice() const override{
return 850;
}
};
// BMW
struct BMWWheel: Wheel{
int getPrice() const override{
return 300;
}
};
struct BMWMotor: Motor{
int getPrice() const override{
return 850;
}
};
struct BMWBody: Body{
int getPrice() const override{
return 1250;
}
};
// Generic car
struct Car{
Car(std::unique_ptr<Wheel> wh,
std::unique_ptr<Motor> mo,
std::unique_ptr<Body> bo):
myWheel(std::move(wh)),
myMotor(std::move(mo)),
myBody(std::move(bo)){}
int getPrice(){
return 4 * myWheel->getPrice() +
myMotor->getPrice() + myBody->getPrice();
}
private:
std::unique_ptr<Wheel> myWheel;
std::unique_ptr<Motor> myMotor;
std::unique_ptr<Body> myBody;
};
int main(){
std::cout << '\n';
Car trabi(std::make_unique<TrabiWheel>(),
std::make_unique<TrabiMotor>(),
std::make_unique<TrabiBody>());
std::cout << "Offer Trabi: " << trabi.getPrice() << '\n';
Car vw(std::make_unique<VWWheel>(),
std::make_unique<VWMotor>(),
std::make_unique<VWBody>());
std::cout << "Offer VW: " << vw.getPrice() << '\n';
Car bmw(std::make_unique<BMWWheel>(),
std::make_unique<BMWMotor>(),
std::make_unique<BMWBody>());
std::cout << "Offer BMW: " << bmw.getPrice() << '\n';
Car fancy(std::make_unique<TrabiWheel>(),
std::make_unique<VWMotor>(),
std::make_unique<BMWBody>());
std::cout << "Offer Fancy: " << fancy.getPrice() << '\n';
std::cout << '\n';
}
I know from the international discussion in my Design Patterns courses that many people know a BMW and a VW, but perhaps have no idea about a Trabi. This also applies to many young people in Germany. Trabi is the abbreviation for Trabant and stands for small cars that were manufactured in the former GDR.
It’s pretty easy to explain the program. The generic car is a composite of four wheels, an engine and a body. Each component is from the abstract base class CarPart
derived and must therefore be the member function getPrice
to implement. The abstract base classes Wheel
, Motor
and Body
are not necessary, but improve the structure of the inheritance hierarchy. If a customer wants a special car, the generic class delegates Car
the call of getPrice
to their car parts. Of course in this class I applied both metapatterns, inheritance and composition, together to make the structure more typesafe and to keep the car parts easily customizable.
A thought experiment
I address the issues of composition and inheritance by answering the following questions:
- How many different cars can you build from existing vehicle parts?
- How many classes are required to solve the same complexity with inheritance?
- How easy/complex is it to use inheritance/composition to support a new car like Audi – given that all parts are available?
- How easy is it to change the price of a car part?
- Let’s say a customer wants a new, fancy car assembled from existing car parts. When do you have to decide whether to build the new car based on inheritance or composition? Which strategy is used at compile time and which at runtime?
Here is my reasoning:
- You can create 3 * 3 * 3 = 27 different cars from the 14 components.
- It takes 27 + 4 = 31 different classes to build 27 different cars. Each class must encode their car parts in their class name, e.g
TrabiWheelVWMotorBMWBody
,TrabiWheelVWMotorVWBody
,TrabiWheelVWMotorTrabiBody
, … . This quickly becomes unmaintainable. The same complexity arises when applying multiple inheritances andTrabiWheelVWMotorBMWBody
there are three base classes. In this case you would have toTrabiWheel
,VWMotor
andBMWBody
derive You would also need the member functiongetPrice
rename. - The composition strategy is just to implement the three car parts for car. This allows you to create 4 * 4 * 4 = 64 different cars from 17 components. In contrast, with inheritance, you have to expand the inheritance tree in all necessary branches.
- It’s pretty easy to change the price of a car part through composition. With inheritance, you have to go through the entire inheritance tree and change the price at every point.
- That’s my main argument: thanks to composition, you can assemble the car parts at runtime. In contrast, the inheritance strategy configures the auto at compile time. For car salespeople, this means that they have to store the car parts in order to assemble them when the customer comes. With inheritance, they have to pre-produce all the configurations of their car.
Of course, that was just my thought experiment. But it should make one point clear. In order to master the combinatorial complexity, simple, connectable components should be used. I call it the Lego principle.
What’s next?
The book Pattern-Oriented Software Architecture, Volume 1″ also provides a very interesting classification of patterns. I will present it in more detail in my next article.
My C++ training courses in 2022:
This year I will be offering the following open C++ courses.
- C++20: 08/23/2022 – 08/25/2022 (guaranteed date, suburb near Herrenberg)
- Clean Code: Best Practices for Modern C++: 10/11/2022 – 10/13/2022 (suburb near Herrenberg)
- Design pattern and architecture pattern with C++: 11/08/2022 – 11/10/2022 (deadline guarantee, online)