• Stars
    star
    100
  • Rank 340,703 (Top 7 %)
  • Language
    Swift
  • License
    MIT License
  • Created over 7 years ago
  • Updated about 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

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

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:

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:

//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

More Repositories

1

ScrollingPagerIndicator

Pager indicator inspired by Instagram. Lightweight and easy to set up.
Java
561
star
2

decoro

Android library designed for automatic formatting of text input by custom rules
Java
373
star
3

tinkoff-asdk-android

Kotlin
56
star
4

tinkoff-asdk-ios

Tinkoff Acquiring SDK
Swift
45
star
5

tinkoff-asdk-ios-old

Tinkoff Acquiring SDK for iOS
Objective-C
36
star
6

tinkoff-asdk-android-old

Tinkoff Acquiring SDK for Android
Java
33
star
7

TinkoffConcurrency

A toolset that makes Swift Concurrency a bit easier
Swift
33
star
8

droidherd

DroidHerd service - k8s android farm orchestration service
Kotlin
31
star
9

TiRecycler

Coolest way to work with Recycler and Adapter
Kotlin
24
star
10

TinkoffMockStrapping

Library for unifying the approach to network mocking in iOS unit- & UI-tests.
Swift
24
star
11

tcscustomrowactionfactory

TCSTableViewRowActionFactory allows you to setup the swipe actions for cells in a table view using UIView and some other convenient methods
Swift
24
star
12

TinkoffID-iOS

SDK авторизации iOS приложений через Тинькофф
Swift
19
star
13

EBA

EBA Architecture Sample
Kotlin
17
star
14

KoTEA

TEA architecture on coroutines
Kotlin
10
star
15

vkarmane-sdk-android

Kotlin
7
star
16

iosDynamicLibraryBuilder

Scripts to build dynamic versions of iOS libraries from static ones
Ruby
7
star
17

fastlane-plugin-compilation_time_report

Generate custom HTML report compilation time of each Swift func
Ruby
5
star
18

TinkoffID-Android

SDK авторизации Android приложений через Тинькофф
Kotlin
5
star
19

vkarmane-sdk-ios

Swift
5
star
20

tisdk-ios

Tinkoff Insurance SDK for iOS
Objective-C
4
star
21

InjectionsCheck

Small library to check injections into Swift objects
Swift
4
star
22

demo-tisdk

Kotlin
2
star