Skyrocket Your App's Responsiveness: 11 Essential Behavioral Design Patterns

Skyrocket Your App's Responsiveness: 11 Essential Behavioral Design Patterns

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

Behavioral Design Patterns are key to enhancing your Swift application's responsiveness and communication between objects. This blog post will delve into 11 essential Behavioral Design Patterns, complete with detailed code examples to integrate into your projects.

1. Chain of Responsibility: Decouple Sender and Receiver of Requests

The Chain of Responsibility pattern creates a chain of receiver objects to handle a request. A sender object sends the request to the first receiver in the chain, which either processes the request or passes it to the next receiver in the chain.

protocol Handler: AnyObject {
    var next: Handler? { get set }
    func handle(request: String)
}

class ConcreteHandlerA: Handler {
    var next: Handler?

    func handle(request: String) {
        if request == "A" {
            print("Handler A processed the request")
        } else {
            next?.handle(request: request)
        }
    }
}

class ConcreteHandlerB: Handler {
    var next: Handler?

    func handle(request: String) {
        if request == "B" {
            print("Handler B processed the request")
        } else {
            next?.handle(request: request)
        }
    }
}

// Usage
let handlerA = ConcreteHandlerA()
let handlerB = ConcreteHandlerB()
handlerA.next = handlerB
handlerA.handle(request: "A")
handlerA.handle(request: "B")

2. Command: Encapsulate a Request as an Object

The Command pattern encapsulates a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.

protocol Command {
    func execute()
}

class ConcreteCommandA: Command {
    private let receiver: Receiver

    init(receiver: Receiver) {
        self.receiver = receiver
    }

    func execute() {
        receiver.actionA()
    }
}

class ConcreteCommandB: Command {
    private let receiver: Receiver

    init(receiver: Receiver) {
        self.receiver = receiver
    }

    func execute() {
        receiver.actionB()
    }
}

class Receiver {
    func actionA() {
        print("Receiver action A")
    }

    func actionB() {
        print("Receiver action B")
    }
}

class Invoker {
    private var command: Command?

    func setCommand(command: Command) {
        self.command = command
    }

    func invoke() {
        command?.execute()
    }
}

// Usage
let receiver = Receiver()
let commandA = ConcreteCommandA(receiver: receiver)
let commandB = ConcreteCommandB(receiver: receiver)

let invoker = Invoker()
invoker.setCommand(command: commandA)
invoker.invoke()
invoker.setCommand(command: commandB)
invoker.invoke()

3. Iterator: Access Elements Sequentially without Exposing the Underlying Representation

The Iterator pattern provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation.

protocol Iterator {
    func hasNext() -> Bool
    func next() -> String?
}

class ConcreteIterator: Iterator {
    private let collection: [String]
    private var currentIndex: Int = 0

    init(collection: [String]) {
        self.collection = collection
    }

    func hasNext() -> Bool {
        return currentIndex < collection.count
    }

    func next() -> String? {
        guard hasNext() else { return nil }
        let item = collection[currentIndex]
        currentIndex += 1
        return item
    }
}

protocol Container {
    func createIterator() -> Iterator
}

class ConcreteContainer:Container {
    private let items: [String]

    init(items: [String]) {
        self.items = items
    }

    func createIterator() -> Iterator {
        return ConcreteIterator(collection: items)
    }
}

// Usage
let container = ConcreteContainer(items: ["A", "B", "C"])
let iterator = container.createIterator()

while iterator.hasNext() {
    print(iterator.next()!)
}

4. Mediator: Encapsulate Object Interaction in a Separate Object

The Mediator pattern defines an object that encapsulates how a set of objects interact. This pattern promotes loose coupling by keeping objects from referring to each other explicitly, and it allows their interaction to vary independently.

protocol Mediator {
    func send(message: String, colleague: Colleague)
}

class ConcreteMediator: Mediator {
    private var colleagues: [Colleague] = []

    func addColleague(colleague: Colleague) {
        colleagues.append(colleague)
    }

    func send(message: String, colleague: Colleague) {
        for c in colleagues {
            if c !== colleague {
                c.receive(message: message)
            }
        }
    }
}

protocol Colleague: AnyObject {
    var mediator: Mediator { get }
    func send(message: String)
    func receive(message: String)
}

class ConcreteColleague: Colleague {
    let mediator: Mediator

    init(mediator: Mediator) {
        self.mediator = mediator
    }

    func send(message: String) {
        mediator.send(message: message, colleague: self)
    }

    func receive(message: String) {
        print("Colleague received: \(message)")
    }
}

// Usage
let mediator = ConcreteMediator()
let colleague1 = ConcreteColleague(mediator: mediator)
let colleague2 = ConcreteColleague(mediator: mediator)

mediator.addColleague(colleague: colleague1)
mediator.addColleague(colleague: colleague2)

colleague1.send(message: "Hello")
colleague2.send(message: "Hi")

5. Memento: Capture and Restore Object State

The Memento pattern captures and externalizes an object's internal state without violating encapsulation so that the object can be restored to that state later.

class Originator {
    private(set) var state: String

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

    func set(state: String) {
        self.state = state
    }

    func saveToMemento() -> Memento {
        return Memento(state: state)
    }

    func restoreFromMemento(memento: Memento) {
        state = memento.state
    }
}

class Memento {
    fileprivate let state: String

    fileprivate init(state: String) {
        self.state = state
    }
}

class Caretaker {
    private var mementos: [Memento] = []

    func addMemento(memento: Memento) {
        mementos.append(memento)
    }

    func getMemento(at index: Int) -> Memento? {
        guard index < mementos.count else { return nil }
        return mementos[index]
    }
}

// Usage
let originator = Originator(state: "Initial State")
let caretaker = Caretaker()
caretaker.addMemento(memento: originator.saveToMemento())

originator.set(state: "State 1")
caretaker.addMemento(memento: originator.saveToMemento())

originator.set(state: "State 2")
caretaker.addMemento(memento: originator.saveToMemento())

originator.restoreFromMemento(memento: caretaker.getMemento(at: 0)!)
print("Restored state: (originator.state)") // "Initial State"

originator.restoreFromMemento(memento: caretaker.getMemento(at: 1)!)
print("Restored state: (originator.state)") // "State 1"

6. Observer: Define a One-to-Many Dependency between Objects

The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

protocol Observer: AnyObject {
    func update(subject: Subject)
}

protocol Subject {
    func attach(observer: Observer)
    func detach(observer: Observer)
    func notify()
}

class ConcreteSubject: Subject {
    private var observers: [Observer] = []
    var state: String = "" {
        didSet {
            notify()
        }
    }

    func attach(observer: Observer) {
        observers.append(observer)
    }

    func detach(observer: Observer) {
        observers = observers.filter { $0 !== observer }
    }

    func notify() {
        for observer in observers {
            observer.update(subject: self)
        }
    }
}

class ConcreteObserver: Observer {
    private(set) var state: String = ""

    func update(subject: Subject) {
        if let subject = subject as? ConcreteSubject {
            state = subject.state
            print("Observer updated with state: \(state)")
        }
    }
}

// Usage
let subject = ConcreteSubject()
let observer1 = ConcreteObserver()
let observer2 = ConcreteObserver()

subject.attach(observer: observer1)
subject.attach(observer: observer2)

subject.state = "New State"

7. Strategy: Define a Family of Algorithms, Encapsulate Each One, and Make Them Interchangeable

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

protocol Strategy {
    func execute(a: Int, b: Int) -> Int
}

class ConcreteStrategyAdd: Strategy {
    func execute(a: Int, b: Int) -> Int {
        return a + b
    }
}

class ConcreteStrategySubtract: Strategy {
    func execute(a: Int, b: Int) -> Int {
        return a - b
    }
}

class Context {
    private let strategy: Strategy

    init(strategy: Strategy) {
        self.strategy = strategy
    }

    func executeStrategy(a: Int, b: Int) -> Int {
        return strategy.execute(a: a, b: b)
    }
}

// Usage
let contextAdd = Context(strategy: ConcreteStrategyAdd())
print("Addition: \(contextAdd.executeStrategy(a: 5, b: 3))")

let contextSubtract = Context(strategy: ConcreteStrategySubtract())
print("Subtraction: \(contextSubtract.executeStrategy(a: 5, b: 3))")

8. State: Change Behavior Based on State

The State pattern allows an object to alter its behavior when its internal state changes. The object appears to change its class.

protocol State {
    func handle(context: Context)
}

class ConcreteStateA: State {
    func handle(context: Context) {
        print("State A handling")
        context.setState(state: ConcreteStateB())
    }
}

class ConcreteStateB: State {
    func handle(context: Context) {
        print("State B handling")
        context.setState(state: ConcreteStateA())
    }
}

class Context {
    private var state: State

    init(state: State) {
        self.state = state
    }

    func setState(state: State) {
        self.state = state
    }

    func request() {
        state.handle(context: self)
    }
}

// Usage
let context = Context(state: ConcreteStateA())
context.request()
context.request()

9. Template Method: Define a Skeleton of an Algorithm in an Operation

The Template Method pattern defines the skeleton of an algorithm in an operation, deferring some steps to subclasses. This pattern lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.

class AbstractClass {
    func templateMethod() {
        primitiveOperation1()
        primitiveOperation2()
    }

    func primitiveOperation1() {
        fatalError("Subclasses must override this method")
    }

    func primitiveOperation2() {
        fatalError("Subclasses must override this method")
    }
}

class ConcreteClass: AbstractClass {
    override func primitiveOperation1() {
        print("ConcreteClass primitive operation 1")
    }

    override func primitiveOperation2() {
        print("ConcreteClass primitive operation 2")
    }
}

// Usage
let concreteClass = ConcreteClass()
concreteClass.templateMethod()

10. Visitor: Separate Algorithm from Object Structure

The Visitor pattern represents an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.

protocol Visitor {
    func visit(element: Element)
}

class ConcreteVisitor: Visitor {
    func visit(element: Element) {
        if let elementA = element as? ConcreteElementA {
            elementA.operationA()
        } else if let elementB = element as? ConcreteElementB {
            elementB.operationB()
        }
    }
}

protocol Element {
    func accept(visitor: Visitor)
}

class ConcreteElementA: Element {
    func accept(visitor: Visitor) {
        visitor.visit(element: self)
    }

    func operationA() {
        print("ConcreteElementA operation")
    }
}

class ConcreteElementB: Element {
    func accept(visitor: Visitor) {
        visitor.visit(element: self)
    }

    func operationB() {
        print("ConcreteElementB operation")
    }
}

// Usage
let elements: [Element] = [ConcreteElementA(), ConcreteElementB()]
let visitor = ConcreteVisitor()

for element in elements {
    element.accept(visitor: visitor)
}

11. Interpreter Design Pattern: Effortlessly Process Complex Grammar

The Interpreter Design Pattern is used to define a representation for a grammar and provide an interpreter to deal with this grammar. Here's a simple example in Swift, demonstrating an arithmetic expression interpreter.

import Foundation

// The abstract expression protocol
protocol Expression {
    func interpret() -> Int
}

// Terminal expression for numbers
class NumberExpression: Expression {
    private let number: Int

    init(_ number: Int) {
        self.number = number
    }

    func interpret() -> Int {
        return number
    }
}

// Non-terminal expression for addition
class AddExpression: Expression {
    private let leftExpression: Expression
    private let rightExpression: Expression

    init(_ left: Expression, _ right: Expression) {
        leftExpression = left
        rightExpression = right
    }

    func interpret() -> Int {
        return leftExpression.interpret() + rightExpression.interpret()
    }
}

// Non-terminal expression for subtraction
class SubtractExpression: Expression {
    private let leftExpression: Expression
    private let rightExpression: Expression

    init(_ left: Expression, _ right: Expression) {
        leftExpression = left
        rightExpression = right
    }

    func interpret() -> Int {
        return leftExpression.interpret() - rightExpression.interpret()
    }
}

// Example usage
let expression1 = NumberExpression(5)
let expression2 = NumberExpression(7)

let addition = AddExpression(expression1, expression2)
print("5 + 7 = \(addition.interpret())") // Output: 5 + 7 = 12

let subtraction = SubtractExpression(expression1, expression2)
print("5 - 7 = \(subtraction.interpret())") // Output: 5 - 7 = -2

By mastering these 11 essential Behavioral Design Patterns in your Swift applications, you can dramatically enhance your app's responsiveness, communication, and overall structure. Implementing these patterns in your projects will help you create more maintainable and scalable code, resulting in a higher level of user satisfaction and a more successful application.

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!