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
- Chain of Responsibility
- Iterator (provides sequential access to the elements of an aggregate object)
- Observer
- State
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
}
}