Design patterns in software development make it easier and much quicker to solve certain types of problems. Instead of reinventing the wheel, you just apply an existing solution.
Knowing them also makes it much easier to understand software architecture patterns, as they rely on the same underlying principles.
The biggest problem with design patterns is that they can be really hard to learn. This is primarily because of how they are taught.
Yes, there are plenty of good resources out there that explain each pattern well. But those patterns are still hard to remember because, while you understand the structure of a pattern, it's not clear at all why you would want to structure your code this way. So, you have to rely on brute-forcing this knowledge into your memory.
I had the same problem when I was learning them. But I also realized that there is a much better way. Why, instead of teaching people the pattern and then letting them figure out how to use them, we start with describing a problem that they can easily associate with, tell them which design patterns can solve this problem and why, and only then show them how those design patterns are structured? This way, the learning becomes easier, as the structure of the patterns will make sense right away.
That's exactly what I did in my book, which I called The easiest way to learn design patterns. The book became a success with over 8,000 copies sold. The feedback was overwhelmingly positive, so it seems I am on the right track with the methodology.
Today, I will start a series of articles that contain the information taken directly from my book. Here, you will learn about the common problems that the design patterns can solve and which design patterns are suitable for each problem and why.
Of course, there are plenty of such problems. Therefore, each article will only cover a handful of them.
So, let’s begin.
Not knowing what object implementations you'll need ahead of time
Imagine that you have a code that uses a particular object type. The code that uses this object only needs to know the signatures of accessible methods and properties of the object. It doesn’t care about the implementation details. Therefore, using an interface that represents the object rather than a concrete implementation of the actual object is desirable.
If you know what concrete implementation your application will be using ahead of time, you can simply apply the dependency inversion principle along with some simple dependency injection. In this case, you will know that it’s always a particular implementation of the interface that will be used throughout your working code and you will only replace it with mocked objects for unit testing. But what if the concrete implementation of the interface needs to be chosen dynamically based on runtime conditions? Or what if the shape of the object needs to be defined based on input parameters in the actual place where the object is about to be used? In either of these cases, the dependency injection approach wouldn’t work.
Let’s look at an example of this. Imagine that you are building an application that can play audio. The application should be able to work on both Windows and Linux. And inside it, you have an interface called IPlayer
that has standard methods that an audio player would have, such as Play
, Pause
, and Stop
. It is the implementation of this interface that actually interacts with the operating system and plays the audio.
The problem is that Windows and Linux have completely different audio architectures; therefore you cannot just have a single concrete implementation of the IPlayer
interface that would work on both operating systems. And you won’t know ahead of time what operating system your application will run on.
Suitable design patterns
Factory Method
Factory Method is a method that returns a type of object that implements a particular interface. This method belongs to a Creator object that manages the lifecycle of the object that is to be returned.
Normally, you would have several variations of the Creator object, each returning a specific implementation of the object that you want. You would instantiate a specific variation of the Creator object based on a specific condition and would then call its Factory Method to obtain the implementation of the actual object.
In our example above, you would have one version of the Creator object that returns a Windows implementation of IPlayer
and another version that returns its Linux implementation. The Creator object will also be responsible for initializing all dependencies that your IPlayer
implementation needs. Some code blocks in your application will check which operating system it runs on and will initialize the corresponding version of the Creator object.
Why would you want to use Factory Method
Why bother using Factory Method, if you can just initialize any objects directly? Well, here is why:
Good use of the single responsibility principle. The Creator object is solely responsible for creating only one specific implementation type of the end object and nothing else.
The pattern has been prescribed in such a way that it makes it easy to extend the functionality of the output object and not violate the open-closed principle.
Easy to write unit tests, as Creational logic will be separated from the conditional logic.
Abstract Factory
Abstract Factory is a design pattern that uses multiple Factory Methods inside of the Creator object, so you can create a whole family of related objects based on a particular condition instead of just one object.
Let’s change the above example slightly. Imagine that our app needs to be able to either play audio or video, so you will need to implement two separate interfaces – IAudioPlayer
and IVideoPlayer
. Once again, it must work on either Linux or Windows.
In this case, your Abstract Factory will have a separate method to return an implementation of IAudioPlayer
and a separate method to return an implementation of IVideoPlayer
. You will have a version of the Factory that is specific to Windows and another version that is specific to Linux.
It’s known as Abstract Factory because it either implements an interface or extends an abstract class. It is then up to the concrete implementation of the Factory to create concrete implementations of the output objects that are both relevant to a specific condition.
Builder
Builder design pattern is similar to the Factory Method, but instead of just returning a concrete implementation of an object all at once, it builds the object step-by-step.
Let’s go back to our OS-independent app that plays audio. In the Factory Method example, we had a concrete implementation of the IPlayer
interface for each operating system. However, if we would choose to use Builder instead of a Factory Method, we would have a single concrete implementation of the interface that would act as a shell object. Let’s call it Player
. It will be this type that gets produced in all scenarios, but the concrete parameters and dependencies will be injected into it based on what kind of operating system it’s running on.
For example, both Linux and Windows allow you to play audio and manipulate its volume via the command line. On Linux, it will be Bash Terminal. On Windows, it will be either cmd or PowerShell.
The principles are similar. In both cases, you would be typing commands. But the exact commands will be completely different. Plus there are likely to be separate commands for playing audio files and for manipulating audio volumes.
So, in this case, our Player
class will simply delegate the playback of audio to operating system components that are accessible via a command line interface. It’s only the actual commands that will be different. And this is where a Builder design pattern comes into play.
Builder consists of two main components – Builder and Director. Builder is a class that returns a specific object type. But it also has a number of methods that modify this object type before it gets returned. The director is a class that has a number of methods, each of which accepts a Builder class, calls methods on it with a specific set of parameters, and gets the Builder to return the finished object.
So, in our case, imagine that our Builder class for the Player
object (which we will call PlayerBuilder
) has the following methods: SetPlaybackInterface
(that accepts a playback interface instance), SetAudioVolumeInterface
(that accepts audio volume interface instance) and BuildPlayer
(that doesn't accept any parameters). When we instantiate our PlayerBuilder
class, we instantiate a private instance of the Player
class inside of it. Then, by executing SetPlaybackInterface
and SetAudioVolumeInterface
methods, we dynamically add the required dependencies to the instance of our Player
class. And finally, by executing the BuildPlayer
method, we are returning a complete instance of a Player
object.
In this case, our Director class will have two methods, both of which accept PlayerBuilder
as the parameter: BuildWindowsPlayer
and BuildLinuxPlayer
. Both of these methods will call all the methods on the PlayerBuilder
class in the same order and both will return an instance of a Player
class. But in the first case, the methods will be called with Windows-specific abstractions of the command line interface, it's the Linux-specific abstractions that would be injected into the Player
instance.
However, unlike either Abstract Factory or Factory Method, Builder is not only used to build an object the implementation details of which can only be made known at runtime. It is also used to gradually build complex objects.
For example, .NET has an inbuilt StringBuilder
class in its core System
library. This class is used for building a string from several sub-strings, so you don’t have to just keep replacing an immutable string in the same variable.
Why would you want to use Builder
Good use of the single responsibility principle. Each Builder method has its own very specific role and there is a single method on the Director class per each condition.
Easy to write unit tests. This is facilitated by the single responsibility principle.
No need to have different versions of a class if only some of its implementation details may change in different circumstances.
Because an object can be built step-by-step, it is the best design pattern to decide what the object will be if you have to adjust its properties one by one by multiple steps of conditional logic.
You can reuse the same code for different representations of the final object.
Making several exact copies of a complex object
Imagine that you have an object that you need to copy multiple times. Because it’s not a primitive type, you cannot just copy the value of the whole thing into a new variable of the same type. Instead, you would need to instantiate a new object of this type and then copy the value of every field from the original object instance.
That would be fine for simple objects with a relatively small number of fields that contain primitive types, such as integers and Booleans. But what if you are dealing with complex objects with many fields, some of which contain other objects? And what if such an object contains private fields that you also want to be copied, but can't access?
In this case, you would need to write a complex code to make a copy of an object. For private fields in particular, you may also need to run additional logic to instantiate them. This way of doing things adds complexity; therefore it makes the code less readable and vulnerable to errors. Plus, as you will have no direct access to private members of the object, you may not necessarily end up with an exact copy and suffer some undesirable side-effects as a result.
Suitable design patterns
Prototype
Prototype is a design pattern that was created specifically for this problem.
If you have a complex object that is meant to be copied often, you can make it cloneable. And this is exactly what this design pattern enables you to do.
You would have an interface with a method that produces an instance of an object that implements this interface. Usually, such a method will be called Clone
. And, if you want to make your object cloneable, you just implement this interface when you define the object type.
Inside this method, you will still need to copy the value of every field into the output object. However, this time, you will only have a single place in the entire code base to do it in – the object itself. So, it will be easier to have a close look at it and consider all edge cases. Likewise, you will have full access to the private members of the object, so your copy will be exact.
Why would you want to use Prototype
All code to copy complex objects is located in one place.
You can copy private members of the object.
Any code that needs to generate a copy of the object will only need to call the
Clone
method without having to worry about the details of the cloning process.
Using many instances of an object while keeping code running smoothly
Imagine that you have a requirement to use many similar objects in your application. Perhaps, you are building a distributed application based on microservices architecture and each of these objects represents a unique connection to one of your service instances. Or maybe you are interacting with multiple database entries and, for each one of them, you need an object that represents a database connection.
In this situation, purely using inbuilt language features will probably be problematic. You will need to instantiate every single one of these objects. Once the object is out of scope, your runtime will need to get rid of it to free up the memory. Once you need a similar object again, you will instantiate it again. And so on.
If you follow this approach, you will probably experience a performance hit. Instantiating a new object each time you need to use it is a relatively expensive process. The runtime will need to allocate memory for it and populate those chunks of memory with new values.
If you are using similar objects in different parts of the application, you will be required to allocate sufficient memory to each instance of the object. And that may become quite a lot of memory if you need to use many of such instances.
Likewise, when you are instantiating a new object, there is always a cost associated with running the constructor of the object’s data type. The more complex the constructor logic is, the bigger the performance hit you will get.
Finally, when you are no longer using an instance of the object and it gets out of scope, there will be a performance cost associated with garbage collection. Remember that the memory will not be freed straight away. It will still be occupied until the garbage collector has found your object and identified that it is no longer being referenced anywhere.
The latter, of course, doesn’t apply to the languages that don’t have an in-built garbage collector. However, in this case, you will have to free the memory yourself if you don’t want to introduce a memory leak. So, if you are using one of such languages, like C++ or Rust, you will have an additional issue in your code to worry about.
Suitable design patterns
Object Pool
Object pool is a pattern that allows you to reuse the object instances, so you won’t have to keep instantiating new objects every time.
You have a single Pool object that stores multiple instances of instantiated objects of a specific type. If any of these objects aren’t being used, they are stored inside the Pool. Once something in the code requests an object, it becomes unavailable to any other parts of the code. Once the caller has finished with the object, it gets returned to the Pool, so it can be reused by other callers.
Initially, you will still need to instantiate objects in the Pool. However, when your Pool grows to a reasonable size, you will be instantiating new objects less and less, as there will be a greater chance of finding objects in it that have already been released by their respective callers.
To ensure that the Pool doesn’t grow too big, there will be a property that will limit its size. Likewise, a mechanism inside of it will determine which object instances to get rid of and which ones to keep. For example, you don’t need an object pool with 1,000 object instances if your application will only ever use 10.
Why would you want to use Object Pool
Objects are being reused, so you will not get any performance penalty associated with instantiating new objects.
Object pool maintains its size as needed, so you will not end up with way more object instances than you would ever expect to use.
Flyweight
This design pattern allows multiple objects to share parts of their state. So, for example, if you have a thousand objects, all of which currently have the same values in some of their attributes, this set of attributes will be moved to a separate object and all thousand instances will be referring to the same instance of it.
This allows you to store way more objects in the memory than you would have been able to otherwise. In the case above, instead of having a thousand instances of a particular object type with all their property values in each, you would have a thousand basic skeleton objects, each of which occupies only a tiny amount of space in memory. The remaining properties of these objects will be stored in memory only once.
One disadvantage of flyweight, however, is that it makes your code complicated. You will need to decide which parts of the state are shared and which aren’t. Also, you will need to do some thinking on the best way of changing the state once it becomes irrelevant to any specific instance of an object.
Based on this, it’s recommended to only use flyweight if you absolutely must support systems where the performance of your code would very noticeably decrease otherwise.
Why would you want to use Flyweight
Squeezing way more information into memory than you would have been able to otherwise.
Prototype
Prototype, which we have already covered, can also help to solve this problem. However, unlike Object Pool, it will not give you any performance benefits.
What Prototype will give you in this situation is the ability to create many similar objects without having to go through a complex process of defining their field values each time.
For example, you may have a service that communicates with several instances of the same microservice, and each of these instances is presented by an object in the code. Let’s say that all microservices are accessed by the same IP address, but a different port. The rest of the connection parameters are also identical.
In this case, your configuration file may contain all shared connection parameters. And you will only have to go through the configuration once to create the prototype object. After that, to create any new connection objects, all you’ll have to do is clone the existing one and change the port number accordingly.
To gain performance benefits, Prototype can be combined with Object Pool. Prototype is especially useful when the objects in the Object Pool are complex.
Why would you use Prototype alongside Object Pool
The objects in the Pool are much easier to instantiate, as they can now be cloned.
Using the same single instance of an object throughout the application
Imagine that you have a requirement to use exactly the same object instance throughout your entire application, regardless of whether the classes that use it can communicate directly with each other or not.
One thing you can do is instantiate it in one place and then pass the instance to every single class. This will work, but it’s less than desirable. It means that you will have to write a lot of extra code that will be hard to read and maintain.
Plus, there is nothing that would stop passing a different object reference into a class that uses it, especially if a developer who is currently working on the code doesn’t know that the original intention was to keep the reference the same across the board. This will probably introduce some bugs.
Suitable design patterns
Singleton
With this design pattern, the class that you want to have a single instance of will have a static method that will return an instance of this class. Behind the scenes, this method will call a private constructor the first time you call it, so the class will be instantiated only once. If you call this method again, then the same instance will be returned as was instantiated before.
Otherwise, the class will have no public constructor, so it will not be possible to create a new instance of it by any external code. This will guarantee that no matter where you are calling the static method from, exactly the same instance of the class will be returned.
Why would you want to use Singleton
Being able to use the same instance of an object in any part of your application without explicitly passing an instantiated object.
It will prevent you from creating more than one instance of a particular type.
Object Pool
We have already covered Object Pool as a design pattern primarily intended for managing multiple instances of the same object type without incurring much of a performance penalty. However, in order to be accessible throughout the application, the Object Pool also needs to be a singleton.
So, in a nutshell, to make the most of the Object Pool, the class that acts as an Object Pool must also implement the Singleton design pattern.
Why would you use Object Pool as Singleton
Your entire application shares the same pool of objects.
You don't have to instantiate more object instances than strictly needed.
Wrapping Up
In this article, we covered four common problems that design patterns were invented to solve. We didn’t go into the details of the actual design patterns. However, knowing what problems each design pattern can solve will make it much easier for you to make sense of its structure once you start looking at it.
Next week, we will continue exploring the problems that design patterns can solve. If you don’t want to wait that long (or if you also want to have a novel look at the design patterns themselves), you can get your copy of the book.
P.S. If you want me to help you improve your software development skills, you can check out my courses and my books. You can also book me for one-on-one mentorship.
Thanks for your Article