Software Design Patterns
Design pattern are typical ways to organise the components of a program in typical situations, whereas Design principles are more general guidelines to make design robust.
Many software engineers could work for many years without knowing a single pattern. It can also happen that we are applying a pattern without even knowing it.
Design Patterns represent some of the most acceptable practices experienced object-oriented software engineers utilize. In object-oriented systems, a Design Pattern methodically names, motivates, and describes a general design that addresses a recurring design challenge. It explains the problem, the solution or approach, when to use it, and the ramifications. It also includes tips and examples for implementation (although a design pattern is not an implementation and is not bound to a particular programming language).
Design Patterns: Six Rules
The essence of Design Patterns consists of the following six rules:
-
They are tried-and-true solutions: Because developers often use Design Patterns, we may be confident that they function. Not only that, but we can also guarantee that they were altered several times and that optimizations were most likely performed.
-
They are simple to re-use: Design Patterns describe a reusable solution that may modify to solve various specific situations because they aren't tied to a specific situation.
Consider the Iterator Design Pattern, reusable across STL despite container and algorithm changes. Iterators act as a glue between the container and the algorithm.
-
They have a strong personalities: Design Patterns may elegantly describe a considerable solution. The Visitor pattern, for example, is used to perform a new operation on a range/group of classes. As a result, the standard library adopted this design with a single function, namely the std::visit algorithm. The same is true for boost::flyweight.
-
They facilitate communication: Developers' knowledge about Design Patterns can communicate more readily about potential solutions to a given challenge.
If we're part of a team of developers, agree on Design Patterns with our colleagues since they can help us solve problems more effectively. We should also follow similar practices for software maintenance, as it makes maintenance operations faster and more efficient.
-
They eliminate the need for code refactoring: When an application is created with Design Patterns in mind, we may not need to rewrite the code later since applying the relevant Design Pattern to a specific problem is already an optimum solution.
If such solutions are later updated, they may be applied effortlessly by any excellent software developer without causing any complications.
-
They reduce the codebase's size: Design Patterns use less code than alternative solutions since they are generally beautiful and optimal. This isn't always the case, because many developers add extra code to improve understanding.
Types of Design Patterns
Design Patterns are divided into the following three categories.
- Creational
-
Class instantiation or object generation is the focus of these Design Patterns.
Class-creational patterns and object-creational patterns are two subsets of these patterns. While class-creation patterns make good use of inheritance in the instantiation process, object-creation patterns use delegation.
Factory Method, Abstract Factory, Builder, Singleton, Object Pool, and Prototype are creational Design Patterns.
Use Case of Creational Design Patterns
- Assume a programmer wants to create a simple DBConnection class to connect to a database and needs to use the database from code in numerous places. The developer will typically create an instance of the DBConnection class and use it to perform database operations wherever they are needed. As each example of the DBConnection class has a different connection to the database, numerous connections to the database are created. To deal with it, we make the DBConnection class a singleton class, which means that only one instance of DBConnection is generated, and only one connection is made. We can control load balance, redundant connections, and so on since we can manage DBConnection from a single instance.
- We can use the Factory design if we wish to create several instances of the same type while maintaining loose coupling. A factory-Design Pattern-implemented class acts as a link between numerous classes – for instance, the use of various database servers such as SQL Server and Oracle. We should use the Factory Design Pattern to achieve loose coupling and create a similar kind of object if we are developing an application with a SQL Server database as the back end. Still, if we need to change the database to Oracle, we will need to modify all of our code. Hence, as Factory Design Patterns maintain loose coupling and easy implementation, we should use the factory layout design to achieve loose coupling and create a similar kind of object.
- Structural
-
These Design Patterns deal with grouping distinct classes and objects together to create larger structures and add new functionality.
Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Private Class Data, and Proxy are structural Design Patterns.
Use Case of Structural Design Patterns
- An Adapter Design Pattern is used when two interfaces are incompatible and want to establish a relationship between them using an adapter. The adapter pattern transforms a class's interface into another interface or class that the client expects, allowing classes that would otherwise be incompatible with operating together. We can use the adapter pattern in these types of incompatible instances.
- Behavioral
-
Identifying and discovering shared communication patterns between items are all about behavioral patterns.
Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Null Object, Observer, State, Scheme, Template method, and Visitor are behavioral patterns.
What Exactly is the Gang of Four (GOF)?
The book Design Patterns: Elements of Reusable Object-Oriented Software, written by four writers, Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, was published in 1994 and introduced the first concept of Design Patterns in software development.
Gang of Four is the collective name for these four authors (GOF). According to these authors, Design Patterns are essentially based on the following object-oriented design principles:
- Not an implementation, but to program to an interface.
- Object composition should take precedence over inheritance.
Behavioral Design Patterns
Chain of Responsibility
The design pattern Chain of Responsibility consists in putting in place a sequence of objects that fallback on each other to handle a request.
That is, a client sends a request to the first object of the sequence. If it can handle it, it does. Otherwise, it passes on the request to the second object of the sequence. And so on
Command
The design pattern Command consists in creating a class to represent the execution of an action, as opposed to having the calling code contain the code of the action.
This resulting class is supposed to be autonomous, in that any caller can invoke it, without passing it additional parameters. It is its constructor that takes all that is needed to perform the operation:
class MyCommand
{
public:
// constructor
void execute();
private:
// All the stuff needed to perform the action
};
The above code is the Java-like traditional presentation of the Command design pattern, but the Command pattern can take various forms. For example, it can take the form of a lambda:
auto myAction =
[stuffToPerformTheAction]
()
{ /* action */ };
One of the benefits of the Command pattern is that the actions become manipulable by themselves: they can be stored, sorted, invoked at a later time, repeatedly invoked, etc.
To me, the Command pattern allows to create a class that revolves around one single focus: executing an action. Seen this way, it is a way to create High Cohesion. And more precisely than this, it is a Pure Fabrication. Indeed, lambdas don't map to something in the domain, in general.
It can be argued that Command involves Polymorphism as well. The GoF book even suggests to add a base class above the objects that perform the action, with a virtual method execute(), in order to have various types of actions behind a unified interface.
This is certainly useful, but my understanding of the pattern is that its essence is about introducing a class to represent the action. Using this class polymorphically is a only a nice addition.
Interpreter
The Interpreter design pattern helps achieving modular design when processing an Abstract Syntax Tree (AST) of a given language.
The AST is the structured representation of an expression in the form of a tree, stemming from the fact that expressions can be made of sub-expressions: the children of a node in the AST are the subparts of the expression represented by that node.
The GoF book takes the example of the language of regular expressions: a given regex can be represented in the form of an AST.
The Interpreter design pattern consists in defining a base class for an expression, with a method called interpret. And then defining one class derived from this base classe for each type of the sub-expression in the language. Each class implements the the method interpret() of its base class, potentially by forwarding some of the work to its children in the AST. The nodes of the AST are made of those classes.
interpret can takes a Context as a parameter. The Context can contain something to be matched up or updated with the expression represented by the AST.
In the example of the AST representing a regular expression, the context contains the string to match it up with. For an AST representing C++ code, the context can contain the object code to output based on the traversal of the AST.
The essence of the Interpreter design pattern lies, in my opinion, in Polymorphism. Indeed, the base class allows to build up a tree in a generic way, and the concrete classes perform the job depending on each sub-expression.
Iterator
Thanks to the STL, we C++ developers are familiar with iterators. Is the GoF design pattern Iterator similar to STL iterators?
Iterator and the STL iterators
The GoF describes iterators as classes that encapsulate the responsibility of the traversal of a collection. So far, that sounds similar to STL iterators.
In the GoF book, the iterator class is instantiated by the collection class, and has the following methods:
class Iterator
{
First()
Next()
IsDone()
CurrentItem()
};
This Iterator has almost the same responsibilities as STL iterators:
Next()corresponds tooperator++IsDone()corresponds to a comparison with an end iterator by usingoperator!=CurrentItem()corresponds tooperator*
Only First() is not covered by the STL iterators: STL iterators don't allow going back to the beginning of a collection.
In the design pattern Iterator, the collection is in charge of producing iterators that allow to traverse it. This is also the case in the STL, with the typical begin() and end() member functions being part of the conventions of the STL.
Iterator and design principles
The alternative to iterators would be that the collection itself handles its traversal, and includes that feature in its interface. The design pattern Iterator is an extraction of the responsibility of traversing the collection, into a dedicated class.
Seen this way, Iterator is a way to achieve High Cohesion. It is a Pure Fabrication as, even though collections can map to domain objects, iterators usually don't.
Polymorphic iterators
The GoF book goes on and shows how to achieve polymorphism with iterators. Even though this is very powerful, in my opinion this is only an extension of the concept of iterator, and not its essence. Indeed, we could have iterators that are not generic nor polymorphic, and that still follow the design pattern Iterator.
For this reason, I think Iterator is more closely related to Pure Fabrication than to Polymorphism.
But for the sake of exploring design patterns, let's review the two sorts of polymorphisms that we can achieve with the design pattern Iterator.
The first type of polymorphism is in the behaviour of the iterator itself. For example, we could imagine an iterator that skips some elements that don't satisfy a predicate. In fact this type of polymorphism is exactly the idea behind range adaptors, in ranges libraries.
The second type of polymorphism is related to the collection. It would be nice to have a unified interface that could iterate on various data structures. For example, we would like to use the same code to iterate on contiguous sequences as well as on trees.
The GoF book suggests to have two hierarchies using inheritance in order to achieve this: Collection and Iterator. Data structure classes like List and Tree are derived from Collection, whereas specific iterator classes like ListIterator and TreeIterator are derived from Iterator.
C++ standard containers such as std::vector and std::map, which are implemented as a contiguous sequence and a tree, respectively, don't operate this way.
They do offer polymorphism, as we can write the same code to traverse a std::vector or a std::map, but they don't rely on inheritance. They rely on generic code, since those containers provide an interface with the same member function names (begin() and end()). This allows to write such polymorphic code:
for (auto const& element : myCollection)
{
// do something with element
}
It is polymorphic because it works whether myCollection is a std::vector or a std::map.
In summary, Iterator is a Pure Fabrication, that Polymorphism can make more powerful.
Mediator
The design pattern Mediator consists in introducing an object that centralises the logic of interaction between a set of other objects.
The GoF uses the example of a GUI that has many elements that interact with each other. One way to implement this would be to have each class communicate with the others when they need to trigger the appropriate reactions.
But doing this introduces intense coupling, as many objects come to interact with each other.
Mediator mitigates this problem by having all objects notify only one object, the Mediator object, whenever they need to trigger a reaction. The Mediator packages all the logic, and forwards the incoming notifications to the appropriate objects.
As a result, all the objects are connected with the Mediator, instead of all objects being connected with each other.
Seen this way, Mediator is a way to achieve the design principle of Indirection.
Memento
The design pattern Memento helps to restore an object to a previous state.
It works the following way: object A creates a Memento object, and sets in it some information about its current state. The Memento objects is stored somewhere, for example in another object B.
Then A lives on its life, and its state changes.
Later, we give back the Memento object to A, and A retrieves the information that it had put into it. After doing this, A is back to the state it was when it created the Memento object.
B is not concerned with what information is in the Memento object, nor even what kind of information it contains. It just holds it in order for it to be available to eA at a later point.
To which design principle does Memento correspond?
To be honest, I'm not sure Memento relates well to one of the GRASP design principles. What do you think?
We could argue that Memento is about hiding the contents of the state of A. So if we come to modify the program and change the type of state that A needs, B won't be affected. This seems to relate to Low Coupling.
But should we consider that it's part of Protected Variations, in the sense that the Memento object hides the state of A? Or is it another type of Low Coupling?
Observer
The Observer pattern is about notifications between objects.
Consider an object A that can undergo events, in the general sense of events
. That can be GUI events, or changes of state, or anything that could be of interest for another object B. A is called Subject and B is called Observer.
A typical example is indeed for GUI events, for example the user has clicked on a widget. The class handling the GUI event needs to notify the business objects of the program that the event has happened, so that they can react accordingly.
Observer is not limited to GUI though. We can see it at any level of a program. For example, Qt's signals and slots are an implementation of the Observer pattern.
A given Subject can have an arbitrary number of Observers.
A central aspect of the Observer design pattern is that the Subject doesn't know what the Observers are. It just knows that they exist and how to notify them. To implement this aspect, we can use polymorphism, for example with inheritance. The Subject class then must implement the Observer interface (inherited).
The Observer interface could be implemented like this:
class Observer
{
virtual void onNotification() = 0;
virtual ~Observer();
};
And the Subject could hold a std::vector<Observer*> (a container of pointers to Observer), and traverse it to call onNotification() on each element, whenever the Subject needs to notify its observers.
State
The State design pattern is useful when an object can be in several formalized states. The GoF book takes the example of a TCP connection that can be Established, Listening or Closed.
There are many other examples in various fields of objects that can be in several states. For example a financial operation could be PendingApproval, Approved or Closed. Or in a project management application, a task could be ToDo, Doing, ReadyForTesting or Done.
One way to implement this is to have a constant or enum representing each state, and a value that can be equal to either one of the possible states:
enum class TaskState
{
ToDo,
Doing,
ReadyForTesting,
Done
};
With the task having the following member:
class Task
{
public:
// ...
private:
TaskState currentState_;
// ...
};
This way of representing the state in code can lead to complicated code, with if statements that test the possible values of currentState_. Depending on those values, the if statements would perform actions and/or modify the value of currentState_.
When such code grows in size, it typically grows in complexity. That is to say that this way of coding doesn't scale.
Why? Because it is the same code that handles all of the various possible states and their changes.
The State design patterns aims at solving this problem by representing each state by a type, and not by just the value of an enum:
class ToDo
{
// ...
};
class Doing
{
// ...
};
class ReadyForTesting
{
// ...
};
class Done
{
// ...
};
Each class owns the code of its corresponding state. Each class also manages the transitions to another state, by instantiating the corresponding class.
The GoF book suggests to have all such state classes inherit from a common base class. This allows to implement state changes by swapping the concrete implementation behind a pointer or reference to the base class.
The State design pattern can also be implemented with templates and phantom types.
Strategy
The Strategy design pattern consists in letting a calling code use one of several possible algorithms, by hiding them behind an interface.
For example, let's consider a certain task X that has three ways of being accomplished: method A, method B and method C. Rather than having A, B and C directly in the code that needs to perform X, we use three classes that implement the same interface, each one implementing one of the three methods.
class X {
public:
virtual void doX() = 0;
};
class MethodA: public X {
public:
void doX() override;
};
class MethodB: public X {
public:
void doX() override;
};
class MethodC: public X {
public:
void doX() override;
};
Some other part of the code sets either one of MethodA, MethodB or MethodC as a concrete implementation of the interface.
Even if Strategy allows to achieve cohesion by having each algorithm put away in its own class, it seems to me that the central aspect of Strategy is rather Polymorphism.
Some (More) Design Patterns
Factory method
(From https://www.fluentcpp.com/2022/06/05/design-patterns-vs-design-principles-factory-method/)
To understand what factory method means, let's analyse each of its two words:
- method: this means member function,
- factory: this means something that creates objects.
All this means that a factory method is a member function that creates objects.
It is important to note that this comes from a book on object oriented design (the GoF's Design Patterns book), that makes heavy use of member functions. But my understanding of the design pattern is that it applies beyond member functions. It applies to free functions too. So a more generic and idiomatic name for this design pattern in C++ could be Factory function.
Facade*
The Adapter Design Pattern*
The Strategy Design Pattern*
Template Method
First off, let's mention that the Template Method design pattern has nothing to do with C++ templates. Actually, there is an implementation of this design pattern in C++ that uses templates, but using templates is more an implementation detail rather than the essence of this pattern.
Template Method has even nothing to do with C++ in particular, and can be implemented in other languages that don't support templates.
Template Method consists in having a piece of code that has one or more customisation points.
Customisation Points
How? By using polymorphism. In the GoF book, the authors suggest to use runtime polymorphism, with inheritance and virtual member functions.
In our example, we would then have a base class that would look like this:
class Task
{
public:
void run();
virtual ~Task() = 0;
private:
virtual void doTheTask() const = 0;
};
It is the non-virtual run member function that contains the Template Method design pattern:
void Task::run()
{
std::cout << "Task in progress... ";
doTheTask();
std::cout << " ...done.\n";
}
A given task has its own class, that implements the Task base class:
class MyTask : public Task
{
private:
void doTheTask() const override;
};
(In case you're wondering, we can indeed override private virtual methods from the base class).
Now if you have code that uses the Task interface, you're guaranteed that logging will be executed without any additional code from the classes that implement the concrete tasks.
NVI
While we're talking about this, note that the Template Method design pattern is a way to implement the Non Virtual Interface, or NVI, pattern.
NVI consists in exposing only non-virtual methods in the public section of a base class. Those methods themselves call private virtual methods, that are implemented in derived classes.
The NVI pattern recognises that the public interface of a base class should not be coupled to the implementation of the virtual methods. Indeed, the former represents the interface, and the latter represents some implementation steps.
Some developers go as far as never defining a member method public and virtual at the same time. Said differently, they use NVI all the time.
NVI is a way to implement the Template Method design pattern. When the NVI public non virtual method simply calls the private virtual method, without any additional treatment, this can be seen as a degenerate case of Template Method.
Template method with C++ templates
Inheritance and virtual tables are just one way of implementing polymorphism.
We can also use C++ templates to implement polymorphism. Templates create another type of polymorphism, that is resolved at compile-time. This is a whole other topic, so if the previous sentence doesn't make perfect sense, it's not a problem. We'll come back to this elsewhere.
An implementation of the Template Method design pattern with C++ templates would look like this:
template <typename Task>
void runTask(Task const& task)
{
std::cout << "Task in progress... ";
task.doTheTask();
std::cout << " ...done.\n";
}
In this case, there is no base class any more. We can pass MyTask, that used to be the derived class in our previous example, to the runTask template function:
auto myTask = MyTask{};
runTask(myTask);
Now the doTheTask member function needs to be public:
class MyTask : public Task
{
public:
void doTheTask() const override;
};
Indeed, in the previous implementation using inheritance, the code using the Template Method design pattern (the base class) had access to the implementation of the task via the redirection of virtual member functions.
Now that the code using the Template Method design pattern is in a free function, it has to call the public interface of MyTask, so its method has to be public (unless runTask is made a friend class).