Effective DI library for rapid development in 200 lines of code.

Overview

EasyDi

CI Status Version Carthage Compatible License Platform Swift Version

Effective DI library for rapid development in 200 lines of code.

Requirements

Swift 5+, iOS 10.3+

Example

To run the example project, clone the repo, and run pod install from the Example directory first.

Installation

EasyDi is available through CocoaPods. To install it, simply add the following line to your Podfile:

pod "EasyDi"

Author

Andrey Zarembo

e-mail: [email protected]

twitter: @andreyzarembo

telegram: @andreyzarembo

Alexey Markov

e-mail: [email protected]

telegram: @big_bada_booooom

License

EasyDi is available under the MIT license. See the LICENSE file for more info.

About

Dependency inversion is very important if project contains more than 5 screens and will be supported for more than a year. Here are three basic scenarios where DI makes life better:

  • Parallel development. One developer will be able to deal with UI, while another one will work with data layer. UI can be developed with test data, and the data layer can be called from the test UI.

  • Tests. By substituting the network layer with stub responses, you can check all the options of UI behavior, including error cases.

  • Refactor. The network layer can be replaced with a new, fast version with a cache and another API, if you leave the protocol with the UI unchanged.

The essence of DI can be described in one sentence: Dependencies for objects should be closed by the protocol and passed to the object when creating from the outside. Instead of

class OrderViewController {
  func didClickShopButton(_ sender: UIButton?) {
    APIClient.sharedInstance.purchase(...)
  }
}

this approach should be used

protocol IPurchaseService {
  func perform(...)
}

class OrderViewController {
  var purchaseService: IPurchaseService?
  func didClickShopButton(_ sender: UIButton?) {
    self.purchaseService?.perform(...)
  }
}

More details with the principle of dependency inversion and the SOLID concept can be found here and here.

EasyDi contains a dependency container for Swift. The syntax of this library was specially designed for rapid development and effective use. It fits in 200 lines, thus can do everything you need for grown-up DI library:

  • Objects creation with dependencies and injection of dependencies into existing ones
  • Separation into containers - Assemblies
  • Types of dependency resolution: objects graph, singleton, prototype
  • Objects substitution and dependency contexts for tests

There are no register / resolve methods in EasyDi. Instead of this, the dependencies are described like this:

var apiClient: IAPIClient {
  return define(init: APIClient()) {
    $0.baseURl = self.baseURL
    return $0
  }
}

Due to this approach it is possible to resolve circular dependencies and use already existing objects.

How to work with EasyDi (A simple example)

Task: move the work with the network from the ViewController to the services and place their creation and dependencies in a separate container. This is a simple and effective way to begin dividing your application into layers. In this example we'll use the service and the viewcontroller from the above example.

PurchaseService:

Void) { guard let apiClient = self.apiClient, let url = self.baseURL else { fatalError("Trying to do something with uninitialized purchase service") } let purchaseURL = baseURL.appendingPathComponent(self.apiPath).appendingPathComponent(objectId) let urlRequest = URLRequest(url: purchaseURL) self.apiClient.post(urlRequest) { (_, error) in let success: Bool = (error == nil) completion( success ) } } }">
protocol IPurchaseService {
  func perform(with objectId: String, then completion: (success: Bool)->Void)
}    

class PurchaseService {

  var baseURL: URL?
  var apiPath = "/purchase/"
  var apiClient: IAPIClient?

  func perform(with objectId: String, then completion: (_ success: Bool) -> Void) {

    guard let apiClient = self.apiClient, let url = self.baseURL else {
      fatalError("Trying to do something with uninitialized purchase service")
    }
    let purchaseURL = baseURL.appendingPathComponent(self.apiPath).appendingPathComponent(objectId)
    let urlRequest = URLRequest(url: purchaseURL)
    self.apiClient.post(urlRequest) { (_, error) in
      let success: Bool = (error == nil)
        completion( success )
    }
  }
}

ViewController:

class OrderViewController: ViewController {

  var purchaseService: IPurchaseService?
  var purchaseId: String?

  func didClickShopButton(_ sender: UIButton?) {

    guard let purchaseService = self.purchaseService, let purchaseId = self.purchaseId else {
      fatalError("Trying to do something with uninitialized OrderViewController")
    }

    self.purchaseService.perform(with: self.purchaseId) { (success) in
      self.presenter(showOrderResult: success)
    }
  }
}

Service dependencies assembly:

class ServiceAssembly: Assembly {

  var purchaseService: IPurchaseService {
    return define(init: PurchaseService()) {
      $0.baseURL = self.apiV1BaseURL
      $0.apiClient = self.apiClient
      return $0
    }
  }

  var apiClient: IAPIClient {
    return define(init: APIClient())
  }

  var apiV1BaseURL: URL {
    return define(init: URL("http://someapi.com/")!)
  }
}

And this is how we inject the service in the viewcontroller:

class OrderViewAssembly: Assembly {

  lazy var serviceAssembly: ServiceAssembly = self.context.assembly()


  func inject(into controller: OrderViewController, purchaseId: String) {
    let _:OrderViewController = define(init: controller) {
      $0.purchaseService = self.serviceAssembly.purchaseService
      $0.purchaseId = purchaseId
      return $0
    }
  }
}

Now you can change the class of the service without touching the OrderViewController code.

Dependency resolution types (Example of average complexity)

ObjectGraph

By default, all dependencies are resolved through the graph of the objects. If the object already exists on the stack of the current object graph, it is used again. This allows us to inject the same object into several objects, and also allow cyclic dependencies. For example, consider the classes A, B and C with links A-> B-> C. (Do not pay attention to RetainCycle).

class A {
  var b: B?
}

class B {
  var c: C?
}

class C {
  var a: A?
}

This is how Assembly looks

class ABCAssembly: Assembly {

  var a:A {
    return define(init: A()) {
      $0.b = self.B()
      return $0
    }
  }

  var b:B {
    return define(init: B()) {
      $0.c = self.C()
      return $0
    }
  }

  var c:C {
    return define(init: C()) {
      $0.a = self.A()
      return $0
    }
  }
}

and here is a dependency graph for two requests of A class instance

var a1 = ABCAssembly.instance().a
var a2 = ABCAssembly.instance().a

Two independent graphs were obtained.

Singleton

But it happens that you need to create a single object, which will then be used everywhere, e.g.: the analytics system or the storage. We don't recommend to use well-known SharedInstance static property of Singleton class, since it will not be possible to replace it. For these purposes, there is a special scope in EasyDi: lazySingleton. The object with 'lazySingleton' scope is created once and its dependencies are injected once. Besides EasyDi does not change that object after creation. For example, we make a singleton of class B.

class ABCAssembly: Assembly {
  var a:A {
    return define(init: A()) {
      $0.b = self.B()
      return $0
    }
  }

  var b:B {
    return define(scope: .lazySingleton, init: B()) {
      $0.c = self.C()
      return $0
    }
  }

  var c:C {
    return define(init: C()) {
      $0.a = self.A()
      return $0
    }
  }
}
var a1 = ABCAssembly.instance().a
var a2 = ABCAssembly.instance().a

This time, one object graph was obtained, because B instance became shared singleton. Since we don't recreate (rebuild) objects with 'lazySingleton' scope, instance of B didn't change its dependencies after 'var a2 = ABCAssembly...'

Prototype

Sometimes each request requires a new object. If we specify 'prototype' scope for the A class instance in our example we will get:

class ABCAssembly: Assembly {
  var a:A {
    return define(scope: .prototype, init: A()) {
      $0.b = self.B()
      return $0
    }
  }

  var b:B {
    return define(init: B()) {
      $0.c = self.C()
      return $0
    }
  }

  var c:C {
    return define(init: C()) {
      $0.a = self.A()
      return $0
    }
  }
}
var a1 = ABCAssembly.instance().a
var a2 = ABCAssembly.instance().a

As a result two graphs of objects are created with 4 copies of object A

It is important to understand that the 'prototype' object is the entry point to the object graph. If you combine prototypes in a loop, the dependency stack will overflow and the application will fall.

Substitutions and contexts for tests (A complex example)

When testing, it is important to maintain test independence. In EasyDi this property is provided by Assemblies contexts. For example, integration tests with singleton objects. Usage example:

let context: DIContext = DIContext()
let assemblyInstance2 = TestAssembly.instance(from: context)

It is important to ensure that peer assemblies have the same context.

class FeedViewAssembly: Assembly {

  lazy var serviceAssembly:ServiceAssembly = self.context.assembly()

}

Another important part of testing are mocks and stubs, that is, objects with defined behavior. With known input data, the object under the test produces a known result. If object does not produce it, then the test fails. More information about testing can be found here. And here's how you can replace the dependency in the object to be tested:

ITheObject in let result = FakeTheObject() result.intParameter = 30 return result }">
//Production code
protocol ITheObject {
  var intParameter: Int { get }
}

class MyAssembly: Assembly {

  var theObject: ITheObject {
    return define(init: TheObject()) {
      $0.intParameter = 10
      return $0
    }
  }
}

//Test code
let myAssembly = MyAssembly.instance()
myAssembly.addSubstitution(for: "theObject") { () -> ITheObject in
  let result = FakeTheObject()
  result.intParameter = 30
  return result
}

Now the theObject property will return stub object of another type with another intParameter.

The same mechanism can be used for A / B testing in the application. For example:

let FeatureAssembly: Assembly {

  var feature: IFeature {
    return define(init: Feature) {
      ...
      return $0
    }
  }
}

let FeatureABTestAssembly: Assembly {

  lazy var featureAssembly: FeatureAssembly = self.context.assembly()

  var feature: IFeature {
    return define(init: FeatureV2) {
      ...
      return $0
    }
  }

  func activate(firstTest: Bool) {
    if (firstTest) {
      self.featureAssembly.addSubstitution(for: "feature") {
        return self.feature
      }
    } else {
      self.featureAssembly.removeSubstitution(for: "feature")
    }
  }
}

In this example a separate container is created for the test. That container creates a second variant of the feature and allows to enable / disable the substitution of the feature.

Dependency injection in the VIPER (Complex example)

It happens that it is necessary to inject dependencies into an existing object, while some other objects depend on it. The simplest example is VIPER, when the Presenter should be added to the ViewController, and it should get a pointer to the ViewController itself.

For this case, EasyDi has 'keys' with which you can return the same object from different methods. It looks like this:

сlass ModuleAssembly: Assembly {

  func inject(into view: ModuleViewController) {
    return define(key: "view", init: view) {
      $0.presenter = self.presenter
      return $0
    }
  }

  var view: IModuleViewController {
    return definePlaceholder()
  }

  var presenter: IModulePresenter {
    return define(init: ModulePresenter()) {
	    $0.view = self.view
      $0.interactor = self.interactor
      return $0
    }
  }

  var interactor: IModuleInteractor {
    return define(init: ModuleInteractor()) {
	    $0.presenter = self.presenter
      ...
      return $0
    }
  }
}

Here, to implement dependencies in the ViewController, the inject method is used, which is linked by the key with the 'view' property. Now, this property returns the object passed to the 'inject' method. Thus, the VIPER module assembly is initiated with 'inject' method.

Road map

  • drop old swift version
  • Swift5+
  • weak singleton
  • update docs
  • SPM
  • to be continue
Comments
  • Поддержка многопоточности

    Поддержка многопоточности

    Привет, текущая реализация DIContext-a судя по всему, не предназначена для использования одновременно из нескольких потоков без дополнительной синхронизации "снаружи". Т.е. создавать объекты из разных потоков используя Assembly получается небезопасно.

    Есть возможность доработать существующий класс контекста для включения "потокобезопасности" либо выделение нового класса потокобезопасного контекста?

    enhancement 
    opened by SergeyKrupov 4
  • Weird NSObject dealloc with Swift 4

    Weird NSObject dealloc with Swift 4

    Swift 4.

    Due to unknown reason NSObjects, that was used with anonymous variables in closures tend to dealloc by unknown reason.

    E.g. example app:

    func inject(into feedViewController: FeedViewController) {
            defineInjection(into: feedViewController) {
                $0.xkcdService = self.serviceAssembly.xkcdService
                $0.imageService = self.serviceAssembly.imageService
            }
        }
    

    Will crash later, because feedViewController deallocated, and variables to it now works like unsafe unretained.

    But this will work:

    func inject(into feedViewController: FeedViewController) {
            defineInjection(into: feedViewController) { (_ feedViewController: inout FeedViewController) in
                feedViewController.xkcdService = self.serviceAssembly.xkcdService
                feedViewController.imageService = self.serviceAssembly.imageService
            }
        }
    

    I've tried to make test, which reproduces such behaviour, but it passed normally.

    bug help wanted Swift 4 
    opened by AndreyZarembo 3
  • Package.swift update

    Package.swift update

    Привет 👋

    Обновил Package.swift, чтобы можно было добавить EasyDi через SPM.

    Потому что было вот так, когда я выставлял зависимость на конкретный комит: Screenshot 2020-02-28 at 20 28 03

    А при указании версии он отваливался с такой ошибкой: Screenshot 2020-02-28 at 20 44 20

    Этот кусок // swift-tools-version:5.0 должен идти первой строкой судя по всему. Иначе он его не считывает, если лежит после копирайта.

    opened by wow-such-amazing 1
  • Handle unbehaviour singleton duplication in incorrect dependencies graph

    Handle unbehaviour singleton duplication in incorrect dependencies graph

    Hi!

    Our team faced a problem with EasyDi, which allow to create two (or more) instance of singleton. Basic example to reproduce:

    class SomeObjectA {
        let objectB: SomeObjectB
        
        init(objectB: SomeObjectB) {
            self.objectB = objectB
        }
    }
    
    class SomeObjectB {
        var objectA: SomeObjectA?
    }
    
    class TestAssembly: Assembly {
        var objectA: SomeObjectA {
            return define(scope: .lazySingleton, init: SomeObjectA(objectB: self.objectB))
        }
        
        var objectB: SomeObjectB {
            return define(scope: .lazySingleton, init: SomeObjectB()) {
                // on this step self.objectA currently not initialized, so EasyDi create a new SomeObjectA instance here
                $0.objectA = self.objectA
                return $0
            }
        }
    }
    

    SomeObjectA has a dependency on a SomeObjectB and inject it over init, also SomeObjectB depends on a SomeObjectA and inject it over property injection.

    let objectA = assembly.objectA
    let objectB = assembly.objectB
    print(objectA, objectB.objectA!) // <SomeObjectA: 0x7fcf9c730f80>, <SomeObjectA: 0x7fcf9c7316a0>
    

    After resolve dependency graph we have two SomeObjectA instance and it contradicts singleton scope. EasyDi should handle this case and throw fatalError/NSException (NSException is preferred for testing).

    For fix this graph needs inject all properties over property injection:

    fileprivate class SomeObjectA {
        var objectB: SomeObjectB?
    }
    
    fileprivate class SomeObjectB {
        var objectA: SomeObjectA?
    }
    
    fileprivate class TestAssembly: Assembly {
        var objectA: SomeObjectA {
            return define(scope: .lazySingleton, init: SomeObjectA()) {
                $0.objectB = self.objectB
                return $0
            }
        }
        
        var objectB: SomeObjectB {
            return define(scope: .lazySingleton, init: SomeObjectB()) {
                $0.objectA = self.objectA
                return $0
            }
        }
    }
    
    let objectA = assembly.objectA
    let objectB = assembly.objectB
    print(objectA, objectB.objectA!) // <SomeObjectA: 0x7faac9e34300>, <SomeObjectA: 0x7faac9e34300>
    

    Also I added Test_SingletonDuplication test and check for NSException is raised when EasyDi created more that 1 singleton instance (by invalid dependencies graph).

    opened by rock88 0
  • fix optional unwrapping for new compiler version

    fix optional unwrapping for new compiler version

    'If value is nil and the type T is an Optional type at runtime, the return value will now be .some(nil) rather than nil. This behavior is consistent with the casting behavior when concrete types are used rather than generic types. (40916953)'

    opened by dvi 0
  • Swift 4 support

    Swift 4 support

    Once I moved to Swift 4 I started receive crashes with bugs as this issue

    I did some research and found this document. In short it describes implemented in Swift 4 proposal to guarantee safer memory access in Swift 4 which I believe influenced logic of 'inout' attribute and escaping closures widely used in Assembly class. At the end of the doc author wrote that for the safer swift they are ready to sacrifice ABI compatibility. To bring our DI library to live we had to change 'define' method signature.

    I also let myself to correct some comments in EasyDi.swift file

    opened by alekseykolchanov 0
Releases(1.6.2)
Owner
Tinkoff.ru
Online financial ecosystem
Tinkoff.ru
Inject Dylib - Swift code to programmatically perform dylib injection

Inject_Dylib Swift code to programmatically perform dylib injection. You can als

Cedric Owens 40 Sep 27, 2022
Tranquillity is a lightweight but powerful dependency injection library for swift.

DITranquillity Tranquillity is a lightweight but powerful dependency injection library for swift. The name "Tranquillity" laid the foundation in the b

Ivlev Alexander 393 Dec 24, 2022
A library to inject your dependencies via property wrappers

?? DependencyInjection A library to inject your dependencies via property wrappers ?? Features DependencyInjection allows you to define the dependenci

Alberto Garcia 4 Dec 10, 2022
Toledo - a dependency injection library for Swift that statically generates resolvers at compile-time.

Toledo Toledo is a dependency injection library for Swift that statically generates resolvers at compile-time. Index Features Installation Usage Licen

Valentin Radu 18 Nov 25, 2022
A Swift package for rapid development using a collection of micro utility extensions for Standard Library, Foundation, and other native frameworks.

ZamzamKit ZamzamKit is a Swift package for rapid development using a collection of micro utility extensions for Standard Library, Foundation, and othe

Zamzam Inc. 261 Dec 15, 2022
A light weight network library with automated model parser for rapid development

Gem A light weight network library with automated model parser for rapid development. Managing all http request with automated model parser calls in a

Albin CR 10 Nov 19, 2022
ROAD – Rapid Objective-C Applications Development

A set of reusable components taking advantage of extra dimension Attribute-Oriented Programming adds. Components Core - support for attributes, reflec

EPAM Systems 54 Nov 19, 2022
A Swift DSL that allows concise and effective concurrency manipulation

NOTE Brisk is being mothballed due to general incompatibilities with modern version of Swift. I recommend checking out ReactiveSwift, which solves man

Jason Fieldman 25 May 24, 2019
A validator for postal codes with support for 200+ regions

PostalCodeValidator A validator for postal codes with support for 200+ regions. import Foundation import PostalCodeValidator if let validator = Posta

FormatterKit 211 Jun 17, 2022
Dash-iOS - Dash gives your iPad and iPhone instant offline access to 200+ API documentation sets

Discontinued Dash for iOS was discontinued. Please check out Dash for macOS instead. Dash for iOS Dash gives your iPad and iPhone instant offline acce

Bogdan Popescu 7.1k Dec 29, 2022
📱 A comprehensive test task for creating an autolayout interface, requesting an API and JSON parsing from Effective Mobile.

ECOMMERCE A comprehensive test task for creating an autolayout interface, requesting an API and JSON parsing from Effective Mobile. ??‍?? Design ✨ Fea

Daniel Tvorun 4 Nov 21, 2022
⏲ A tiny package to measure code execution time. Only 20 lines of code.

Measure ⏲ A tiny package to measure code execution time. Measure.start("create-user") let user = User() Measure.finish("create-user") Console // ⏲ Mea

Mezhevikin Alexey 3 Oct 1, 2022
A very useful and unique iOS library to open image picker in just few lines of code.

ImagePickerEasy A very simple solution to implement UIImagePickerController() in your application. Requirements Swift 4.2 and above Installation Image

wajeehulhassan 6 May 13, 2022
Magical Data Modeling Framework for JSON - allows rapid creation of smart data models. You can use it in your iOS, macOS, watchOS and tvOS apps.

JSONModel - Magical Data Modeling Framework for JSON JSONModel allows rapid creation of smart data models. You can use it in your iOS, macOS, watchOS

JSONModel 6.9k Dec 8, 2022
Magical Data Modeling Framework for JSON - allows rapid creation of smart data models. You can use it in your iOS, macOS, watchOS and tvOS apps.

JSONModel - Magical Data Modeling Framework for JSON JSONModel allows rapid creation of smart data models. You can use it in your iOS, macOS, watchOS

JSONModel 6.8k Nov 19, 2021
Flexible JSON traversal for rapid prototyping

RBBJSON RBBJSON enables flexible JSON traversal at runtime and JSONPath-like que

Robb Böhnke 164 Dec 27, 2022
A rapid prototype of UISwitch built with Swift and PaintCode.

LTJelloSwitch A cute UISwitch prototype built in about 8 hours. One of my co-worker gave me this concept last year. It was written in Objective-C and

Lex Tang 366 Aug 5, 2022
Store and retrieve Codable objects to various persistence layers, in a couple lines of code!

tl;dr You love Swift's Codable protocol and use it everywhere, who doesn't! Here is an easy and very light way to store and retrieve Codable objects t

null 149 Dec 15, 2022
Get the data from Accelerometer, Gyroscope and Magnetometer in only Two or a few lines of code.

Get the data from Accelerometer, Gyroscope and Magnetometer in only Two or a few lines of code. CoreMotion now made insanely simple :octocat: :satellite:

Muhammad Haroon Baig 1.1k Nov 16, 2022
An iOS framework to easily create a beautiful and engaging onboarding experience with only a few lines of code.

Onboard Click Here For More Examples Important Onboard is no longer under active development, and as such if you create any issues or submit pull requ

Mike 6.5k Dec 17, 2022