Slide 1

Slide 1 text

Dependency Injection in Practice Yoichi Tagaya Appdevcon 2017, Amsterdam

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

Dependency Injection in Practice 1. Basic Concepts 2. Example Program Applying Dependency Injection 3. Advanced Features 5PEBZT5BML

Slide 5

Slide 5 text

Introduction • Reduce Technical Debt • Asset Catalogs • Dependency Injection • Live Playgrounds "Improving Existing Apps with Modern Best Practices"
 by Woody L. at WWDC 2016

Slide 6

Slide 6 text

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.

Slide 7

Slide 7 text

Inversion of Control class Person { let pet = Cat() } You create what you use

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

Coupling of Dependency Tight coupling Loose coupling

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

Example Program Applying Dependency Injection

Slide 13

Slide 13 text

Main Problems to Develop a Large App

Slide 14

Slide 14 text

Main Problems to Develop a Large App • Event handling • State management

Slide 15

Slide 15 text

Main Problems to Develop a Large App • Event handling - Functional Reactive Programming - ReactiveSwift - RxSwift

Slide 16

Slide 16 text

Main Problems to Develop a Large App • State management - Complicated problem - Caused by dependency often - Dependency injection is a solution (but not the only)

Slide 17

Slide 17 text

Sample App

Slide 18

Slide 18 text

Sample App -PHJO -PHJO'BJMVSF -PHPVU -PHJO

Slide 19

Slide 19 text

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 }

Slide 20

Slide 20 text

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.

Slide 21

Slide 21 text

Let's Dive into the Code

Slide 22

Slide 22 text

Let’s Dive into the Code 1. Implementation with Account Manager singleton. 2. Refactoring with Dependency Injection. 3. Unit testing.

Slide 23

Slide 23 text

User Model struct User { let username: String let fullname: String }

Slide 24

Slide 24 text

final class LocalAccountManager { static let shared = LocalAccountManager() private init() { } } LocalAccountManager Singleton 4JOHMFUPO

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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! }

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

Implemented

Slide 30

Slide 30 text

But Singleton is Used LocalAccountManager.shared.isLoggedIn LocalAccountManager.shared.currentUser LocalAccountManager.shared.login(...) LocalAccountManager.shared.logout()

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

Improved by Refactoring • Removed dependency to LocalAccountManager singleton from the view controller. • Injected an LocalAccountManager instance to the view controller.

Slide 35

Slide 35 text

But Still Tightly-Coupled with LocalAccountManager final class MainViewController: UIViewController { var accountManager: LocalAccountManager! }

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

Depend on the Protocol final class MainViewController: UIViewController { // Set this property before you use this class. var accountManager: AccountManager! // Omit } 3FQMBDFEXJUIUIFQSPUPDPM

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

Decoupled Dependency • Loosely-coupled the view controller with AccountManager protocol. • AccountManager can be replaced with any concrete type conforming to the protocol.

Slide 41

Slide 41 text

Let's Add Unit Tests

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

Moved the View Logic • View logic was moved from View Controller to View Model. • It’s easier to test View Model.

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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.

Slide 48

Slide 48 text

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.

Slide 49

Slide 49 text

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.

Slide 50

Slide 50 text

Testable by Refactoring✅

Slide 51

Slide 51 text

Summary of the Example App 1. Tightly-coupled dependency to singleton LocalAccountManager 2. Refactoring by dependency injection 3. Made the program testable✅

Slide 52

Slide 52 text

Advanced Features

Slide 53

Slide 53 text

Dependency Injection Container (DI Container)

Slide 54

Slide 54 text

Problem of Dependency Injection At the beginning... Dependency Injection Code let a = A(b: B()) " Dependency #

Slide 55

Slide 55 text

Problem of Dependency Injection Soon... " Dependency # let a = A(b: B(c: C())) $ Dependency Dependency Injection Code

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

Dependency Injection Container $ ' * & # ) " % ( %*$POUBJOFS Dependency Graph Register the dependency graph let a = ... Resolve dependencies upon your request

Slide 58

Slide 58 text

// Program entry point. let container = Container() Swinject Example $ ' & # " How to use a container

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

Dependency Injection Container • Pros - It's easy to use. - Definitions are unified. • Cons - Error at runtime for ! 1SPCMFNPGEZOBNJDEFQFOEFODZJOKFDUJPO

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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)