The Bridge pattern: how to transform classes with multiple aspects in order to comply the Single Responsibility Principle.

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

Non è stato fornito nessun testo alternativo per questa immagine
Figure 1: Number of class combinations grows in geometric progression.


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.


Non è stato fornito nessun testo alternativo per questa immagine
Figure 2:We can prevent the explosion of a class hierarchy by transforming it into several related hierarchies.

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.

Non è stato fornito nessun testo alternativo per questa immagine
Figure 3: bridge implementation code for 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:

Non è stato fornito nessun testo alternativo per questa immagine
Figure 4: pattern bridge structure

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.

Non è stato fornito nessun testo alternativo per questa immagine
Figure 5: universal remote control.

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.

Non è stato fornito nessun testo alternativo per questa immagine
Figure 6: 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.

Non è stato fornito nessun testo alternativo per questa immagine
Figure 7: TVDevice and DVDPlayerDevice classes

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.

Non è stato fornito nessun testo alternativo per questa immagine
Figure 8: RemoteButton class is the Abstraction of bridge pattern structure.

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.

Non è stato fornito nessun testo alternativo per questa immagine
Figure 9: RedefinedAbstraction classes: TVRemoteMute and DVDRemote

And here is the unit test showing how the developed code works correctly.

Non è stato fornito nessun testo alternativo per questa immagine
Figure 10: Unit test for Entertainment Devices Remote Controls : code and output console.


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).

Non è stato fornito nessun testo alternativo per questa immagine

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.


Non è stato fornito nessun testo alternativo per questa immagine
Figure 11: multiplatform GUI framework class diagram.

So, below we can see an implementation, only conceptual, in C++ language for the GUI framework multiplatform.

Non è stato fornito nessun testo alternativo per questa immagine
Figure 12: multiplatform GUI framework : conceptual code

And here is the unit test showing how the developed code works correctly

Non è stato fornito nessun testo alternativo per questa immagine

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 :

  1. Identify the orthogonal dimensions in our classes.
  2. See what operations the client needs and define them in the base abstraction class.
  3. Determine the operations available on all Implementors. Declare the ones that the abstraction needs in the general implementation interface.
  4. For all implementors in our domain create we have to concrete implementation classes, but we have to make sure they all follow the implementation interface.
  5. Inside the abstraction class, we have to add a reference field for the implementation type. The abstraction delegates most of the work to the implementation object that’s referenced in that field.
  6. If we have several variants of high-level logic, we have to create refined abstractions for each variant by extending the base abstraction class.
  7. The client code should pass an implementation object to the abstraction’s constructor to associate one with the other. After that, the client can forget about the implementation and work only with the abstraction object.


And finally here are a couple of suggestions on using the bridge pattern:

  1. We have to use Bridge Pattern when we need to extend a class into multiple orthogonal (independent) dimensions. The Bridge suggests extracting a separate class hierarchy for each of the dimensions. The original class will delegate related work to objects belonging to those hierarchies instead of doing everything itself.
  2. Also we have to use Bridge Pattern if we need to be able to switch implementation at runtime, because Bridge pattern allows us to easily replace the implementation object within the abstraction at runtime too, like assign new value to a field.

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/".?

要查看或添加评论,请登录

社区洞察

其他会员也浏览了