Practical Design Patterns in Swift

DesignPatterns

This is training project taken from Practical Design Patterns in Swift

1. Creational Design Patterns

  • Singleton (ensures that there is only one instance of a type)
  • Prototype (concerned with the cloning of objects)
  • Factory Method (creates objects without knowing its exact type)

2. Structural Design Patterns

  • Adapter (wraps an incompatible type and exposes an interface that’s familiar to the caller)
  • Decorator (allows to add new responsibilities to objects dynamically)
  • Facade (simplifies the usage of complex types)
  • Flyweight (reduces memory usage by sharing common data between objects)
  • Proxy (manage and controll access to specific objects)

3. Behavioral Design Patterns

1.1 Singleton

Concurrency issue: one thread could write to property another read from this property => crash

Solution 1

Execute code in serial queue

public func set(value: Any, forKey key: String) {
    serialQueue.sync {
        self.settings[key] = value
    }
}

Solution 2 (optimized for performance)

Execute code in concurrent queue with reader’s right lock WRITE .async with flags .barrier. Code won’t be processed until all other operations complete

public func set(value: Any, forKey key: String) {
    concurrentQueue.async(flags: .barrier) {
        self.settings[key] = value
    }
}

READ .sync

public func string(forKey key: String) -> String? {
    var result: String?
    concurrentQueue.sync {
        result = self.settings[key] as? String
    }
    return result
}

1.2 Prototype

Problem: 1 object creates in 1ms, then 1000 in 1000ms. TOO LONG! Prototype patterns helps to decrease creation time.

Value types – has protorype behaviour outside a “box”. Reference types – doesn’t have.

Solution

class NameClass: NSCopying {
    // other code
    func copy(with zone: NSZone? = nil) -> Any {
        return NameClass(firstName: self.firstName, lastName: self.lastName)
    }
  
    // call this method to clone instance
    func clone() -> NameClass {
        return self.copy() as! NameClass
    }
}

// using
var steve = NameClass(firstName: "Steve", lastName: "Johnson")
var john = steve.clone()

Changing john instance doesn’t affect on steve object

1.3 The Factory Method

This pattern encapsulates objects creation in one method. This method returns objects which types implements protocol.

Problem: objects created directly depending on each type – this is not flexible for future changing (what if you would change type?)

Solution

struct SerializerFactory {
    
    static func makeSerializer(_ type: Serializers) -> Serializable? {
        let result: Serializable?
        switch type {
        case .json: result = JSONSerializer()
        case .plist: result = PropertyListSerializer()
        case .xml: result = XMLSerializer()
        }
        return result
    }
    
}

All this classes – JSONSerializer, PropertyListSerializer, XMLSerializer – implement Serializable protocol

2.1 Adapter

Adapter pattern becomes a link between 3rd party library and existing source code

Problem: third party library doesn’t conform to existing protocol but has similar functionality which you need

Solution 1

Create class wrapper wich will implement existing protocol from source code

// adapter class which implements existing protocol
class AmazonPaymentsAdapter: PaymentGateway {
    // amazonPayments - object from 3rd party library
    var totalPayments: Double {
        let total = amazonPayments.payments
        print("Total payments received via Amazon Payments: \(total)")
        return total
    }
    
    func receivePayment(amount: Double) {
        amazonPayments.paid(value: amount, currency: "USD")
    }
    
}

Solution 2

Create an extension to type from 3rd party library

extension AmazonPayments: PaymentGateway {
    func receivePayment(amount: Double) {
        self.paid(value: amount, currency: "USD")
    }
    
    var totalPayments: Double {
        let total = self.payments
        print("Total payments received via Amazon Payments: \(total)")
        return total
    }
}

2.2 Decorator

Problem: you have a type and you want to expand it’s functionality

Solution 1

Create class inherited from target class and wrap the value. Then to create ‘decorate’ more functions/properties etc.

	class UserDefaultsDecorator: UserDefaults {
    // wrapped value
    private var userDefaults = UserDefaults.standard
    
    convenience init(userDefaults: UserDefaults) {
        self.init()
        self.userDefaults = userDefaults
    }
    
    // new function
    func set(date: Date?, forKey key: String) {
        userDefaults.set(date, forKey: key)
    }
    // ...

Solution 2

Use extensions if you don’t need to use stored properties / property observers

extension UserDefaults {
    
    func set(date: Date?, forKey key: String) {
        self.set(date, forKey: key)
    }
    //...
}

2.3 Facade

Problem: you have a big library but want to use only 3 functions

Solution

Create class in which you wrap functions from library to reuse it later – in Beta testing for instance

2.4 Flyweight

Problem: you have to creat 1000 shaceships. 1 spaceship == 300 KB of memory -> 1000 spaceships == 300 MB of memory.

Solution

Create reference type object with common data shared with other 1000 “spaceships” to use.

// every spaceship have mesh and texture
class SharedSpaceShipData {
    private let mesh: [Float]
    private let texture: UIImage?
    //...
}
// spaceship class with reference to common data in shared spaceship data object
class SpaceShip {
    private var position: (Float, Float, Float)
    private var intrisicState: SharedSpaceShipData
    //...
}

2.5 Proxy

Problem: additional functionality may be needed while accessing an object (DB for instance. You are not using DB directly)

Solution

class RandomIntWithID {
    var value: Int = {
        print("value initialized")
        return Int.random(in: Int.min...Int.max)
    }()
  
    // property will be initialized only after first call
    lazy var uid: String = {
        print("uid initialized")
        return UUID().uuidString
    }()
}

2.6 The Chain of Responsibility

2.7 Iterator

Problem: you want to create class that can be iterated (to use your object in for-in loop)

Solution

You need to create structure which implements IteratorProtocol and implement Sequence protocol in your class

// custom queue which implements protocol Sequence
extension Queue: Sequence {
    
    func makeIterator() -> QueueIterator<T> {
        return QueueIterator(self)
    }
    
}

// queue iterator which helps to iterate through the queue
struct QueueIterator<T>: IteratorProtocol {
    private let queue: Queue<T>
    private var currentNode: Node<T>?
    
    init(_ queue: Queue<T>) {
        self.queue = queue
        currentNode = queue.head
    }
    
    mutating func next() -> T? {
        guard let node = currentNode else { return nil }
        let nextKey = currentNode?.key
        currentNode = node.next
        return nextKey
    }
}

2.8 Observer

2.9 State

GitHub

View Github