The Bridge pattern: how to transform classes with multiple aspects in order to comply the Single Responsibility Principle.
In this article we continue to talk about structural design patterns: this time we will look at the?"Bridge?Pattern"?(.1).
The "Bridge Pattern" is a structural pattern that lets us split a class with multiple aspects into two separate hierarchies, abstraction and implementation, which can be developed independently of each other
Let's give a simple example to clarify the scenario.
Suppose we have a Shape class with a couple of subclasses: Circle and Square. We want to extend this class hierarchy to incorporate colors, so we plan to subclass red and blue shapes. However, since we already have two subclasses, we should create four combinations of classes (BlueCircle, BlueSquare, RedCircle, RedSquare). Adding new types of shapes and colors to the hierarchy will make it grow exponentially. For example, in order to add a triangular shape you would introduce two subclasses, one for each color. Also, adding a new color would require creating three subclasses, one for each shape type. The further we go, the worse it becomes.
This problem occurs because we are trying to extend Shape classes in two independent dimensions: by shape and by colour. This is a very common problem with class inheritance
The Bridge pattern attempts to solve this problem by switching from inheritance to object composition. This means that we extract one of the dimensions into a separate class hierarchy, so that the original classes refer to an object of the new hierarchy, instead of having all of its states and behaviors within one class.
Following this approach, we can extract color-related code into its own class with two subclasses: Red and Blue . The Shape class then gets a reference field that points to one of the color objects. Now the shape can delegate any color-related work to the linked color object. This reference will act as a bridge between the Shape and Color classes. From now on, adding new colors won't require changing the shape hierarchy and vice versa.
Below is the Kotlin implementation of this example of colored shapes and its unit test.
let us now try to generalize this solution. The class diagram of the bridge design pattern will look like the following:
The following participants take part in this design pattern:
-) The Abstraction (Shape) provides high-level control logic. It maintains a reference to an object of type Implementor and relies on the implementor object to do the actual low-level work.
-) The Refined Abstractions (Circle, Square) provide variants of control logic. Like their parent, they work with different implementations via the general implementation interface.
-) The Implementation (Color) declares the interface that’s common for all concrete implementations. An abstraction can only communicate with an implementation object via methods that are declared here. This interface doesn't have to correspond exactly to Abstraction's interface; in fact, the two interfaces can be quite different. Typically the Implementor interface provides only primitive operations, and Abstraction defines higher-level operations based on these primitives.
-) Concrete Implementations (Red, Blue) contain specific code.
-) Usually, the Client is only interested in working with the abstraction. Abstraction forwards client requests to its Implementor object.
This structure allows the Abstraction and the Implementation to be developed independently and the client code can access only the Abstraction part without being concerned about the Implementation part.??The abstraction is an interface or abstract class and the implementer is also an interface or abstract class.??The abstraction contains a reference to the implementer. Children of the abstraction are referred to as refined abstractions, and children of the implementer are concrete implementers. Since we can change the reference to the implementer in the abstraction, we are able to change the abstraction’s implementer at run-time. Changes to the implementer do not affect client code.?It decreases the coupling between class abstraction and its implementation fulfils the Single Responsibility Principle (SRP) (.2).
And now let's give one more example, this time less trivial, of applying the bridge pattern.
The problem we want to solve is the following: we want to create a universal remote control that can work with different televisions but also with radios and DVD players. Once the device code is set on the universal remote, the remote must be able to control the device.
With the Bridge Design Pattern we create two layers of abstraction. The first is the Implementor of the Bridge structure that is an abstract class representing different types of devices. The second is the Abstraction element that is an abstract class that will represent different types of remote controls. This allows us to use an infinite variety of devices and remote controllers. We will call the Abstraction as RemoteButton and the Implementor as EntertainmentDevice.
So EntertainmentDevice represents the most abstract of devices that exist and will implement those features that every single device must have. In fact, whether it's a television or a radio or a DVD player, the remote control will have to control the volume, so what we will define in our first abstract class are the buttons that will manage the volume for all our devices. We therefore establish that the seven key will represent high volume and the eight key will represent low volume. We also leave the behavior of keys five and six undefined.
This time we will use the Java language, here is the code for EntertainmentDevice class.
Afterwards we will create more concrete devices (Concrete Implementor), and then we will define the behavior of the five and six keys, which means we will define that for TV the channel number should increase when the six key is pressed or decrease each time five is pressed, instead for the DVD player the six key will skip to the next chapter and the five key will skip to the previous chapter.
Here is an implementation of TVDevice and DVDPlayerDevice.
and now we have to create the second level of abstraction, the one represented in the Bridge structure by Abstraction (fig 4), that is RemoteButtons class, our abstract remote control which must contain a reference to a specific device (TV, Radio or DVDPlayer) and which must declare all those abstract methods that will differ between the various versions of the remote controllers.
now let's create the RedefinedAbstractions classes, that is those classes obtained by defining the abstract methods of RemoteButton. We need to redefine the behavior of the key nine. In fact, there are very specific differences with the key nine: the TV remote each time the key nine is pressed will mute the audio while in the remote control for the DVD player the key nine will pause or restart the content. Below we see the code of our RedefinedAbstraction classes: TVRemoteMute and DVDRemote.
And here is the unit test showing how the developed code works correctly.
As a last example of application of bridge pattern we see a conceptual example of a multiplatform GUI framework. This time we use the C++ language (.3).
Suppose we have to create an application that, properly configured, can work in Windows, Linux and iOS, and can also adapt its GUI to mobile devices or the desktop environment.
领英推荐
Without a proper structure, the code of this app could become a mess, where hundreds of conditionals connect different types of GUI with various APIs all over the code.
If instead we think of the bridge pattern then the abstraction can be represented by a graphical user interface (GUI), and the implementation could be the underlying operating system code (API) which the GUI layer calls in response to user interactions.
Generally speaking, we can extend such an app in two independent directions:
? Have several different GUIs (for instance, for mobile or for desktop)
? Support several different APIs (for example, to be able to launch the app under Windows, Linux, and macOS).
Below we see the class diagram of this implementation.
So, below we can see an implementation, only conceptual, in C++ language for the GUI framework multiplatform.
And here is the unit test showing how the developed code works correctly
After analyzed the structure of the bridge pattern and after examining some examples in the three languages of Android Studio (Kotlin, Java?and C++) ?let's summarize the steps to implements it :
And finally here are a couple of suggestions on using the bridge pattern:
Before concluding, however, let's remind the links of the pattern bridge with the SOLID Principles:
Before concluding, let's see how the bridge pattern behaves with respect to the SOLID principles
-) We can introduce new abstractions and implementations independently from each other so this pattern is comply with Open/Closed Principle (.4).
-) We split a class with multiple aspects into more separate class with a single aspect, so it is comply with Single Responsibility Principle (.2).
But we must be careful of making the code more complicated by applying the model to a highly cohesive class (.5).
That's it for the "Bridge Pattern".
I remind you my newsletter?"Sw Design & Clean Architecture":??https://lnkd.in/eUzYBuEX?where you can find my previous articles and where you can register, if you have not already done, so you will be notified when I publish new articles.
thanks for reading my article, and I hope you have found the topic useful,
Feel free to leave any feedback.
Your feedback is very appreciated.
?Thanks again.
Stefano
?
?
References:
1.?Gamma, Helm, Johnson, Vlissides, “Design Patterns”, Addison Wesley (2° Edition October 2002). pp 175-184.
2.?S. Santilli: "https://www.dhirubhai.net/pulse/single-responsibility-principle-stefano-santilli/".?
3. Alexander Shvets, "Dive Into Design Patterns", Refactoring.Guru, 2021.
4.?S. Santilli: "https://www.dhirubhai.net/pulse/open-closed-principle-stefano-santilli/".?
5.?S. Santilli: "https://www.dhirubhai.net/pulse/cohesion-coupling-stefano-santilli/".?