Anatomy of the bridge pattern

The bridge pattern is oriented around two interfaces:

Instead of implementing the Abstraction interface directly, the bridge pattern encourages the introduction of the Implementor interface, which in turn will be the top-level interface for feature implementation.

Let's consider the following code. It shows a class that performs some work:

class Abstraction {
func start()
func stop()
}

If we want to be able to swap the implementation of start() or stop() with other implementations, it's quite complicated. We could leverage subclassing, but it may be inconvenient, as it forces us to refactor our entire program.

The bridge pattern can help us with this problem. First, let's extract the interface for Abstraction:

protocol AbstractionType {
init(implementor: ImplementorType)

func start()
func stop()
}

extension AbstractionType {
func restart() {
stop()
start()
}
}

It's important to note that each AbstractionType needs to be initialized with an Implementor, which will actually perform the work:

protocol ImplementorType {
func start()
func stop()
}

It is now possible to implement a concrete Abstraction:

class Abstraction: AbstractionType {
private let implementor: ImplementorType

required init(implementor: ImplementorType) {
self.implementor = implementor
}

func start() {
print("Starting")
implementor.start()
}

func stop() {
print("Stopping")
implementor.stop()
}
}

We also need to add more than one Implementor:

class Implementor1: ImplementorType {
func start() {
print("Implementor1.start()")
}

func stop() {
print("Implementor1.stop()")
}
}

class Implementor2: ImplementorType {
func start() {
print("Implementor2.start()")
}

func stop() {
print("Implementor2.stop()")
}
}

We can now benefit from those Abstraction objects that provide different implementations:

var abstraction = Abstraction(implementor: Implementor1())
abstraction.restart()

abstraction = Abstraction(implementor: Implementor2())
abstraction.restart()

The previous code will output the following:

Implementor1.stop()
Implementor1.start()
Implementor2.stop()
Implementor2.start()

With this pattern, we have successfully swapped at runtime the implementation of Abstraction without replacing its code.

Now, let's imagine we want to test that the Abstraction object is performing the restart properly; that is, calling stop() and then start() in order.

Let's write TestImplementor, feed it to the Abstraction object, and see how it goes:

class TestImplementor: ImplementorType {
var stopCalled = false
var startCalled = false
var inProperOrder = false

func start() {
inProperOrder = stopCalled == true && startCalled == false
startCalled = true
print("TestImplementor.start() \(startCalled) \(stopCalled) \(inProperOrder)")
}

func stop() {
inProperOrder = startCalled == false && stopCalled == false
stopCalled = true
print("TestImplementor.stop() \(startCalled) \(stopCalled) \(inProperOrder)")
}
}

With TestImplementor, we can ensure that the logic of our Abstraction object stays sound over timeat least, that the right methods are called in the right orderwithout worrying about the side-effects of the implementation:

let testImplementor = TestImplementor()
abstraction = Abstraction(implementor: testImplementor)
abstraction.restart()

// Check the status of the implementor
assert(testImplementor.inProperOrder)
assert(testImplementor.startCalled)
assert(testImplementor.stopCalled)

The following will also have been logged:

TestImplementor.stop() false true true
TestImplementor.start() true true true

These logs are expected, and the assert() should all pass, as Abstraction is properly implemented.

We've now successfully leveraged the bridge pattern to decouple abstractions and implementation details, as well as providing three different implementations for the same abstraction, one of which was testing.