Remember that, at the beginning of the chapter, we discussed dynamic composition that allows you to change the implementation of an object at runtime. The bridge pattern is another structural pattern that allows this.
The bridge pattern decouples an abstraction from its implementation. This means that this pattern separates the implementation of an object from its representation and interface.
Thus, firstly, the implementation can fully be encapsulated, and secondly, the implementation and representation can independently change, without having any constraints between them.
In the class diagram of the bridge pattern, the separation between the abstraction and the implementation is very well represented—notice the left-hand side and right-hand side of the following diagram:
The bridge pattern uses a minimum of four participants:
AbstractClass
represents the domain objects. This class contains the interface used by clients and contains a reference to an object that implements the Implementation
interface.ConcreteClass
is the class that implements the methods defined in the AbstractClass
.ImplementationBase
class defines the method signature of the concrete implementation classes. The methods defined here differ from the methods of the Abstract
class. These two sets of methods are different. Generally, methods of the AbstractClass
are high-level methods, while the methods of the implementation
class are low-level methods.ConcreteImplementationA
(B
…) classes are concrete classes that realize methods introduced in the ImplementationBase
interface.The operation of AbstractClass
and its subclasses invokes the methods defined in the ImplementationBase
interface, which represent the bridge.
We should be able to turn on the light or TV using the same object. Once my code to turn on the light or TV will be implemented in my client, I don't need to modify it if the ConcreteImplementation
structure changes. Using the bridge pattern, I will use an object that inherits from AbstractClass
. This object contains a method that the client will consume. This method doesn't turn on the TV, but it calls the method defined in the ImplementationBase
class; thus, depending on the object that our abstract object uses, it will run the actions that are defined in the ConcreteImplementation
class, which are represented by the TV or the light.
Given the preceding problem, we will first define the method and a property that contains the object we want to manipulate. This object will implement the ImplementationBase
interface, which represents the bridge.
The object that will be manipulated by clients will have a turnOn()
method. This is the only method known by the client:
// IAbstractBridge protocol IAbstractBridge { var concreteImpl: ImplementationBase {get set} func turnOn() }
Now, we will define the ImplementationBase
interface. It contains the run()
method that each ConcreteImplementation
class will implement:
//Bridge protocol ImplementationBase { func run() }
Our interfaces are now ready; we can create the RemoteControl
class that our clients will use. Depending on the object referred in the concreteImpl
property, the turnOn()
method will call the run method of the concreteImpl
object. To obtain a reference to the concreteImpl
object, we will add an argument to the constructor (init
) of the RemoteControl
class:
/* Concrete Abstraction */ class RemoteControl: IAbstractBridge { var concreteImpl: ImplementationBase func turnOn() { self.concreteImpl.run() } init(impl: ImplementationBase) { self.concreteImpl = impl } }
Finally, we implement our ImplementationBase
class for the TV
and Light
classes. A run()
method is needed in each of them. The run()
method contains all the needed logic that will permit you to turn on the light or TV. In our example, we only display a text that indicates the action has been completed:
/* Implementation Classes 1 */ class TV: ImplementationBase { func run() { println("tv turned on"); } } /* Implementation Classes 2 */ class Light: ImplementationBase { func run() { println("light turned on") } }
From the client's perspective, we will use our RemoteControl
abstraction class, pass the final object to the constructor when we want to manipulate (the Light
or TV
class), and call the turnOn()
method of the RemoteControl
object to execute the action:
let tvRemoteControl = RemoteControl(impl: TV()) tvRemoteControl.turnOn() let lightRemoteControl = RemoteControl(impl: Light()) lightRemoteControl.turnOn()
Thanks to Playground, we can now see the live result, which is as follows:
We can see two messages on the right-hand side of our Playground file: tv turned on and light turned on, which means that the run()
method of each final object has been correctly executed.