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:
- Abstraction
- Refined Abstraction
- Implementation Interface
- 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()
}
}
}Bridge vs Strategy — Clarifying the Difference
Though the example presented resembles the Strategy Pattern, the intent of Bridge Pattern differs significantly. Strategy is about selecting one of multiple algorithms or behaviors at runtime, often varying only a single operation or method. Bridge, by contrast, is about decoupling two orthogonal class hierarchies — abstraction and implementation — so each can evolve independently.
In Strategy, the context and the strategy typically belong to a single conceptual dimension (e.g., “sort algorithm”). In Bridge, the abstraction dimension (e.g., “shape” or “UI component”) is completely separate from the implementation dimension (e.g., “render target,” “persistence layer,” “algorithm variant”). This separation allows adding new abstractions and new implementations without combinatorial explosion. In short: use Strategy when you need to swap behavior; use Bridge when you need architectural flexibility across two independent axes.
Real-world Swift Use Cases for Bridge
Bridge becomes particularly powerful in Swift and iOS/macOS projects where performance, modularity, and platform abstractions matter. Common scenarios include:
- Rendering across platforms/frameworks — for example, you may define an abstraction such as
GraphicComponentand multiple rendering implementations: one usingUIKit, another usingSwiftUI, and perhaps a third for vector drawing or PDF export. Bridge allows you to support all render targets transparently while keeping a unified abstraction for the rest of your code. - Storage and persistence layers — you might define a protocol
SettingsStorageas the abstraction, and concrete implementations likeUserDefaultsStorage,KeychainStorage,FileStorage, or even a mock storage for testing. This lets the rest of the system consumeSettingsStoragewithout caring about underlying mechanics, and makes it easy to introduce new storage back-ends later. - Logging and telemetry — you can abstract logging functionality behind a
Loggerinterface and provide multiple implementations: console logging for development, file logging for diagnostics, or remote logging for production or telemetry pipelines. Switching or combining them becomes trivial with Bridge.
These patterns align with Swift best practices: using protocols for abstraction, dependency injection for flexibility, and keeping implementation details behind abstractions — all leading to maintainable, scalable, and testable codebases.
Enhanced Example: Two-Axis Variation with Abstraction and Implementation Hierarchies
To better illustrate Bridge’s power, consider this extended example:
protocol Meal {
func cook()
}
protocol Cookware {
var meal: Meal { get set }
func cook()
}
final class FryingPan: Cookware {
var meal: Meal
init(meal: Meal) {
self.meal = meal
}
func cook() {
meal.cook()
}
}
final class Grill: Cookware {
var meal: Meal
init(meal: Meal) {
self.meal = meal
}
func cook() {
print("Grill preheat…")
meal.cook()
}
}
final class Oven: Cookware {
var meal: Meal
init(meal: Meal) {
self.meal = meal
}
func cook() {
print("Oven preheat…")
meal.cook()
}
}
struct FriedEggs: Meal {
func cook() {
print("Frying eggs…")
}
}
struct RoastBeef: Meal {
func cook() {
print("Roasting beef…")
}
}Here, you have two independent hierarchies:
- Cookware hierarchy:
FryingPan,Grill,Oven— abstraction axis - Meal hierarchy:
FriedEggs,RoastBeef, etc. — implementation axis
With this design, you can combine any cookware with any meal:
let tasks: [Cookware] = [
FryingPan(meal: FriedEggs()),
Grill(meal: RoastBeef()),
Oven(meal: FriedEggs()),
]
tasks.forEach { $0.cook() }This pattern avoids class explosion (e.g., no need for separate GrillEggs, GrillBeef, OvenEggs, etc.), retains type safety, and remains open for extension: you can add new Cookware or new Meal without touching existing code.
When Bridge Is Overkill — Use Judiciously
While Bridge offers powerful structure, it may be unnecessary for simple cases. If your abstraction hierarchy contains only one concrete subclass and you don’t expect additional variants, introducing a bridge may add needless complexity. Similarly, if you don’t need multiple independent axes of variation, employing the simpler Strategy Pattern might suffice.
Therefore, before adopting Bridge, evaluate whether your design truly benefits from separate abstraction and implementation hierarchies — and whether that separation will pay off in scalability, maintainability or testability.
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.
Check out other posts in the Design Patterns series:
- Visitor Design Pattern in Swift
- Template Method Design Pattern in Swift
- Strategy Design Pattern in Swift
- State Design Pattern in Swift
- Observer Design Pattern in Swift
- Memento Design Pattern in Swift
- Mediator Design Pattern in Swift
- Iterator Design Pattern in Swift
- Command Design Pattern in Swift
- Chain of Responsibility Design Pattern in Swift
- Proxy Design Pattern in Swift
- FlyWeight Design Pattern in Swift
- Facade Design Pattern in Swift
- Decorator Design Pattern in Swift
- Composite Design Pattern in Swift
- Adapter Design Pattern in Swift
- Singleton Design Pattern in Swift
- Prototype Design Pattern in Swift
- Builder Design Pattern in Swift
- Abstract Factory Design Pattern in Swift
- Factory Method Design Pattern in Swift
- Design Patterns: Basics
