Swift Solutions: Bridge

Swift Solutions is a series of articles covering design patterns. In each post we discuss what the pattern is, when it applies, and how to implement it in a Swifty way.

Moving away from creational patterns, today we will be covering the Bridge Pattern. It is defined by the Gang of Four as a pattern that “decouples an abstraction from its implementation so that the two can vary independently.” I don’t know about you, but that definition went over my head the first time I read it, so I decided to deconstruct the definition as we progress. By the end of this article, you will have a solid understanding of when this pattern applies, and how to implement it with ease.

Use Case

The Bridge Pattern is used to solve a problem programmers refer to as an “exploding class hierarchy.” Exploding class hierarchies occur when the number of classes increases sharply with additional features.

Since recognizing when to use a pattern is almost as important as learning the pattern itself, we will illustrate a simplified version of the problem, then go through code that demonstrates the problem. Finally, we will cover how to implement the solution.

Understanding the Exploding Class Hierarchy

I want you to draw your attention to the abstract burgers and their concrete implementations as meals. Say we wanted to add a new burger, how many additional classes would you need to create?

If you answered three, you would be correct:

Now imagine if we added a side of salad into the mix. That would require an additional three classes (one for each burger it must be implemented with).

The Issue Over Time

The exploding class hierarchy becomes unmanageable over time. Imagine our app grew to have ten burgers and sides:

  • A single new side would require ten new classes; one implementation subclass under each existing burger
  • A new burger would require eleven classes; one for the new burger, and ten subclasses for each existing side

This sharp increase in classes quickly becomes problematic, and it’s the exact issue the bridge pattern aims to solve.

Exploding Class Hierarchy in Code

Now that we grasp the issue, let’s go through an example in code that demonstrates the problem:

class AudioCommunicationsDevice {
func sendAudio() {
}
}

class CellPhone: AudioCommunicationsDevice {
override func sendAudio() {
// Implement function in subclasses
}
}

class WalkieTalkie: AudioCommunicationsDevice {
override func sendAudio() {
// Implement function in subclasses
}
}

The CellPhone and WalkieTalkie correspond to Hamburger and ChickenBurger from our previous example. Both are meant to be subclassed to implement audio-sending functionality. Let’s add ways to send audio and see what the exploding class hierarchy looks like in code.

class SecureCellPhone: CellPhone {
override func sendAudio() {
// Send encrypted audio
}
}

class SecureWalkieTalkie: WalkieTalkie {
override func sendAudio() {
// Send encrypted audio
}
}

class PlainAudioCellPhone: CellPhone {
override func sendAudio() {
// Send unencrypted audio
}
}

class PlainAudioWalkieTalkie: WalkieTalkie {
override func sendAudio() {
// Send unencrypted audio
}
}

So in this example, we live during a hypothetical time where communication devices were only capable of sending one type of audio. We have devices that send plain audio, and at a premium, some devices send encrypted audio.

Do you see the explosion class hierarchy here? It is subtle, but when we needed to add a new way of sending audio (for example, encrypted audio), we had to create two additional classes: SecureCellPhone and SecureWalkieTalkie.

Adding a New Device

Let’s add a new device and see how many more classes are added.

// New Device
class LandlinePhone: AudioCommunicationsDevice {
override func sendAudio() {
// Implement function in subclasses
}
}

// Audio Types
class SecureLandlinePhone: LandlinePhone {
override func sendAudio() {
// Send encrypted audio
}
}

class PlainAudioLandlinePhone: LandlinePhone {
override func sendAudio() {
// Send plain audio
}
}

We needed to create three new classes just to add one new device! The ideal increase would be one class per new device or audio-handling feature. The rate of classes is increasing rapidly every time we add an audio-type/device.

Illustrating the Solution

Now that we have an understanding of what the exploding class hierarchy looks like, let’s go back to our meal example from earlier. The below illustration shows how things would look with the bridge pattern applied:

Two Hierarchies

It is unintuitive, but the solution to avoiding this mess is to create two separate hierarchies. This is what the definition means when it states:

The Bridge Pattern decouples an abstraction from its implementation…
  1. “Abstraction” refers to a base class or protocol. In our examples, it refers to the various burger classes. These burgers aren’t to be instantiated as the restaurant does not sell them stand-alone. They must be subclassed with an implementation as a complete meal in order to be used.
  2. “Implementation” refers to the meal classes we make once we include sides with our burgers.

So we decouple our burgers (abstraction) from our meals/sides (implementation) and are left with two class hierarchies.

Bridging Between Hierarchies

The definition then goes on to say:

The Bridge Pattern decouples an abstraction from its implementation so that the two can vary independently.

Now that we have two separate hierarchies, we need a way to connect the two. We do this by giving each Burger a property of type Side. This “has-a” relationship is the “bridge” connecting both hierarchies.

We do all this so that the two can vary independently. In other words, we no longer need to create a class for every combination of burgers and sides. Since their hierarchies are separated, each new burger or side requires only one new class.

This all may seem a bit abstract, so let’s jump back into code and demonstrate.

Coding the Solution

We need a separate hierarchy for devices and audio-types. Let’s start with audio-types:

protocol AudioHandling {
func handle(audio: Audio) -> Audio
}

class AudioEncryptor: AudioHandling {
func handle(audio: Audio) -> Audio {
// Encrypt and return Audio
}
}

class PlainAudioHandler: AudioHandling {
func handle(audio: Audio) -> Audio {
// return default audio
}
}

We create an AudioHandling protocol, and each conforming class will focus on preparing the audio for the device to send.

Now let’s reimplement our devices:

class CellPhone: AudioCommunicationsDevice {

let audioHandler: AudioHandling

init(audioHandler: AudioHandling) {
super.init()

self.audioHandler = audioHandler
}

override func sendAudio() {
// Use audioHandler(audio:) to prepare audio, then send audio
}
}

class WalkieTalkie: AudioCommunicationsDevice {

let audioHandler: AudioHandling

init(audioHandler: AudioHandling) {
super.init()

self.audioHandler = audioHandler
}

override func sendAudio() {
// Use audioHandler(audio:) to prepare audio, then send audio
}
}

We gave each device an audioHandler property, which allowed our devices to “bridge” over and work with all AudioHandling types.

Here is an updated visual that reflects our code:

Bridge in Action

Here is a brief example of how the bridge pattern is used:

let audioHandler = AudioEncryptor()
let walkieTalkie = WalkieTalkie(audioHandler: audioHandler)

walkieTalkie.sendAudio()

Our application of the bridge is fairly straight forward. We just choose a device to create, and pass in any audio handler.

Conclusion

Pat yourself on the back! You now know how to:

  1. Effectively counter exploding class hierarchies
  2. Build a bridge between two separate hierarchies

Another tool has been added to your programming kit! But we are not stopping just yet. Check in next week for another swifty design pattern!

Originally published at emanleet.com on July 29, 2017.