Bridge Design Pattern in Swift


Greetings, traveler!

Let’s continue our discussion on Structural Design Patterns. Today, we will explore the Bridge pattern. The Bridge pattern divides a complex system into two hierarchies—abstraction and implementation—that can be developed separately. This allows for more flexibility and easier maintenance of the system.

Problem

The Bridge pattern addresses a common issue that programmers encounter — the so-called “exploding class hierarchy.” This occurs when the number of classes in a system exponentially grows as new functions are added.

For example, let’s say you have a “Car” class. To create new variations, you could create a “Hatchback” and “Station Wagon” subclasses. Then, to add color options, you would make additional classes such as “Red Hatchback” and “Red Station Wagon,” “Green Hatchback,” and “Green Station Wagon.” After that, you might want to add a “Sedan” option, requiring even more subclasses to be created. 

And here, the Bridge Pattern comes to the rescue. There are four participants in this story:

  1. Abstraction
  2. Refined Abstraction
  3. Implementation Interaface
  4. Concrete Implementation

Example of usage

Let’s go straight to the live example. Imagine that we have a task — to cook dinner. We have food and a frying pan for that. We’ll write the code right away.

// Abstraction
protocol FryingPanProtocol {
    var meal: MealProtocol { get set }
    
    func cook()
}

// Implementation Interface
protocol MealProtocol {
    func cook()
}

// Refined Abstraction
final class FryingPan: FryingPanProtocol {
    
    var meal: MealProtocol
    
    init(meal: MealProtocol) {
        self.meal = meal
    }
    
    func cook() {
        meal.cook()
    }
    
}

// Concrete Implementation
final class FriedEggs: MealProtocol {
    
    func cook() {
        print("The eggs are cooked")
    }
    
}

// Concrete Implementation
final class RoastBeef: MealProtocol {
    
    func cook() {
        print("The roast beef is cooked")
    }
    
}

The Frying Pan acts as the bridge between the client and the meal, helping to cook any desired dinner.

// Client
final class DinnerManager {
    
    let roastBeef = RoastBeef()
    let friedEggs = FriedEggs()

    lazy var panForBeef = FryingPan(meal: roastBeef)
    lazy var panForEggs = FryingPan(meal: friedEggs)
    
    func serveDinner() {
        [panForBeef, panForEggs].forEach {
            $0.cook()
        }
    }
    
}

Conclusion

The Bridge Design Pattern helps hide implementation details from the client and implements the open-closed principle. It makes it possible to manage different modules independently. 

And that’s all I wanted to tell you about this pattern today. Now, let’s move on to the next Structural Design Pattern — the Composite Pattern.