Bridge – Design Patterns in Swift

Introduction

The Bridge design pattern is used to resolve two problems. One is an abstraction and its implementation should be defined and extended independently from each other. The other one is a compile-time binding between an abstraction and its implementation should be avoided so that an implementation can be selected at run-time. When using subclassing, different subclasses implement an abstract class in different ways. But implementation is bound to the abstraction at compile-time and cannot be changed at run-time.

Bridge Pattern – UML

Use Bridge Pattern in Swift

The Abstraction defines the interface for the “control” part of the two class-hierarchies. It maintains a reference to an object of the Implementation hierarchy and delegates all of the real work to this object.

import XCTest

class Abstraction {

    fileprivate var implementation: Implementation

    init(_ implementation: Implementation) {
        self.implementation = implementation
    }

    func operation() -> String {
        let operation = implementation.operationImplementation()
        return "Abstraction: Base operation with:\n" + operation
    }
}

You can extend the Abstraction without changing the Implementation classes.

class ExtendedAbstraction: Abstraction {

    override func operation() -> String {
        let operation = implementation.operationImplementation()
        return "ExtendedAbstraction: Extended operation with:\n" + operation
    }
}

The Implementation defines the interface for all implementation classes. It doesn’t have to match the Abstraction’s interface. In fact, the two interfaces can be entirely different. Typically the Implementation interface provides only primitive operations, while the Abstraction defines higher-level operations based on those primitives.

protocol Implementation {

    func operationImplementation() -> String
}

Each Concrete Implementation corresponds to a specific platform and implements the Implementation interface using that platform’s API.

class ConcreteImplementationA: Implementation {

    func operationImplementation() -> String {
        return "ConcreteImplementationA: Here's the result on the platform A.\n"
    }
}

class ConcreteImplementationB: Implementation {

    func operationImplementation() -> String {
        return "ConcreteImplementationB: Here's the result on the platform B\n"
    }
}

Except for the initialization phase, where an Abstraction object gets linked with a specific Implementation object, the client code should only depend on the Abstraction class. This way the client code can support any abstraction-implementation combination.

class Client {

    static func someClientCode(abstraction: Abstraction) {
        print(abstraction.operation())
    }
}

Let’s see how it all works together. The client code should be able to work with any pre-configured abstraction-implementation combination.

class BridgeConceptual: XCTestCase {

    func testBridgeConceptual() {

        let implementation = ConcreteImplementationA()
        Client.someClientCode(abstraction: Abstraction(implementation))

        let concreteImplementation = ConcreteImplementationB()
        Client.someClientCode(abstraction: ExtendedAbstraction(concreteImplementation))
    }
}

Conclusion

A Bridge can be recognized by a clear distinction between some controlling entity and several different platforms that it relies on. The Bridge pattern is especially useful when dealing with cross-platform apps, supporting multiple types of database servers, or working with several API providers of a certain kind (for example, cloud platforms, social networks, etc.)

Leave a Reply

Your email address will not be published. Required fields are marked *