Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Dependency injection and its friends

Dependency injection and its friends

How to use DI in Swift

Vlad Vysotsky

August 24, 2017
Tweet

More Decks by Vlad Vysotsky

Other Decks in Technology

Transcript

  1. Constructor injection: the dependencies are provided through a class constructor.

    protocol Service { } class ServiceImplementation: Service { } class Client { private let service: Service init(service: Service) { self.service = service } }
  2. Property injection: the client exposes a property that the injector

    uses to inject the dependency. protocol Service { } class ServiceImplementation: Service { } class Client { var service: Service? } let client = Client() client.service = ServiceImplementation()
  3. protocol Service { } class ServiceImplementation: Service { } class

    Client { func loadConfig(from url: URL?, with service: Service) { //It does something } } let client = Client() let service = ServiceImplementation() client.loadConfig(from: URL(string: "example.com"), with: service) Method injection: the dependency provides an injector method that will inject the dependency into any client passed to it.
  4. Make your code a better place class DocumentLoader { static

    let shared = DocumentLoader() private lazy var cache: NSCache<NSString, Document> = { let cache = NSCache<NSString, Document>() cache.totalCostLimit = 10 return cache }() func loadDocument(by name: String) throws -> Document { if let document = cache.object(forKey: name as NSString) { return document } guard let url = Bundle.main.url(forResource: name, withExtension: nil) else { throw NSError(domain: "com.myapp.DocumentLoader", code: -100, userInfo: nil) } let document = Document(data: try Data(contentsOf: url)) cache.setObject(document, forKey: name as NSString, cost: 1) return document } }
  5. Agreed input and output 1. Optionals are used in a

    smart way 2. Think how you can mock the dependencies of the method 3. All errors are predefined 4. The flow are under control (no aborts or fatal errors inside)
  6. Agreed input and output enum DocumentLoaderError: Error { case invalidName(String)

    case invalidData(URL) } ... func loadDocument(by name: String) throws -> Document { if let document = cache.object(forKey: name as NSString) { return document } guard let url = Bundle.main.url(forResource: name, withExtension: nil) else { throw DocumentLoaderError.invalidName(name) } let document: Document do { document = Document(data: try Data(contentsOf: url)) } catch { throw DocumentLoaderError.invalidData(url) } cache.setObject(document, forKey: name as NSString, cost: 1) return document }
  7. Shared state 1. Construct objects when you need them 2.

    Singletones are sharing their state between different places and even queues 3. Creating an object, using it and then its deallocating can be better than having this object during the app lifetime
  8. Injected external dependencies 1. Composition principle 2. Protocols define a

    contract how to use an object 3. There is an ability to build a dependency graph and then resolve it
  9. The scope defines how the object will be created and

    delivered/passed to other objects which require this one to work with
  10. Common scopes 1. Default scope (graph) 2. Singleton scope 3.

    Custom-created scopes a. `Weak` scopes b. `Clean` scopes c. `User` scopes d. Scopes depending on life cycle
  11. Typhoon + Popular + Under active development - Hard to

    start using - Uses ObjC runtime for resolving - Heavyweight - No type checks
  12. Swinject + Pure swift + Doesn’t require to hardcode itself

    into the project + Under active development + Lots of extensioms - Graph resolves in the runtime - No Obj-C support
  13. Cleanse + Pure swift + Lightweight + From Dagger 2

    creators - Graph resolves in the runtime - No Obj-C support - Needs to be hardcoded into the project using lots of its own abstractions
  14. let container = Container() container.register(Animal.self) { _ in Cat(name: "Mimi")

    } container.register(Person.self) { r in PetOwner(pet: r.resolve(Animal.self)!) } How to declare a container
  15. Transient If ObjectScope.transient is specified, an instance provided by a

    container is not shared. In other words, the container always creates a new instance when the type is resolved. Graph (the default scope) With ObjectScope.graph, an instance is always created, as in ObjectScope.transient, if you directly call resolve method of a container, but instances resolved in factory closures are shared during the resolution of the root instance to construct the object graph. Container In ObjectScope.container, an instance provided by a container is shared within the container and its child containers. Weak In ObjectScope.weak an instance provided by a container is shared within the container and its child containers as long as there are other strong references to it. Once all strong references to an instance cease to exist, it won't be shared anymore and new instance will be created during next resolution of the type. Provided scopes
  16. class ServiceAssembly: Assembly { func assemble(container: Container) { container.register(FooServiceProtocol.self) {

    r in return FooService() } container.register(BarServiceProtocol.self) { r in return BarService() } } } class ManagerAssembly: Assembly { func assemble(container: Container) { container.register(FooManagerProtocol.self) { r in FooManager(service: r.resolve(FooServiceProtocol.self)!) } container.register(BarManagerProtocol.self) { r in return BarManager(service: r.resolve(BarServiceProtocol.self)!) } } } let assembler = Assembler([ ServiceAssembly(), ManagerAssembly() ], propertyLoaders: [ JsonPropertyLoader(bundle: .mainBundle(), name: "properties") ]) Modularizing services
  17. QA