Object-Oriented Design Patterns explained using practical examples
We have a look at the 23 Gang of Four design patterns for object oriented software design. While a lot of blog posts describe them using examples around concepts like Animal, Car or Pizza, we explain each of them by a practical example. It is certainly not wrong to learn design patterns with the help of real world analogies and then be able to apply them to software design problems. But it might also help others to see how they are applied in specific software design problems.
These type of pattern support the creation of objects. Because in certain situations there are more elegant ways than using the new operator.
The Abstract Factory provides an interface for creating families of related or dependent objects without the need to specify their concrete classes. In the example below the ShapeFactory can be used to create objects based on the String shapeType. So, we do not need to specify the concrete class, but only need to pass the type string. This pattern uses inheritance to define the factories that create objects.
The Factory Method defines an interface for object creation but let’s the subclass decide which object to create. Referring to the code below, a Encryptor object needs an encryption algorithm. Through getEncryptionAlgorithm() it is ensured that the Encryptor actually get’s an EncryptorAlgorithm. However, each Encryptor decides which EncryptionAlgorithm is really used.
In classes that have a lot of fields you oftentimes end up with many constructors as you might need objects using different field combinations. The Builder pattern enables a more readable object creation and let’s you specify the fields that are actually needed. In the example below a DataFetcher object can be created by passing the fields that are needed in a specific case. This also makes the code more readable, while only one constructor is needed.
The prototype pattern helps if objects are expensive to create and new objects will be similar to existing objects. It uses the clone method to duplicate existing instances to be used as a prototype for new instances.
In the example below defined access control objects are retrieved simply by using a key and a copy of the corresponding access control object is returned. This way, objects that are often needed are “created” more easily.
A Singleton ensures that only one instance of an object is created and that this instance is globally accessible. There are not many occasions where it is acceptable to use a Singleton as it introduces global state. Logging is one meaningful example for the Singleton as the information flow happens only in one direction and therefore global state is not a big issue (in a multithread context it becomes a bit more complicated though). The private constructor in the example below ensures that the object creation only happens through getInstance() which at the same time serves as the global accessor.
These type of pattern helps to design relationships between objects.
The Adapter Pattern works between two independent or incompatible interfaces. This is for example useful if third party code is used, but cannot be changed. In the example below, there is an application that basically sorts arrays. This is specified through the Sorter interface. The NumberSorter, a third-party library, though accepts only Lists. Therefore the SortListAdapter is created that accepts arrays, but uses the NumberSorter internally.
The Bridge pattern is used to decouple interfaces from implementations, if there are hierarchies in interfaces as well as implementations. That way the abstraction and implementation are allowed to vary independently.
The DrawAPI and Shape subclasses in the code below are decoupled from each other and can vary independently. The DrawAPI implementation is passed to the Shape implementation at runtime.
The composite pattern allows to treat a group of objects the same way as a single object. This is for example used in tree-like object structures where a parent node’s operation influences or is dependent on child nodes. The code below organises a file folder structure as a tree. The size operation on a folder is propagated to its children which could be a file or another folder. As a client the operation only needs to be called on the parent.
The decorator pattern allows to add functionality to an object at run-time without altering its structure. In the example below there is a simple TextField class. Through decorators additional functionality can be added at runtime for example to add scrolling or borders to the TextField. This is more flexible than deriving classes like ScrollableTextField, BorderTextField, etc.
A Facade simplifies the interface to an object or a group of objects “behind” this facade. This is for example used to harmonise interfaces of third party code or to simplify the interface for the client. In the example below SocialSharing provides an interface to multiple third party code.
The Flyweight pattern is applied if lots of objects from one class need to be constructed. In this case objects are shared to reduce the memory load. As shown below, Line objects in different colours are added to a pool of objects for reuse. If a new line object is requested, it is first checked whether such an object already exists and can be reused, otherwise the object is created and added to the pool for future reuse.
In this pattern an object is a proxy to something else and can control the creation and access of it. The proxy could interface to anything, a large object in memory, file, or other resources. In the example below the RealImage contains all the data of an image, while the ProxyImage is more lightweight and in this example only uses the real image when it needs to be displayed. So, when only using the ProxyImage object, to e.g. showing the url, the memory consumption is reduced compared to the usage of the RealImage where for example every pixel needs to be accessible.
These type of pattern are concerned with the communication between objects. In most cases the dependencies between communicating objects are reduced through these patterns which leads to better software design.
Chain of Responsibility
This pattern creates a chain of receiver objects for a request. It avoids coupling the sender of a request to the receiver and gives multiple objects the chance to handle the request. Receiving objects are linked together. In the example below a chain of middleware units is constructed. The request, containing email and password, is forwarded in the chain. New elements in the chain can be easily added.
In the command pattern an object is used to encapsulate all information needed to perform an action or trigger an event at a later time. As shown below, the ActionOpen defines the action as well as the object which the action is executed on. More information is not needed and another object, here menu, can execute the action.
The Interpreter pattern defines a representation for the grammar of a language and provides the ability to interpret sentences of that language. In the code snippet below a Plus object can interpret “operand1 + operand2” and knows what to do with that expression. More interpreters can be added without changing the structure too much. The Parser then uses interpreters to understand input.
The Iterator is used to traverse a container of data to access the container’s elements without the need to know the underlying structure. Also, new traversal variants can be added without changing the interface of the objects or the data structure itself.
The ChannelSurfer, depicted below, can access channels in consecutive manner. The RemoteControl object does not need to care how the channels are organised. A RandomChannelSurfer, for example, can be added without much problems.
If two or more objects need to cooperate, the Mediator pattern might be applied. Especially if the objects do not know each other, if they should not be tightly coupled, or their interaction is complex, this pattern can help.
In the example below there is some interaction needed between UI elements. This interaction is happening through the AuthenticationDialog as a Mediator. This way the interaction is not defined in one of the participating objects, but extracted into the mediator.
The Memento pattern is useful if a certain state of an object should be saved for later usage. Thereby it does not violate the encapsulation of that object’s implementation details. It can for example be used for the implementation of an undo mechanism.
In the observer pattern observer objects subscribe to an observable object to be notified every time the observable changes its data. Observers are loosely coupled and can be added and removed at run-time.
With regard to how the observer finally gets the data from the observable, there are two variants: push and pull. The code below shows the pull variant, which is more flexible as in this case the observable does not need to know how the observer wants to receive the data, but the observer can fetch the data as wished.
The State pattern lets an object alter its behaviour when its internal state changes. This pattern is similar to the strategy pattern, but in this case it is decided internally how the objects behaves. This is especially helpful if complex conditions define how the object should behave. New states can be added independently from existing states.
In the context of the Strategy pattern there exist multiple variants for one algorithm where one variant is chosen to be executed at runtime.
In the example below a Compressor object can be used with one of two different algorithms defined at run-time. The method that executes the action (createArchive) then applies the chosen algorithm.
The Template pattern defines a structure for sub classes in which steps of an algorithm and their order are defined. This ensures that the sub classes follow the exact same steps, providing better overview and consistency. It also allows to define default implementations for steps that can be overridden by subclasses.
As shown below all the parsers inheriting from DataParser follow the exact same steps when parse() is executed. This ensures for example that all parsers close the files and log the status.
The Visitor pattern allows to apply one or more operation to a set of objects at run-time without having the operations tightly coupled with the object structure.
This let’s you implement double dispatch where a function call to different concrete functions is depending on the run-time type of two objects. In the example below on the one hand side there are route elements and on the other hand side visitor objects that each execute different functions on the route elements. If there are more visitors added in the future, the route element does not need to change.
These examples only provide short overviews for each design pattern and try to provide an example for each of them that shows the applicability in a software engineering context. The examples helped me to understand all the patterns, though better examples might exist. Other resources provide a more detailed description. Important is to recognise the patterns in the code and to use them when applicable and meaningful in own code.