Skip to content

Instantly share code, notes, and snippets.

@cellularmitosis
Last active October 15, 2019 07:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cellularmitosis/65646339ac9a689c19b5e3c0dfd9dcb7 to your computer and use it in GitHub Desktop.
Save cellularmitosis/65646339ac9a689c19b5e3c0dfd9dcb7 to your computer and use it in GitHub Desktop.
Exploring "Dependency Object" Patterns in Swift / iOS

Blog 2019/10/2

<- previous | index | next ->

Exploring "Dependency Object" Patterns in Swift / iOS

Attached are three variations on creating a "dependencies" object to contain all of an application's dependencies.

This approach can be used to alleviate the verbose plumbing code of passing all dependencies down through the init()'s of a tree of objects.

Design issues / decisions:

  • Should "providing" protocols use funcs or vars?

    • i.e. var serviceA vs func serviceA()
  • For deps which themselves have deps ("internal nodes of the deps tree"), should they take a deps object or should we ban them from using the deps pattern?

    • i.e. if C depends on A and B, should we class ServiceC { init(deps: Deps) } or should we class ServiceC { init(a: ServiceA, b: ServiceB) }?
  • If internal nodes take a deps object, the deps object will run into a bootstrapping problem (ServiceC.init() needs a Deps and Deps.init() needs a ServiceC)

    • We can get around this by using force-unwrapped vars (and a static builder method)
    • Or avoid the issue by banning internal nodes from using the deps pattern.
  • If internal nodes are banned from the deps pattern, we lose the ability to replace a dep at run-time and have every part of the system automatically start using the new dep (if ServiceC uses ServiceA (given via init), replacing Deps.serviceA at runtime with a fake does not affect ServiceC, which will still use the original A).

  • Deps1.swift:

    • "providing" protocols use vars
    • "internal" nodes use deps pattern
    • use force-unwrap and static factory method to work around the bootstrapping problem
    • doesn't compile because it turns out var foo: Foo! doesn't satisfy a var foo: Foo { get } protocol
  • Deps2.swift:

    • "providing" protocols use funcs
    • "internal" nodes do NOT use deps pattern
    • there is no bootstrapping problem, but internal dep nodes don't get updated deps
  • Deps3.swift:

    • "providing" protocols use funcs
    • "internal" nodes use deps pattern
    • use force-unwrap and static factory method to work-around the bootstrapping problem
    • there is an awkward property / func naming conflict (e.g. var aService and func aService())

It looks like Deps3.swift is the least-worst of the three approaches.

// Deps1.playground
import UIKit
// Design issues / decisions:
//
// - should "providing" protocols use funcs or vars?
// - i.e. `var serviceA` vs `func serviceA()`
//
// - for deps which themselves have deps ("internal nodes of the deps tree"),
// should they take a deps object or should we ban them from using the deps pattern?
// - i.e. if C depends on A and B, should we `class ServiceC { init(deps: Deps) }`
// or should we `class ServiceC { init(a: ServiceA, b: ServiceB) }`?
//
// - if internal nodes take a deps object, the deps object will run into a bootstrapping problem
// (`ServiceC.init()` needs a `Deps` and `Deps.init()` needs a `ServiceC`)
// - we can get around this by using force-unwrapped vars (and a static builder method)
// - or avoid the issue by banning internal nodes from using the deps pattern.
//
// - if internal nodes are banned from the deps pattern, we lose the ability to replace
// a dep at run-time and have every part of the system automatically start using the new
// dep (if ServiceC uses ServiceA (given via init), replacing Deps.serviceA
// at runtime with a fake does not affect ServiceC, which will still use the original A).
// This playground explores:
// - "providing" protocols use vars
// - "internal" nodes use deps pattern
// - use force-unwrap to work-around the bootstrapping problem
// Ultimately, it looks like this approach will not work because `var foo: Foo!`
// does not satisfy a `var foo: Foo { get }` protocol.
// service data types
public typealias A = Int
public typealias B = Int
public typealias C = (A,B)
// service protocols
public protocol AServicing {
func getAs() -> [A]
}
public protocol BServicing {
func getBs() -> [B]
}
public protocol CServicing {
func getCs() -> [C]
}
// service implementations
public class AService: AServicing {
public func getAs() -> [A] {
return [1,2,3]
}
}
public class BService: BServicing {
public func getBs() -> [B] {
return [11,12,13]
}
}
public class CService: CServicing {
public init(deps: AServiceProviding & BServiceProviding) {
_deps = deps
}
public func getCs() -> [C] {
return Array(zip(_deps.aService.getAs(), _deps.bService.getBs()))
}
private let _deps: AServiceProviding & BServiceProviding
}
// deps protocols
public protocol AServiceProviding {
var aService: AServicing { get }
}
public protocol BServiceProviding {
var bService: BServicing { get }
}
public protocol CServiceProviding {
var cService: CServicing { get }
}
// deps implementation
public class Deps: AServiceProviding, BServiceProviding, CServiceProviding {
public var aService: AServicing
public var bService: BServicing
// this fails to compile because the force-unwrapped version apparently doesn't satisfy the protocol requirement:
public var cService: CServicing!
public init(aService: AServicing, bService: BServicing) {
self.aService = aService
self.bService = bService
}
public static func makeDeps(
aService: AServicing,
bService: BServicing,
cService: CServicing? = nil
) -> Deps {
let deps = Deps(aService: aService, bService: bService)
deps.cService = cService ?? CService(deps: deps)
return deps
}
}
// Some objects which use the deps:
public class ControllerAB {
public let deps: AServiceProviding & BServiceProviding
public init(deps: AServiceProviding & BServiceProviding) {
self.deps = deps
}
public func total() -> Int {
return (deps.aService.getAs() + deps.bService.getBs())
.reduce(0, { $0 + $1 })
}
}
public class ControllerC {
public let deps: CServiceProviding
public init(deps: CServiceProviding) {
self.deps = deps
}
public func total() -> Int {
return deps.cService.getCs()
.reduce(0, { $0 + $1.0 + $1.1 })
}
}
// use real services:
let a = AService()
a.getAs() // [1,2,3]
let b = BService()
b.getBs() // [11,12,13]
let deps1 = Deps.makeDeps(aService: a, bService: b)
let c = CService(deps: deps1)
c.getCs() // [(1,11),(2,12),(3,13)]
deps1.cService.getCs() // [(1,11),(2,12),(3,13)]
let controllerab1 = ControllerAB(deps: deps1)
controllerab1.total() // 42
let controllerc1 = ControllerC(deps: deps1)
controllerc1.total() // 42
// fake A and B implementations:
public class FakeAService: AServicing {
public func getAs() -> [A] {
return [-1,-2,-3]
}
}
public class FakeBService: BServicing {
public func getBs() -> [B] {
return [-11,-12,-13]
}
}
// use fake A with real C:
let fa = FakeAService()
fa.getAs() // [-1,-2,-3]
let deps2 = Deps.makeDeps(aService: fa, bService: b)
let c2 = CService(deps: deps2)
c2.getCs() // [(-1,11),(-2,12),(-3,13)]
deps2.cService.getCs() // [(-1,11),(-2,12),(-3,13)]
let controllerab2 = ControllerAB(deps: deps2)
controllerab2.total() // 30
let controllerc2 = ControllerC(deps: deps2)
controllerc2.total() // 30
// fake B with real C:
let fb = FakeBService()
fb.getBs() // [-11,-12,-13]
let deps3 = Deps.makeDeps(aService: a, bService: fb)
let c3 = CService(deps: deps3)
c3.getCs() // [(1,-11),(2,-12),(3,-13)]
deps3.cService.getCs() // [(1,-11),(2,-12),(3,-13)]
let controller3 = ControllerAB(deps: deps3)
controller3.total() // -30
let controllerc3 = ControllerC(deps: deps3)
controllerc3.total() // -30
// fake A and fake B with real C:
let deps4 = Deps.makeDeps(aService: fa, bService: fb)
let c4 = CService(deps: deps3)
c4.getCs() // [(-1,-11),(-2,-12),(-3,-13)]
deps4.cService.getCs() // [(-1,-11),(-2,-12),(-3,-13)]
let controllerab4 = ControllerAB(deps: deps4)
controllerab4.total() // -42
let controllerc4 = ControllerC(deps: deps4)
controllerc4.total() // -42
// or use a fake C directly:
public class FakeCService: CServicing {
public func getCs() -> [C] {
return [(0,0)]
}
}
let fc = FakeCService()
fc.getCs() // [(0,0)]
let deps5 = Deps.makeDeps(aService: a, bService: b)
deps5.cService.getCs() // [(1,11),(2,12),(3,13)]
deps5.cService = fc
deps5.cService.getCs() // [(0,0)]
let controllerab5 = ControllerAB(deps: deps5)
controllerab5.total() // 42
let controllerc5 = ControllerC(deps: deps5)
controllerc5.total() // 0
// Deps2.playground
import UIKit
// Design issues / decisions:
//
// - should "providing" protocols use funcs or vars?
// - i.e. `var serviceA` vs `func serviceA()`
//
// - for deps which themselves have deps ("internal nodes of the deps tree"),
// should they take a deps object or should we ban them from using the deps pattern?
// - i.e. if C depends on A and B, should we `class ServiceC { init(deps: Deps) }`
// or should we `class ServiceC { init(a: ServiceA, b: ServiceB) }`?
//
// - if internal nodes take a deps object, the deps object will run into a bootstrapping problem
// (`ServiceC.init()` needs a `Deps` and `Deps.init()` needs a `ServiceC`)
// - we can get around this by using force-unwrapped vars (and a static builder method)
// - or avoid the issue by banning internal nodes from using the deps pattern.
//
// - if internal nodes are banned from the deps pattern, we lose the ability to replace
// a dep at run-time and have every part of the system automatically start using the new
// dep (if ServiceC uses ServiceA (given via init), replacing Deps.serviceA
// at runtime with a fake does not affect ServiceC, which will still use the original A).
// This playground explores:
// - "providing" protocols use funcs
// - "internal" nodes do NOT use deps pattern
// - (there is no bootstrapping problem, but internal dep nodes don't get updated deps)
// This approach suffers from the fact that "internal" dep nodes have to be manually updated if deps change.
// service data types
public typealias A = Int
public typealias B = Int
public typealias C = (A,B)
// service protocols
public protocol AServicing {
func getAs() -> [A]
}
public protocol BServicing {
func getBs() -> [B]
}
public protocol CServicing {
func getCs() -> [C]
}
// service implementations
public class AService: AServicing {
public func getAs() -> [A] {
return [1,2,3]
}
}
public class BService: BServicing {
public func getBs() -> [B] {
return [11,12,13]
}
}
public class CService: CServicing {
public init(aService: AServicing, bService: BServicing) {
_aService = aService
_bService = bService
}
public func getCs() -> [C] {
return Array(zip(_aService.getAs(), _bService.getBs()))
}
private let _aService: AServicing
private let _bService: BServicing
}
// deps protocols
public protocol AServiceProviding {
var aService: AServicing { get }
}
public protocol BServiceProviding {
var bService: BServicing { get }
}
public protocol CServiceProviding {
var cService: CServicing { get }
}
// deps implementation
public class Deps: AServiceProviding, BServiceProviding, CServiceProviding {
public var aService: AServicing
public var bService: BServicing
public var cService: CServicing
public init(aService: AServicing, bService: BServicing, cService: CServicing) {
self.aService = aService
self.bService = bService
self.cService = cService
}
}
// Some objects which use the deps:
public class ControllerAB {
public let deps: AServiceProviding & BServiceProviding
public init(deps: AServiceProviding & BServiceProviding) {
self.deps = deps
}
public func total() -> Int {
return (deps.aService.getAs() + deps.bService.getBs())
.reduce(0, { $0 + $1 })
}
}
public class ControllerC {
public let deps: CServiceProviding
public init(deps: CServiceProviding) {
self.deps = deps
}
public func total() -> Int {
return deps.cService.getCs()
.reduce(0, { $0 + $1.0 + $1.1 })
}
}
// use real services:
let a = AService()
a.getAs() // [1,2,3]
let b = BService()
b.getBs() // [11,12,13]
let c = CService(aService: a, bService: b)
c.getCs() // [(1,11),(2,12),(3,13)]
let deps1 = Deps(aService: a, bService: b, cService: c)
let controllerab1 = ControllerAB(deps: deps1)
controllerab1.total() // 42
let controllerc1 = ControllerC(deps: deps1)
controllerc1.total() // 42
// fake A and B implementations:
public class FakeAService: AServicing {
public func getAs() -> [A] {
return [-1,-2,-3]
}
}
public class FakeBService: BServicing {
public func getBs() -> [B] {
return [-11,-12,-13]
}
}
// use fake A with real C:
let fa = FakeAService()
fa.getAs() // [-1,-2,-3]
let c2 = CService(aService: fa, bService: b)
c2.getCs() // [(-1,11),(-2,12),(-3,13)]
let deps2 = Deps(aService: fa, bService: b, cService: c2)
deps2.cService.getCs() // [(-1,11),(-2,12),(-3,13)]
let controllerab2 = ControllerAB(deps: deps2)
controllerab2.total() // 30
let controllerc2 = ControllerC(deps: deps2)
controllerc2.total() // 30
// fake B with real C:
let fb = FakeBService()
fb.getBs() // [-11,-12,-13]
let c3 = CService(aService: a, bService: fb)
c3.getCs() // [(1,-11),(2,-12),(3,-13)]
let deps3 = Deps(aService: a, bService: fb, cService: c3)
deps3.cService.getCs() // [(1,-11),(2,-12),(3,-13)]
let controller3 = ControllerAB(deps: deps3)
controller3.total() // -30
let controllerc3 = ControllerC(deps: deps3)
controllerc3.total() // -30
// fake A and fake B with real C:
let c4 = CService(aService: fa, bService: fb)
c4.getCs() // [(-1,-11),(-2,-12),(-3,-13)]
let deps4 = Deps(aService: fa, bService: fb, cService: c4)
deps4.cService.getCs() // [(-1,-11),(-2,-12),(-3,-13)]
let controllerab4 = ControllerAB(deps: deps4)
controllerab4.total() // -42
let controllerc4 = ControllerC(deps: deps4)
controllerc4.total() // -42
// or use a fake C directly:
public class FakeCService: CServicing {
public func getCs() -> [C] {
return [(0,0)]
}
}
let fc = FakeCService()
fc.getCs() // [(0,0)]
let deps5 = Deps(aService: a, bService: b, cService: fc)
deps5.aService.getAs() // [1,2,3]
deps5.bService.getBs() // [11,12,13]
deps5.cService.getCs() // [(0,0)]
let controllerab5 = ControllerAB(deps: deps5)
controllerab5.total() // 42
let controllerc5 = ControllerC(deps: deps5)
controllerc5.total() // 0
// problem: updating Deps with a fake A doesn't affect it's C:
let deps6 = Deps(aService: a, bService: b, cService: c)
deps6.aService.getAs() // [1,2,3]
deps6.cService.getCs() // [(1,11),(2,12),(3,13)]
deps6.aService = fa
deps6.aService.getAs() // [-1,-2,-3]
deps6.cService.getCs() // [(1,11),(2,12),(3,13)]
// we have to manually update C:
deps6.cService = CService(aService: fa, bService: b)
deps6.cService.getCs() // [(-1,11),(-2,12),(-3,13)]
// Deps3.playground
import UIKit
// Design issues / decisions:
//
// - should "providing" protocols use funcs or vars?
// - i.e. `var serviceA` vs `func serviceA()`
//
// - for deps which themselves have deps ("internal nodes of the deps tree"),
// should they take a deps object or should we ban them from using the deps pattern?
// - i.e. if C depends on A and B, should we `class ServiceC { init(deps: Deps) }`
// or should we `class ServiceC { init(a: ServiceA, b: ServiceB) }`?
//
// - if internal nodes take a deps object, the deps object will run into a bootstrapping problem
// (`ServiceC.init()` needs a `Deps` and `Deps.init()` needs a `ServiceC`)
// - we can get around this by using force-unwrapped vars (and a static builder method)
// - or avoid the issue by banning internal nodes from using the deps pattern.
//
// - if internal nodes are banned from the deps pattern, we lose the ability to replace
// a dep at run-time and have every part of the system automatically start using the new
// dep (if ServiceC uses ServiceA (given via init), replacing Deps.serviceA
// at runtime with a fake does not affect ServiceC, which will still use the original A).
// This playground explores:
// - "providing" protocols use funcs
// - "internal" nodes use deps pattern
// - use force-unwrap to work-around the bootstrapping problem
// This approach works, but the disadvantages are:
// - you have to use a static factory method: Deps.makeDeps()
// - the awkward property / func naming conflicts (e.g. `var aService` and `func aService()`)
// service data types
public typealias A = Int
public typealias B = Int
public typealias C = (A,B)
// service protocols
public protocol AServicing {
func getAs() -> [A]
}
public protocol BServicing {
func getBs() -> [B]
}
public protocol CServicing {
func getCs() -> [C]
}
// service implementations
public class AService: AServicing {
public func getAs() -> [A] {
return [1,2,3]
}
}
public class BService: BServicing {
public func getBs() -> [B] {
return [11,12,13]
}
}
public class CService: CServicing {
public init(deps: AServiceProviding & BServiceProviding) {
_deps = deps
}
public func getCs() -> [C] {
return Array(zip(_deps.aService().getAs(), _deps.bService().getBs()))
}
private let _deps: AServiceProviding & BServiceProviding
}
// deps protocols
public protocol AServiceProviding {
func aService() -> AServicing
}
public protocol BServiceProviding {
func bService() -> BServicing
}
public protocol CServiceProviding {
func cService() -> CServicing
}
// deps implementation
public class Deps: AServiceProviding, BServiceProviding, CServiceProviding {
public var aService_: AServicing
public var bService_: BServicing
public var cService_: CServicing!
public func aService() -> AServicing {
return aService_
}
public func bService() -> BServicing {
return bService_
}
public func cService() -> CServicing {
return cService_
}
public init(aService: AServicing, bService: BServicing) {
aService_ = aService
bService_ = bService
}
public static func makeDeps(
aService: AServicing,
bService: BServicing,
cService: CServicing? = nil
) -> Deps {
let deps = Deps(aService: aService, bService: bService)
deps.cService_ = cService ?? CService(deps: deps)
return deps
}
}
// Some objects which use the deps:
public class ControllerAB {
public let deps: AServiceProviding & BServiceProviding
public init(deps: AServiceProviding & BServiceProviding) {
self.deps = deps
}
public func total() -> Int {
return (deps.aService().getAs() + deps.bService().getBs())
.reduce(0, { $0 + $1 })
}
}
public class ControllerC {
public let deps: CServiceProviding
public init(deps: CServiceProviding) {
self.deps = deps
}
public func total() -> Int {
return deps.cService().getCs()
.reduce(0, { $0 + $1.0 + $1.1 })
}
}
// use real services:
let a = AService()
a.getAs() // [1,2,3]
let b = BService()
b.getBs() // [11,12,13]
let deps1 = Deps.makeDeps(aService: a, bService: b)
let c = CService(deps: deps1)
c.getCs() // [(1,11),(2,12),(3,13)]
deps1.cService().getCs() // [(1,11),(2,12),(3,13)]
let controllerab1 = ControllerAB(deps: deps1)
controllerab1.total() // 42
let controllerc1 = ControllerC(deps: deps1)
controllerc1.total() // 42
// fake A and B implementations:
public class FakeAService: AServicing {
public func getAs() -> [A] {
return [-1,-2,-3]
}
}
public class FakeBService: BServicing {
public func getBs() -> [B] {
return [-11,-12,-13]
}
}
// use fake A with real C:
let fa = FakeAService()
fa.getAs() // [-1,-2,-3]
let deps2 = Deps.makeDeps(aService: fa, bService: b)
let c2 = CService(deps: deps2)
c2.getCs() // [(-1,11),(-2,12),(-3,13)]
deps2.cService().getCs() // [(-1,11),(-2,12),(-3,13)]
let controllerab2 = ControllerAB(deps: deps2)
controllerab2.total() // 30
let controllerc2 = ControllerC(deps: deps2)
controllerc2.total() // 30
// fake B with real C:
let fb = FakeBService()
fb.getBs() // [-11,-12,-13]
let deps3 = Deps.makeDeps(aService: a, bService: fb)
let c3 = CService(deps: deps3)
c3.getCs() // [(1,-11),(2,-12),(3,-13)]
deps3.cService().getCs() // [(1,-11),(2,-12),(3,-13)]
let controller3 = ControllerAB(deps: deps3)
controller3.total() // -30
let controllerc3 = ControllerC(deps: deps3)
controllerc3.total() // -30
// fake A and fake B with real C:
let deps4 = Deps.makeDeps(aService: fa, bService: fb)
let c4 = CService(deps: deps3)
c4.getCs() // [(-1,-11),(-2,-12),(-3,-13)]
deps4.cService().getCs() // [(-1,-11),(-2,-12),(-3,-13)]
let controllerab4 = ControllerAB(deps: deps4)
controllerab4.total() // -42
let controllerc4 = ControllerC(deps: deps4)
controllerc4.total() // -42
// or use a fake C directly:
public class FakeCService: CServicing {
public func getCs() -> [C] {
return [(0,0)]
}
}
let fc = FakeCService()
fc.getCs() // [(0,0)]
let deps5 = Deps.makeDeps(aService: a, bService: b)
deps5.cService().getCs() // [(1,11),(2,12),(3,13)]
deps5.cService_ = fc
deps5.cService().getCs() // [(0,0)]
let controllerab5 = ControllerAB(deps: deps5)
controllerab5.total() // 42
let controllerc5 = ControllerC(deps: deps5)
controllerc5.total() // 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment