Turbocharge Your Swift Code with These 7 Powerful Structural Design Patterns

Turbocharge Your Swift Code with These 7 Powerful Structural Design Patterns

To check the previous article in this Design Patterns in Swift please visit Creational Design Patterns.

Structural Design Patterns play a crucial role in organizing your code and creating flexible, efficient, and scalable systems. In this blog post, we'll explore 7 game-changing Structural Design Patterns for Swift developers, complete with detailed code examples that you can implement in your projects right away. Get ready to turbocharge your Swift code!

1. Adapter: Bridge the Gap Between Incompatible Interfaces

The Adapter pattern allows two incompatible interfaces to work together by wrapping one with an adapter that matches the other's expectations.

protocol Target {
    func request()
}

class Adaptee {
    func specificRequest() {
        print("Specific request")
    }
}

class Adapter: Target {
    private let adaptee: Adaptee

    init(adaptee: Adaptee) {
        self.adaptee = adaptee
    }

    func request() {
        adaptee.specificRequest()
    }
}

// Usage
let adaptee = Adaptee()
let adapter: Target = Adapter(adaptee: adaptee)
adapter.request()

2. Bridge: Decouple Abstractions from Implementations

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

protocol Renderer {
    func render(shape: String)
}

class VectorRenderer: Renderer {
    func render(shape: String) {
        print("Drawing \(shape) as vector")
    }
}

class RasterRenderer: Renderer {
    func render(shape: String) {
        print("Drawing \(shape) as raster")
    }
}

class Shape {
    let renderer: Renderer

    init(renderer: Renderer) {
        self.renderer = renderer
    }

    func draw() {
        fatalError("Subclasses must implement this method")
    }
}

class Circle: Shape {
    func draw() {
        renderer.render(shape: "circle")
    }
}

// Usage
let vectorRenderer: Renderer = VectorRenderer()
let circle = Circle(renderer: vectorRenderer)
circle.draw()

3. Composite: Treat a Group of Objects as a Single Object

The Composite pattern allows you to treat a group of objects as a single object. This is especially useful when you need to work with a hierarchy of objects that have the same interface.

protocol Component {
    func operation()
}

class Leaf: Component {
    func operation() {
        print("Leaf operation")
    }
}

class Composite: Component {
    private var children: [Component] = []

    func add(_ component: Component) {
        children.append(component)
    }

    func remove(_ component: Component) {
        children.removeAll { $0 === component }
    }

    func operation() {
        for child in children {
            child.operation()
        }
    }
}

// Usage
let leaf1 = Leaf()
let leaf2 = Leaf()

let composite = Composite()
composite.add(leaf1)
composite.add(leaf2)
composite.operation()

4. Decorator: Add New Behavior to Objects Without Modifying Their Classes

The Decorator pattern allows you to add new behavior to objects without modifying their classes. This is achieved by wrapping an object with a decorator that implements the same interface.

protocol Beverage {
    func cost() -> Double
    func description() -> String
}

class Coffee: Beverage {
    func cost() -> Double {
        return 1.0
    }

    func description() -> String {
        return "Coffee"
    }
}

class BeverageDecorator: Beverage {
    private let beverage: Beverage

    init(beverage: Beverage) {
        self.beverage = beverage
    }

    func cost() -> Double {
        return beverage.cost()
    }

    func description() -> String {
        return beverage.description()
    }
}

class Milk: BeverageDecorator {
    override func cost() -> Double {
        return super.cost() + 0.5
    }

    override func description() -> String {
        return super.description() + ", Milk"
    }
}

// Usage
let coffee: Beverage = Coffee()
let coffeeWithMilk = Milk(beverage: coffee)
print(coffeeWithMilk.description())
print(coffeeWithMilk.cost())

5. Facade: Simplify Complex Interfaces

The Facade pattern provides a simplified interface to a complex subsystem, making it easier for clients to interact with it.

class ComplexSubsystemA {
    func operationA() {
        print("Operation A")
    }
}

class ComplexSubsystemB {
    func operationB() {
        print("Operation B")
    }
}

class Facade {
    private let subsystemA: ComplexSubsystemA
    private let subsystemB: ComplexSubsystemB

    init(subsystemA: ComplexSubsystemA, subsystemB: ComplexSubsystemB) {
        self.subsystemA = subsystemA
        self.subsystemB = subsystemB
    }

    func simplifiedOperation() {
        subsystemA.operationA()
        subsystemB.operationB()
    }
}

// Usage
let subsystemA = ComplexSubsystemA()
let subsystemB = ComplexSubsystemB()
let facade = Facade(subsystemA: subsystemA, subsystemB: subsystemB)
facade.simplifiedOperation()

6. Flyweight: Minimize Memory Usage with Shared Objects

The Flyweight pattern helps minimize memory usage by sharing objects with similar state, instead of creating new ones.

class Flyweight {
    private let sharedState: String

    init(sharedState: String) {
        self.sharedState = sharedState
    }

    func operation(uniqueState: String) {
        print("Shared state: \(sharedState), unique state: \(uniqueState)")
    }
}

class FlyweightFactory {
    private var flyweights: [String: Flyweight] = [:]

    func getFlyweight(for key: String) -> Flyweight {
        if let existingFlyweight = flyweights[key] {
            return existingFlyweight
        } else {
            let newFlyweight = Flyweight(sharedState: key)
            flyweights[key] = newFlyweight
            return newFlyweight
        }
    }
}

// Usage
let factory = FlyweightFactory()
let flyweight1 = factory.getFlyweight(for: "shared1")
flyweight1.operation(uniqueState: "unique1")

let flyweight2 = factory.getFlyweight(for: "shared1") // Reuses existing flyweight
flyweight2.operation(uniqueState: "unique2")

7. Proxy: Control Access to Objects with a Surrogate

The Proxy pattern provides a surrogate object that controls access to another object. This can be used for various purposes such as security, lazy loading, or remote object access.

protocol Subject {
    func request()
}

class RealSubject: Subject {
    func request() {
        print("Real subject request")
    }
}

class Proxy: Subject {
    private let realSubject: RealSubject

    init(realSubject: RealSubject) {
        self.realSubject = realSubject
    }

    func request() {
        if checkAccess() {
            realSubject.request()
            logAccess()
        }
    }

    private func checkAccess() -> Bool {
        // Perform access control checks here
        print("Access granted")
        return true
    }

    private func logAccess() {
        print("Access logged")
    }
}

// Usage
let realSubject = RealSubject()
let proxy: Subject = Proxy(realSubject: realSubject)
proxy.request()

By implementing these 7 powerful Structural Design Patterns in your Swift projects, you can dramatically improve the organization and flexibility of your code. These patterns will help you create clean, efficient, and scalable systems that can adapt to changing requirements with ease.

I hope you enjoyed this article, and if you have any questions, comments, or feedback, then feel free to comment here or reach out via Twitter.

Thanks for reading!

Check the next article in this series Behavioral Design Patterns in Swift.