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

Dependency Injection in Practice (Appdevcon)

Dependency Injection in Practice (Appdevcon)

Appdevcon 2017 in Amsterdam, March 16-17, 2017
http://appdevcon.nl/

Yoichi Tagaya

March 17, 2017
Tweet

More Decks by Yoichi Tagaya

Other Decks in Programming

Transcript

  1. Yoichi Tagaya w J04&OHJOFFSBU w 0OFPG4QPOTPSTPG"QQEFWDPO w $$.BSLFUQMBDFBQQ w +164.%-T

    6,3FMFBTFE w )JSJOHBU  4BO'SBODJTDP  -POEPO  5PLZP w 5XJUUFS (JU)VC!ZPJDIJUHZ
  2. I'm an Author of Swinject • Dependency Injection framework for

    Swift • Maintained by 5 members • 1,3k+ stars in GitHub • Supporting iOS, macOS, tvOS, watchOS and Linux • 4 extensions provided • github.com/Swinject/Swinject
  3. Dependency Injection in Practice 1. Basic Concepts 2. Example Program

    Applying Dependency Injection 3. Advanced Features 5PEBZT5BML
  4. Introduction • Reduce Technical Debt • Asset Catalogs • Dependency

    Injection • Live Playgrounds "Improving Existing Apps with Modern Best Practices"
 by Woody L. at WWDC 2016
  5. What's Dependency Injection Inversion of Control for resolving dependencies I'm

    talking to you. ↺ You're talking to me. You call libraries. ↺ Frameworks call you. You create what you use. ↺ What you use are created and passed.
  6. Inversion of Control class Person { let pet = Cat()

    } You create what you use What you use are created and passed class Person1 { let pet: Cat init(pet: Cat) { self.pet = pet } } let p1 = Person1(pet: Cat()) class Person2 { var pet: Cat? } let p2 = Person2() p2.pet = Cat() class Person3 { func play(pet: Cat) { // Play with pet } } let p3 = Person3() p3.play(pet: Cat()) *OJUJBMJ[FS*OKFDUJPO $POTUSVDUPS*OKFDUJPO 1SPQFSUZ*OKFDUJPO .FUIPE*OKFDUJPO
  7. Coupling of Dependency class Person { let pet = Cat()

    } Tight coupling Loose coupling • Only a cat can be a pet • Not flexible
  8. Coupling of Dependency class Person { let pet = Cat()

    } protocol Animal { } class Cat: Animal { } class Dog: Animal { } class Person4 { let pet: Animal init(pet: Animal) { self.pet = pet } } let catPerson = Person4(pet: Cat()) let dogPerson = Person4(pet: Dog()) Tight coupling Loose coupling • Only a cat can be a pet • Not flexible • Any animal can be a pet • Flexible
  9. Main Problems to Develop a Large App • Event handling

    - Functional Reactive Programming - ReactiveSwift - RxSwift
  10. Main Problems to Develop a Large App • State management

    - Complicated problem - Caused by dependency often - Dependency injection is a solution (but not the only)
  11. Login State Management Singleton account manager named like: • class

    AccountManager • class LoginManager • class UserAccount Used as: if AccountManager.shared.isLoggedIn { // Do things } else { // Do other things }
  12. Problems of Singleton Login Manager • Tightly coupled with external

    login service. • Difficult to write unit tests. • Log in/out your app again and again during development.
  13. Let’s Dive into the Code 1. Implementation with Account Manager

    singleton. 2. Refactoring with Dependency Injection. 3. Unit testing.
  14. final class LocalAccountManager { static let shared = LocalAccountManager() private

    init() { } private (set) var currentUser: User? var isLoggedIn: Bool { return currentUser != nil } } LocalAccountManager Singleton 4JOHMFUPO
  15. final class LocalAccountManager { static let shared = LocalAccountManager() private

    init() { } private (set) var currentUser: User? var isLoggedIn: Bool { return currentUser != nil } func login(username: String, password: String) -> Bool { guard currentUser == nil else { return false } guard username == "yoichi" && password == "secure_one" else { return false } currentUser = User(username: "yoichi", fullname: "Yoichi Tagaya") return true } func logout() { currentUser = nil } } LocalAccountManager Singleton 4JOHMFUPO
  16. MainViewController final class MainViewController: UIViewController { @IBOutlet private weak var

    label: UILabel! @IBOutlet private weak var usernameField: UITextField! @IBOutlet private weak var passwordField: UITextField! @IBOutlet private weak var button: UIButton! }
  17. MainViewController final class MainViewController: UIViewController { @IBOutlet private weak var

    label: UILabel! @IBOutlet private weak var usernameField: UITextField! @IBOutlet private weak var passwordField: UITextField! @IBOutlet private weak var button: UIButton! @IBAction private func buttonTapped(_ sender: Any) { if LocalAccountManager.shared.isLoggedIn { logout() } else { login() } } private func login() { let username = usernameField.text ?? "" let password = passwordField.text ?? "" let status = LocalAccountManager.shared.login(username: username, password: password) if status, let user = LocalAccountManager.shared.currentUser { label.text = "Hello \(user.fullname)!" button.setTitle("Log out", for: .normal) } else { label.text = "Failed to log in." } } private func logout() { LocalAccountManager.shared.logout() label.text = "Please log in." button.setTitle("Log in", for: .normal) } } 6TJOHTJOHMFUPO 6TJOHTJOHMFUPO
  18. Refactoring to Inject Dependency final class MainViewController: UIViewController { //

    Set this property before you use this class. var accountManager: LocalAccountManager! // Omit } 1SPQFSUZJOKFDUJPO
  19. Refactoring to Inject Dependency final class MainViewController: UIViewController { //

    Set this property before you use this class. var accountManager: LocalAccountManager! // Omit @IBAction private func buttonTapped(_ sender: Any) { if accountManager.isLoggedIn { logout() } else { login() } } private func login() { let username = usernameField.text ?? "" let password = passwordField.text ?? "" let status = accountManager.login(username: username, password: password) if status, let user = accountManager.currentUser { label.text = "Hello \(user.fullname)!" button.setTitle("Log out", for: .normal) } else { label.text = "Failed to log in." } } private func logout() { accountManager.logout() label.text = "Please log in." button.setTitle("Log in", for: .normal) } } 1SPQFSUZJOKFDUJPO 3FQMBDFTJOHMFUPO XJUIUIFQSPQFSUZ
  20. Refactoring to Inject Dependency @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { // Setup window let window = UIWindow(frame: UIScreen.main.bounds) window.makeKeyAndVisible() self.window = window // Setup root view controller let storyboard = UIStoryboard(name: "Main", bundle: nil) let vc = storyboard.instantiateInitialViewController() as! MainViewController vc.accountManager = LocalAccountManager.shared window.rootViewController = vc return true } } *OKFDUBOJOTUBODF PG"DDPVOU.BOBHFS
  21. Improved by Refactoring • Removed dependency to LocalAccountManager singleton from

    the view controller. • Injected an LocalAccountManager instance to the view controller.
  22. Introduce a Protocol to Decouple protocol AccountManager { var currentUser:

    User? { get } var isLoggedIn: Bool { get } func login(username: String, password: String) -> Bool func logout() } extension AccountManager { var isLoggedIn: Bool { return currentUser != nil } } &YUSBDUFEJOUFSGBDF PG-PDBM"DDPVOU.BOBHFS "EEFEEFGBVMUJNQMFNFOUBUJPO CZQSPUPDPMFYUFOTJPO
  23. Conform to the Protocol final class LocalAccountManager: AccountManager { private

    (set) var currentUser: User? func login(username: String, password: String) -> Bool { guard currentUser == nil else { return false } guard username == "yoichi" && password == "secure_one" else { return false } currentUser = User(username: "yoichi", fullname: "Yoichi Tagaya") return true } func logout() { currentUser = nil } } $POGPSNFEUP UIFQSPUPDPM *NQMFNFOUBUJPOJTUIFTBNF
  24. Depend on the Protocol final class MainViewController: UIViewController { //

    Set this property before you use this class. var accountManager: AccountManager! // Omit } 3FQMBDFEXJUIUIFQSPUPDPM
  25. Inject Instance to View Controller @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate

    { var window: UIWindow? var accountManager: AccountManager = LocalAccountManager() func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { // Setup window let window = UIWindow(frame: UIScreen.main.bounds) window.makeKeyAndVisible() self.window = window // Setup root view controller let storyboard = UIStoryboard(name: "Main", bundle: nil) let vc = storyboard.instantiateInitialViewController() as! MainViewController vc.accountManager = accountManager window.rootViewController = vc return true } } *OKFDUUIFJOTUBODF $SFBUFBOJOTUBODF
  26. Decoupled Dependency • Loosely-coupled the view controller with AccountManager protocol.

    • AccountManager can be replaced with any concrete type conforming to the protocol.
  27. Introduce a View Model final class MainViewModel { private let

    accountManager: AccountManager private (set) var labelText = "Please log in." private (set) var buttonText = "Log in" init(accountManager: AccountManager) { self.accountManager = accountManager } } 5FYUTUPEJTQMBZJO7JFX *OJUJBMJ[FSJOKFDUJPO
  28. Introduce a View Model final class MainViewModel { // Omit

    func login(username: String, password: String) { guard !accountManager.isLoggedIn else { labelText = "Log out first." return } let status = accountManager.login(username: username, password: password) if status, let user = accountManager.currentUser { labelText = "Hello \(user.fullname)!" buttonText = "Log out" } else { labelText = "Failed to log in." } } func logout() { guard accountManager.isLoggedIn else { labelText = "Log in first." return } accountManager.logout() labelText = "Please log in." buttonText = "Log in" } } .PWFEMPHJOMPHPVUMPHJD GSPN7JFX$POUSPMMFS
  29. Use the View Model final class MainViewController: UIViewController { //

    Set this property before you use this class. var accountManager: AccountManager! private lazy var viewModel: MainViewModel = MainViewModel(accountManager: self.accountManager) // Omit @IBAction private func buttonTapped(_ sender: Any) { if accountManager.isLoggedIn { viewModel.logout() } else { let username = usernameField.text ?? "" let password = passwordField.text ?? "" viewModel.login(username: username, password: password) } updateTexts() } private func updateTexts() { label.text = viewModel.labelText button.setTitle(viewModel.buttonText, for: .normal) } } *OKFDUEFQFOEFODZ 6TFUIFWJFXNPEFM 6TFUIFWJFXNPEFM
  30. Moved the View Logic • View logic was moved from

    View Controller to View Model. • It’s easier to test View Model.
  31. Add Unit Tests // MARK: Successful Login final class MainViewModelTests:

    XCTestCase { // MARK: Mock private final class SuccessfulAccountManager: AccountManager { private (set) var currentUser: User? @discardableResult func login(username: String, password: String) -> Bool { currentUser = User(username: "test", fullname: "Test Test") return true } func logout() { currentUser = nil } } .PDLTVDDFFEJOH UPMPHJOBMXBZT
  32. Add Unit Tests // MARK: Successful Login final class MainViewModelTests:

    XCTestCase { // MARK: Mock private final class SuccessfulAccountManager: AccountManager { private (set) var currentUser: User? @discardableResult func login(username: String, password: String) -> Bool { currentUser = User(username: "test", fullname: "Test Test") return true } func logout() { currentUser = nil } } // MARK: Tests func testLogin_saysHelloToTheUserOnSuccess() { let accountManager = SuccessfulAccountManager() let viewModel = MainViewModel(accountManager: accountManager) viewModel.login(username: "", password: "") XCTAssertEqual(viewModel.labelText, "Hello Test Test!") } .PDLTVDDFFEJOH UPMPHJOBMXBZT *OKFDUUIFNPDL The message is checked in this example, but checking
 a state parameter or flag makes a unit test stable.
  33. Add Unit Tests func testLogin_showsErrorIfAlreadyLoggedIn() { let accountManager = SuccessfulAccountManager()

    accountManager.login(username: "", password: "") let viewModel = MainViewModel(accountManager: accountManager) viewModel.login(username: "", password: "") XCTAssertEqual(viewModel.labelText, "Log out first.") } func testLogout_asksToLoginAgain() { let accountManager = SuccessfulAccountManager() accountManager.login(username: "", password: "") let viewModel = MainViewModel(accountManager: accountManager) viewModel.logout() XCTAssertEqual(viewModel.labelText, "Please log in.") } func testLogout_promptsToLoginIfNotLoggedIn() { let accountManager = SuccessfulAccountManager() let viewModel = MainViewModel(accountManager: accountManager) viewModel.logout() XCTAssertEqual(viewModel.labelText, "Log in first.") } } The message is checked in this example, but checking
 a state parameter or flag makes a unit test stable.
  34. Add Unit Tests // MARK: - Failed Login extension MainViewModelTests

    { // MARK: Mock private final class FailingAccountManager: AccountManager { private (set) var currentUser: User? func login(username: String, password: String) -> Bool { return false } func logout() { } } // MARK: Tests func testLogin_showsErrorOnLoginFailure() { let accountManager = FailingAccountManager() let viewModel = MainViewModel(accountManager: accountManager) viewModel.login(username: "", password: "") XCTAssertEqual(viewModel.labelText, "Failed to log in.") } } *OKFDUUIFNPDL .PDLGBJMJOHUPMPHJOBMXBZT The message is checked in this example, but checking
 a state parameter or flag makes a unit test stable.
  35. Summary of the Example App 1. Tightly-coupled dependency to singleton

    LocalAccountManager 2. Refactoring by dependency injection 3. Made the program testable✅
  36. Problem of Dependency Injection Soon... " Dependency # let a

    = A(b: B(c: C())) $ Dependency Dependency Injection Code
  37. let a = A(b: B(c: C(f: F()), e: E(f: F())))

    Problem of Dependency Injection Much later... $ ' * & # ) " % ( Hard to manage Dependency Injection Code
  38. Dependency Injection Container $ ' * & # ) "

    % ( %*$POUBJOFS Dependency Graph Register the dependency graph let a = ... Resolve dependencies upon your request
  39. // Program entry point. let container = Container() container.register(AProtocol.self) {

    r in A(b: r.resolve(BProtocol.self)!) } Swinject Example $ ' & # " How to use a container #JOTUBODFJTNBOBHFE CZUIFDPOUBJOFS
  40. // Program entry point. let container = Container() container.register(AProtocol.self) {

    r in A(b: r.resolve(BProtocol.self)!) } container.register(BProtocol.self) { r in B(c: r.resolve(CProtocol.self)!, e: r.resolve(EProtocol.self)!) } Swinject Example $ ' & # " How to use a container #JOTUBODFJTNBOBHFE CZUIFDPOUBJOFS
  41. // Program entry point. let container = Container() container.register(AProtocol.self) {

    r in A(b: r.resolve(BProtocol.self)!) } container.register(BProtocol.self) { r in B(c: r.resolve(CProtocol.self)!, e: r.resolve(EProtocol.self)!) } container.register(CProtocol.self) { r in C(f: r.resolve(FProtocol.self)!) } container.register(EProtocol.self) { r in E(f: r.resolve(FProtocol.self)!) } container.register(FProtocol.self) { _ in F() } Swinject Example $ ' & # " How to use a container #JOTUBODFJTNBOBHFE CZUIFDPOUBJOFS
  42. // Program entry point. let container = Container() container.register(AProtocol.self) {

    r in A(b: r.resolve(BProtocol.self)!) } container.register(BProtocol.self) { r in B(c: r.resolve(CProtocol.self)!, e: r.resolve(EProtocol.self)!) } container.register(CProtocol.self) { r in C(f: r.resolve(FProtocol.self)!) } container.register(EProtocol.self) { r in E(f: r.resolve(FProtocol.self)!) } container.register(FProtocol.self) { _ in F() } // Anywhere later... let a = container.resolve(AProtocol.self)! Swinject Example $ ' & # " How to use a container #JOTUBODFJTNBOBHFE CZUIFDPOUBJOFS +VTUBTLUPHFU BO"JOTUBODF
  43. Dependency Injection Container • Pros - It's easy to use.

    - Definitions are unified. • Cons - Error at runtime for ! 1SPCMFNPGEZOBNJDEFQFOEFODZJOKFDUJPO
  44. Static dependency injection in Cake Pattern • Pros - Lack

    of definitions checked by compiler. - No error at runtime for dependency injection. • Cons - Less easy than DI container. %FQFOEFODZ*OKFDUJPOXJUIUIF$BLF1BUUFSOJO4XJGUCZ#PC$PUUSFMM
 IUUQBDRVJIJSFNFEFQFOEFODZJOKFDUJPOXJUIUIFDBLFQBUUFSOJOTXJGU (SFBUBSUJDMFUPMFBSO$BLF1BUUFSO
  45. Summary • Basic Concepts - Inversion of Control - Tight/loose

    coupling • Example Program - State management problem - Refactoring by Dependency Injection • Advanced Features - Dependency Injection Container (dynamic) - Cake Pattern (static)