The easiest way to learn design patterns, part 2
In the previous post, we started discussing the problems design patterns can solve. This problem-first approach makes learning design patterns much easier than the conventional method.
Typically, when design patterns are taught, the learner is introduced to its structure and code examples (that are often vague and not relatable to any real-world problems). This is why many software developers find it hard to remember such patterns. The code simply looks over-engineered for no reason and it’s hard to relate it to any real problem.
A much more effective approach is to describe the types of problems first, describe what design pattern can solve such problems, and only then go through the structure of each design pattern. If the learner has a good idea of why each design pattern is used, it would now make much sense why the code is structured the way it is. It will be easier to remember too.
I have written a whole book based on this methodology, which I called The Easiest Way to Learn Design Patterns. The book received a lot of positive feedback and this is what prompted me to share with you the most important section of it in the form of online articles.
So, let’s continue going through the list of problems design patterns can solve.
Third-party components aren’t directly compatible with your code
Imagine that you are working with a third-party library that returns data in a specific format. The problem is, however, that this is not the data format that the rest of your application works with.
For example, your code may only be working with JSON, while the third-party library only delivers data as XML. In this situation, the last thing that you would want to do is rewrite the rest of your code to make it compatible with XML, especially if you don’t expect anything other than this library to use XML.
There is also another variety of this problem that you may encounter. Once again, you are dealing with a third-party library, but this time, its accessible interface is written differently from how the set of related functionality is written in your application. If this third-party library is meant to be able to replace your own components, ideally it should use the same signatures on its accessible members as your own classes are using. Once again, in this case, the last thing you want to do is include special cases into what was meant to be generic code.
It doesn’t necessarily have to be a third-party library though. It could be your own legacy code that was written a while ago, when either a different philosophy was implemented or modern best practices weren’t properly followed. In this case, you cannot just change the legacy components, as it has already been extensively tested and many other components within the system rely on it.
All these examples have one thing in common – you need to work with something that is structurally different from the rest of your code, but making changes to your own code is not desirable.
Suitable design patterns
Adapter
You can think of an Adapter as a wrapper class for whatever component you would want to use in your code that is currently incompatible with it. The Adapter class will be accessible by using exactly the same interfaces that are normally used in your code, while internally it will be calling methods on the external component. If it needs to transfer the data, it will do so in a format that is compatible with the rest of your code base.
Basically, the Adapter class is analogous to a real-life physical adapter, such as an electric socket plug adapter. As you may know, the US, UK, and continental Europe use different electric sockets. So if you are traveling from the US to Europe, you won’t be able to just plug your electronic devices in. You will have to get an adapter that has the same input configuration as a wall socket in the US but has the same output pin configuration as a European socket plug.
Why would you use Adapter
Adapter allows you to isolate interface conversion functionality to only a single place in your code.
Open-closed principle is enforced
The access point into the immutable external component is standardized along with the rest of your code.
Adding new functionality to existing objects that cannot be modified
Imagine that you have the following situation. There is either some third party library, or your own legacy code that you need to use. And you need to make some changes to its functionality. You would either need to modify the existing behavior, or add some new behavior. But you can’t change the components that you are about to use. This is because you either don’t have access to the internal code of those components, or simply aren’t allowed to make this change.
Or maybe the external component is not fully compatible with your code, so you will need to both make it compatible and then extend it. However, making the component compatible with your code is a different problem that we have already covered. The specific problem that we are looking at now is the ability to extend the behavior of the objects that you can’t modify directly.
Suitable design patterns
Decorator
Decorator is a design pattern that is similar to Adapter. Just like with Adapter, the Decorator class acts as a wrapper around the original object. However, instead of changing the interface of the original object, it uses the same interface as the existing object. The API will be exactly the same.
You can think of a silencer on a pistol to be analogous to the Decorator design pattern. The original pistol already has a specific configuration that you cannot change. But attaching a silencer to it makes it behave differently. While the silencer is attached, however, the original pistol still remains intact. The silencer can be removed at any point.
So, when you apply the Decorator design pattern, anything in your code that could previously use the original object would still be able to use the Decorator class in its place. And this is precisely because the original structure of the interface was left intact.
Why would you use Decorator
It will give you the ability to easily add functionality to those objects that you can’t modify directly.
Open-closed principle is enforced.
You can use it recursively by applying additional decorators on top of the existing ones.
You can dynamically add responsibilities to an object at runtime.
The single responsibility principle is applied well, as each decorator can be made responsible for only a single enhanced functionality
Accessing complex back-end logic from the presentation layer
Imagine that you are dealing with a whole range of complex classes, all of which you would need to access from a single layer of your application. It could be, for example, code that has been auto-generated from a WSDL definition. Or it could be some manually written code where different classes retrieve data from different data sources.
This scenario is very typical, as it uses the commonly used multi-layer architecture. In such architecture, there would be a separate layer responsible for presentation, a separate layer responsible for back-end business logic, and a separate layer responsible for data storage. But in real-life applications, it often isn't as simple as this. The application may be retrieving its data from multiple sources, such as several different database types and external services.
Because you will need to access all of these classes from the same layer within your application, using those classes directly would probably not be the most optimal thing to do. You would need to refer to all of these classes from the other system components that need to access them.
This is especially problematic with auto-generated classes, as you won’t be able to easily create abstractions for them. So, you will either have to manually edit auto-generated code (which isn’t a good idea) or pass the concrete classes as references to the components that need them (which is also a bad idea and a clear violation of the dependency inversion principle).
The problem will become even more apparent if those complex classes are meant to be updated fairly frequently. In this case, you will have to keep updating all the references to them.
Another problem associated with complex logic is operations that are expensive to run. But what if you need to access the results of such operations frequently? Well, luckily, there are design patterns that can help you solve this problem.
Suitable design patterns
Facade
Facade is a class that controls access to a set of complex objects and makes it simple. Most often, just like an Adapter, it would change the access interface into these classes. However, unlike Adapter, it will usually be a wrapper around several of such classes. Or it may be responsible for a diverse set of interactions between a number of moving system parts rather than just converting one type of a call to another type.
Direct interaction with those complex classes happens only inside the Facade class, so the other system components are completely shielded from it. All the client class will be concerned about is calling the specific methods on the Facade. The client doesn’t care how Facade delivers what it needs. It only cares that Facade does the job that is expected of it.
When any of the above-mentioned components need to be updated, the update in the logic will only need to happen inside the Facade, which will prevent the other parts of the system from having bugs unintentionally introduced into them by forgetting to update the logic.
Why would you use Facade
A convenient way of simplifying access to a complex subset of the system.
All other application components are shielded away from having to interact with complex logic.
Since all the code that interacts with a complex subsystem is located in one place, it’s easier not to miss updates to the logic if any of the subsystem components get updated.
Proxy
Proxy is not suitable for wrapping complex logic into a simple accessible interface, but it’s still suitable for dealing with subsystem components that are not easy to work with directly.
For example, you may be in a situation where you would only need data from a particular service on rare occasions. If this data takes a long time to obtain and rarely changes, a proxy can be used to access this data once and store it in memory until it changes.
Or you may have a situation where you would need to restrict access to a particular service based either on a specific outcome of business logic or the roles that the user is assigned to. In this case, the proxy would conduct this check before the actual service is accessed.
So, essentially, a Proxy is nothing other than a wrapper around a class that has exactly the same access interface as the original class, so both classes are interchangeable. The Proxy class is there to restrict access to the original class. It can also be used to implement any pre-processing of the request if it’s needed before the original class can be accessed. A Proxy can implement additional access rules that cannot be added to the original class directly.
Why would you use Proxy
You can abstract away all complex implementation details of accessing a particular class.
You can apply additional request validation before you access a particular class.
You can get it only to access the actual class when it’s necessary, which would positively affect the performance.
User interface and business logic are developed separately
You have two separate teams working on the application. One team consists of front-end specialists, who are capable of making a really beautiful user interface. The other team isn’t as good at building user interfaces, but it’s really good at writing business logic.
Also, what you intend to do is make the user interface compatible with several different types of back-ends. Perhaps, the user interface is built by using a technology that can run on any operating system, such as Electron, while there are different versions of back-end components available for different operating systems.
Suitable design patterns
Bridge
Bridge is the design pattern that was developed specifically to solve this problem. It is a way of developing two parts of an application independently of each other.
When Bridge is used, the UI part of the application is known as the interface, while the back-end business logic part of the application is known as the implementation. This is not to be confused with interface and implementation as object-oriented programming concepts. In this case, both the user interface and the back end would have various interfaces and concrete classes that implement them. So, what is known as the interface doesn’t only consist of interfaces, and what is known as implementation doesn’t exclusively consist of concrete classes.
Usually, Bridge is designed up-front and developers agree how the interface and implementation are to communicate with one another. After that, both of these components can be developed independently. One team will focus on business logic, while another team will focus on usability.
With this design, implementations can be swapped. So, as long as the access points of the implementation are what the interface expects them to be, any implementation can be used.
Using different implementations for different operating systems or data storage technologies is one of the examples. However, you can also develop a very simple implementation with faked data for the sole purpose of testing the user interface.
Why would you use Bridge
You can develop the user interface and business logic independently.
You can easily plug the user interface into a different back-end with the same access point signatures.
Those who are working on the user interface don’t have to know the implementation details of the business logic.
Open-closed principle is enforced.
The single responsibility principle is well implemented.
Facade
Facade, which we had a look at in ___chapter 12___, can be used in certain circumstances, although it's often less suitable than Bridge.
For example, imagine that you have to access the back end of the app via WSDL, which would have some auto-generated code associated with it. This is where a Facade class would be helpful, as it will abstract away all complex implementation details of this communication mechanism.
This is applicable to scenarios where the business logic layer is hosted by a third party. Likewise, if, for whatever reason, the business logic and the UI applications cannot be designed together up-front, it also might be a suitable scenario to use Facade. This is especially true when the back-end business logic application has a complex access API.
But if you don’t have to deal with the complex contracts between front-end and back-end components, then facade is not the most useful design pattern to solve this specific problem.
Why would you use Facade instead of Bridge
Easier to implement when the service with the business logic is hosted by a third party.
Easier to implement when the interface and the implementation cannot be designed upfront.
Proxy
Proxy design pattern that we had a look at in ___chapter 12___ is useful if your application is relatively simple.
Essentially, you may have some back-end classes and their simplified representations that the UI components will interact with. In this case, you may have interchangeable implementations of back-end classes that the same proxies can deal with.
As Proxy delivers results without necessarily relying on the object it is providing an abstraction for, it's especially useful in situations where the back-end service with the business logic is expected to be modified and redeployed frequently. This way, the UI would still be fully operational even while the back-end is being redeployed.
Likewise, as the main purpose of Proxy is to deliver results to the client without having to trigger the actual business logic each time, it's a very useful design pattern to implement in a situation where triggering the actual business logic is computationally expensive.
Why would you use Proxy instead of Bridge
No outage of user-accessible functionality during back-end re-deployment.
Much better performance when the business logic is computationally expensive.
Building a complex object hierarchy
Imagine that you need to construct a structure where objects need to act as containers for similar objects. It could be that you are building a tree-like data structure. Or perhaps you are building a representation of a file storage system where folders can contain files and other folders.
Suitable design patterns
Composite
Composite is a design pattern that allows you to build a tree-like structure in an efficient way.
There are two types of classes involved – a leaf class and a composite class. Both of these classes implement the same interface; however, a composite class would have more methods on it.
A leaf class is the simplest class in the structure. It cannot have any additional members. A composite class, on the other hand, can have a collection of members, which can be absolutely any type that implements the interface that both leaf and composite classes implement.
Essentially, a composite class can contain other instances of composite class and leaf instances. Therefore, a composite class would have methods that would allow it to manipulate the internal collection of its children, such as `Add`, `Remove`, etc.
The best analogy would be a file storage structure. You can think of files as leaf objects and folders as composite objects. A folder can contain other folders and files, while a file is a stand-alone object that cannot contain any other objects.
Why would you use Composite
Really easy way of building any tree-like structures.
Different types of members of the tree structure are easily distinguishable.
Implementing complex conditional logic
So, you need to implement either a switch statement with several cases or an if-else logic with several conditions.
A traditional way of dealing with this is just to implement specific code under each condition. However, if you are dealing with complex conditional logic, your code will become harder to read and maintain.
If you have this conditional logic inside a single method, it will probably become quite difficult to write unit tests for this method. Your code will probably violate SOLID principles, as the method will have more than one responsibility. It will be responsible for both making conditional decisions and executing different types of logic based on those decisions. You may end up having as many responsibilities inside this method as there are conditions in your statement!
Suitable design patterns
Strategy
Strategy is a design pattern where you have a container object, usually referred to as Context, that contains a specific interface inside of it. Strategy is any class that implements this interface.
The exact implementation of a Strategy object inside the Context object is assigned dynamically. So, the Context object allows you to replace the exact implementation of the Strategy object at any time.
Strategy object would usually have one core action method that is defined on the interface and the Context object will have a wrapper method with a similar signature that will call this method on the current Strategy implementation.
So, what you would normally do is write several Strategy implementations, each of which would contain its own version of the method that executes the action. Then, when you need to have any multi-condition logic, all you will do under each condition is pass a particular implementation of the Strategy into the Context class. In the end, you just execute the action method on the Context class.
If you do this, all that your method with the conditional logic will be responsible for is deciding which Strategy to pass into the Context class. And it will be trivial to validate this behavior with unit tests. Then, each Strategy implementation will be solely responsible for its own specific version of the logic that is to be executed, which is also easy to read and write unit tests against.
Why would you use Strategy
A really easy way of isolating specific conditional behavior into its own method.
Helps to enforce the single responsibility principle by making the code with the conditional statements solely responsible for outlining the conditions.
It becomes easy to write unit tests against any code with complex conditional logic.
Factory Method
Factory method, which we already had a look at in chapter ___chapter 6___, is also designed to be used in a complex conditional logic, but its usage is different from the usage of Strategy.
Factory Method allows you to generate any object of a particular type and, just like with Strategy, you can decide on the exact implementation of the Factory Method based on conditions. However, there is one subtle, but significant, difference.
Strategy is used for executing a particular behavior or returning a short-lived simple data object, while the Factory Method is used for generating a long-lived object that can execute its own logic outside of the Factory Method.
For example, let’s imagine that you have two databases – the main database and the archive database. They both store data in exactly the same format, while the data itself is different.
If you would need to decide which database to return the data from at the request time, you would use Strategy. For example, a Boolean value that determines whether or not to get the data from the archive is defined as a part of the request. Then you would select a Strategy implementation depending on whether that value is set to true or false.
If true, you would use the implementation that gets the data from the archive database. If false, you would get the implementation that retrieves the data from the main database. But whichever database the data comes from, it's the same data structure in the code that represents the data.
Now, imagine that you need to decide whether or not to use the archive database at config time. This way, you would have an abstraction that represents a database connection. When you launch the application, a Factory Method will assign the representation of either the main or the archive database to this abstraction depending on the config value. After that, any request will retrieve the data from whichever database was set up by this logic when the application was first launched.
The differences between Factory Method and Strategy
Strategy is used for conditional execution of a specific action that may involve the retrieval of relatively simple data objects.
Factory Method is used for conditionally creating long-lived objects that have their own logic.
Abstract factory
Because Abstract Factory is nothing other than a collection of related Factory Methods, it is applicable in the same way as the Factory Method is, but only if you need to obtain a group of related objects rather than a single object.
Wrapping up
Next week, we will go through the third and final articles describing the problems that software engineers commonly face and which design patterns can solve each of these problems. If you want to have a more in-depth look at design patterns, including code samples in C# for both design patterns and SOLID principles, you can find them in my 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.