The following list shows some important design patterns, which are design principles to achieve great program architectures.
SOLID Design Principles
Single Responsibility Principle (SRP)
A class should only have a single responsibility.
Journal class and
PersistanceManager class for saving the journal entries instead of putting the saving functionality into the
Journal class itself.
Open-Closed Principle (OCP)
Entities should be open for extension but closed for modification. It is better to extend a class (for example using multiple inheritance) rather than modifying a class that has already been tested (the change may be necessary due to changed requirements). Patterns that extend a class without modifying the class itself are the observer pattern and the decorator pattern patterns.
Product class with different traits (color, size) and
ProductFilter class that has methods to filter for items in a vector that contains pointers to products (
ProductFilter::by_color_and_size). Adding more methods shows that the ProductFilter class would be modified, which should be avoided following the OCP. Instead, make a robust class, where the behavior can be changed dynamically. To solve this problem use for example the Specification Pattern (which is no GOF pattern). Create a specification interface
ISpecification that has a single pure virtual function
bool is_satisfied(T item) to check if an item satisfies a specification. Use anotehr interface for the Filter
IFilter with a single pure virtual funciton
vector<T*> filter(vector<T*> items, ISpecification<T>& spec). A concrete class that implements the IFilter interface will loop through the items and use the passed specification to return only the items that satisfy the specification. The only thing that is missing, is to implement concrete specification classes that inherit from the ISpecification interface. For example color, size and composite types that have a constructor which initializes their members (color, size, or both) and implement
is_satisfied by comparing the passed element with the member variable. Do not modify the original classes that are probably already tested, instead extend them using inheritance.
Liskov Substitution Principle (LSP)
Objects should be replaceable with instances of their subtypes without altering the correctness of the program. To follow this principle use the factory pattern (factory method or abstract factory).
Example: The classical rectangles and squares example shows how this principle is violated. Assume a
process(Rectangles& rect) function which can output wrong results when called with a
Square class that inherits from, or is a base class of
Rectangle. To solve this use a factory
RectangleFactory class that has methods to generate Rectangles (
Rectangle CreateRectangel(int width, int height),
Rectangle CreateSquare(int size)).
Interface Segregation Principle (ISP)
Many client-specific interfaces are better than one general purpose interface. No client (somebody how uses your code) should be forced to depend on methods that it does not use. Patterns that use this principle are the decorator pattern.
Example: Break up an interface into multiple other interfaces. Assume a multi-function device that is able to print, scan and fax some
Documents. A violation would be to use a single interface
IMachine which has pure virtual methods (
fax(vector<Documents*> docs)). A client that only wants to use some functionalities of this interface has to make an implementation
MFP (Multi Functional Peripheral). This is a bad idea for two main reasons: everytime only parts of the functionality changes, all the other methods need to be recompiled too. Anotehr reason is that the client probably does not need all of the functionality but has to implement it using the
IMachine interface. ISP is about breaking up the monolithic interface and create piece-wise abstractions. Therefore, create a spearate interface for the printing
IScanner and faxing
IFax functionality. These interfaces implement only the functionality that is related to them. If a client requires a printer
Printer, it has to implement the
IPrinter interface and its method
print(vector<Documents*> docs). If a client needs the whole machine, you can use multiple inheritance
struct IMachine : IPrinter, IScanner. An actual implementation would then result in the decorator pattern, which is an aggregate of the functionalities that it inherits:
Machine : IMachine would have member variable references or shared pointers of type
IScanner and a constructor that initializes these members (this is also an example of Dependency Injection). The actual implementations of
scan methods would then proxy it over to the variable members and use their methods
IScanner::scan. With this design it is possible to use different implementations of functionality that implment an interface (for example a color printer that implements
IPrinter) and pass it to the constructor of the
Machine struct, which would be an inversion of control container that is then reconfigured. Summary: Break up interfaces into small interfaces and to use a big interface that contains all parts use multiple inheritance. The concrete realisation would be a decorator pattern.
Dependency Inversion Principle (DIP)
Dependencies should be abstract rather than concrete. Depend upon abstractions. Do not depend upon concrete classes.
- High-level modules should not depend on low-level modules. Both should depend upon abstraction.
Example: reporting component (high-level module) should depend on a
ConsoleLogger, but can depend on an
ILogger. A logging mechanism (low-level module) should also depend upon an abstraction. This makes it easier to substitue during runtime and for testing.
- Abstraction should not depend upon details. Details should depend upon abstractions: Dependencies on interfaces and supertypes (base classes) is better than dependencies on concrete types. This is because it is hard to substitue afterwards if a concrete type is used (for example as a function argument). Compare this with the LSP (substitue a subtype with a supertype).
- Inversion of Control (IoC): the actual process of creating abstractions and getting them to replace dependencies.
- Dependency Injection: use of software frameworks (boost di) to ensure that a component’s dependencies are satisfied. These dependencies don’t have to be concrete types but can be interfaces or supertypes. These can be substitute with the subtypes as they are configured in the inversion of control container. The Inversion of Control Container manages the dependency injection. Injection means that the Inversion of Control Container initializes dependencies of a particular class by passing them as constructor parameters.
Example: Assume a car class
Car that has a shared pointer member that points to an egnine class
Engine, which is initialized by passing an engine object as a parameter to the constructor. This means that the engine is injected into the car, which it then depends upon.
In case there are other dependencies that need to be set by passing them to the constructor, all of them need to be instantiated before. A dependency injection (di) library such as boost di can avoid this tedious task. This is done using an injector that initializes all dependencies and injects them into the
Car class. A di library does this by checking the arguments of the constructor.
Another example would be an
ILogger interface for logging that has a virtual
LOG(const std::string& s) method. A concrete class
ConsoleLogger : ILogger. An injector can be bound using
bind to use a concrete ConsoleLogger when a client requests an
Further Design Principles
Encapsulate what varies
Identify the aspects of your application that vary and separate them from what stays the same.
Favor composition over inheritance. With composition it is possible to delegate behaviors instead of inheriting them.
Program to an interface, not an implementation.
Strive for loosely coupled designs between objects that interact. Loosely coupled designs allow us to build flexible object oriented systems that can handle change because they minimize the interdependency between objects. This principle can be seen in the observer pattern.
Classes should be open for extension but closed for modification. Instead of modifying a class by adding new methods and members use inheritance or even better composition. Extend the class by adding a new interface class member. This makes it possible to change the behavior even at runtime.
Depend on abstractions. Do not depend on concrete classes.
Talk only to your friends. This principle is important for maintining a low level of coupling between multiple classes. It prevents us from creating designs that have a larage number of classes coupled together so that changes in one part of the system cascade to other parts. Having dependencies between many classes, results in a fragile system that will be costly to maintain and complex for others to understand.
To follow this principle the following guidlines should be met: regarding any method of an object, the method should only invoke methods that belong to:
- The object itself
- Objects passed in as a parameter to the method
- Any object the method creates or instantiates
- Any components of the object
Calling methods on objects that were returned from calling other methods would violate this principle as the following example shows:
To avoid violating the principle and keeping the dependencies low, it is possible to delegate the request to the object directly without the need to call methods on a returned object:
This however, implies that the object provides this method for us.
While the principle reduces dependencies between objects and maintenance, one disadvantage of this principle is that more “wrapper” classes are written to handle method calls to other components. This can result in increased complexity and development time as well as decreased runtime performance.
The Hollywood Principle. Don't call us, we'll call you.
This principle gives a way to prevent “dependency rot”, which happens when using interweaving dependencies between high- and low-level components. The principle guides us to put decision making in high-level modules that can decide how and when to call low-level modules. With the Hollywood Principle, we allow low-level components to hook themselves into a system, but the high-level components determine when they are needed, and how. The low-level never call a high-level component directly. It gives a technique for creating designs that allow low-level structures to interoperate while preventing other classes from becoming too dependent on them. A low-level component is allowd to call a method of a high-level component, for example when using an inherited method up in the hierarchy. The goal is to avoid creating explicit circular dependencies between the low-lefvel and the high-level components.
Like the Dependency Inversion Principle, the Hollywood Principle has the goal of decoupling. It provides a technique for building frameworks or components so that lower-level components can be hooked into the computation, but without creating dependencies between the lower-level coponentnts and the hight-level layers.
Single Responsibility A class should have only one reason to change.
Every responsibility of a class is an area of potential change. More than one responsibility means more than one area of change. This principle guides us to keep each class to a single responsibility. Cohesion is a term used to as a measure of how closely a class or a module supports a single purpose or responsibility. Cohesion is a more general concept than the Single Responsibility Principle, but the two are closely related. Classes that adhere to the principle tend to have high cohesion and are more maintainable than classes that take on multiple responsibilities and have low cohesion.