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

Managed side effects

Managed side effects

Talk by Vitalii Malakhovskiy.

⁃ расскажу как мы пишем сложную логику, чтобы потом в этом еще и разобраться
⁃ снова про data-driven
⁃ как потом покрыть эту логику тестами
⁃ когда так делать не надо

This talk was made for CocoaHeads Kyiv #15 which took place Jul 28, 2019. (https://cocoaheads.org.ua/cocoaheadskyiv/15)

Video: https://youtu.be/bhEn-VOH0q0

CocoaHeads Ukraine

July 28, 2019
Tweet

More Decks by CocoaHeads Ukraine

Other Decks in Programming

Transcript

  1. managed
    side effects
    1 — Vitalii Malakhovskyi, BetterMe

    View Slide

  2. pain
    2 — Vitalii Malakhovskyi, BetterMe

    View Slide

  3. sometimes tests
    makes everything
    even worth
    3 — Vitalii Malakhovskyi, BetterMe

    View Slide

  4. how do we solve
    these problems
    4 — Vitalii Malakhovskyi, BetterMe

    View Slide

  5. does it actually
    gives anything
    5 — Vitalii Malakhovskyi, BetterMe

    View Slide

  6. managed
    side effects
    6 — Vitalii Malakhovskyi, BetterMe

    View Slide

  7. malakhovskyi
    vitalii
    7 — Vitalii Malakhovskyi, BetterMe

    View Slide

  8. 8 — Vitalii Malakhovskyi, BetterMe

    View Slide

  9. constantly updating,
    long term products
    9 — Vitalii Malakhovskyi, BetterMe

    View Slide

  10. maintainability
    10 — Vitalii Malakhovskyi, BetterMe

    View Slide

  11. ?
    11 — Vitalii Malakhovskyi, BetterMe

    View Slide

  12. cheap code changes
    cheap tests changes
    12 — Vitalii Malakhovskyi, BetterMe

    View Slide

  13. View Slide

  14. UI
    data driven approach
    14 — Vitalii Malakhovskyi, BetterMe

    View Slide

  15. chapter 1
    problem
    15 — Vitalii Malakhovskyi, BetterMe

    View Slide

  16. business logic
    16 — Vitalii Malakhovskyi, BetterMe

    View Slide

  17. data processing rules
    according to business
    needs
    — wiki
    17 — Vitalii Malakhovskyi, BetterMe

    View Slide

  18. service
    manager
    facadeservice
    interactor
    18 — Vitalii Malakhovskyi, BetterMe

    View Slide

  19. images service
    1) fetch if already downloaded
    2) download
    3) store and return
    19 — Vitalii Malakhovskyi, BetterMe

    View Slide

  20. measure component
    complexity by it's tests
    !
    20 — Vitalii Malakhovskyi, BetterMe

    View Slide

  21. looks simple
    - fetch success -> data
    - fetch fail -> download fail -> error
    - fetch fail -> download success -> store fail -> error
    - fetch fail -> download success -> store success -> data
    21 — Vitalii Malakhovskyi, BetterMe

    View Slide

  22. unit test
    integration test
    22 — Vitalii Malakhovskyi, BetterMe

    View Slide

  23. dependencies
    23 — Vitalii Malakhovskyi, BetterMe

    View Slide

  24. mocks
    24 — Vitalii Malakhovskyi, BetterMe

    View Slide

  25. verify that
    we properly calling
    dependencies
    25 — Vitalii Malakhovskyi, BetterMe

    View Slide

  26. async behavior
    26 — Vitalii Malakhovskyi, BetterMe

    View Slide

  27. keep up with
    youtube trends,
    use rx
    27 — Vitalii Malakhovskyi, BetterMe

    View Slide

  28. and then use
    separate library
    to mock it
    28 — Vitalii Malakhovskyi, BetterMe

    View Slide

  29. before
    - fetch success -> data
    - fetch fail -> download fail -> error
    - fetch fail -> download success -> store fail -> error
    - fetch fail -> download success -> store success -> data
    29 — Vitalii Malakhovskyi, BetterMe

    View Slide

  30. after
    - verify m(fetch)
    - m(fetch) success -> data
    - m(fetch) fail -> verify m(download)
    - m(fetch) fail -> m(download) fail -> error
    - m(fetch) fail -> m(download) success -> verify m(store)
    - m(fetch) fail -> m(download) success -> m(store) fail -> error
    - m(fetch) fail -> m(download) success -> m(store) success -> data
    30 — Vitalii Malakhovskyi, BetterMe

    View Slide

  31. bdd style tests
    describing behavior
    31 — Vitalii Malakhovskyi, BetterMe

    View Slide

  32. describe("ImagesService") {
    context("when asking for image") {
    beforeEach { /* setup sut */ }
    it("should fetch data") { ... }
    context("but image is not cached") {
    beforeEach { /* stub fetching */ }
    it("should download image") { ... }
    context("and image download succeed") {
    beforeEach { /* stub downloading */ }
    it("should store data") { ... }
    }
    }
    }
    }
    32 — Vitalii Malakhovskyi, BetterMe

    View Slide

  33. describe("ImagesService") {
    context("when asking for image") {
    beforeEach { /* setup sut */ }
    it("should fetch data") { ... }
    context("but image is not cached") {
    beforeEach { /* stub fetching */ }
    it("should download image") { ... }
    context("and image download succeed") {
    beforeEach { /* stub downloading */ }
    it("should store data") { ... }
    }
    }
    }
    }
    33 — Vitalii Malakhovskyi, BetterMe

    View Slide

  34. everything looks fine
    34 — Vitalii Malakhovskyi, BetterMe

    View Slide

  35. used approaches
    - protocols
    - dependency injection
    - covered with integration tests (bdd)
    35 — Vitalii Malakhovskyi, BetterMe

    View Slide

  36. chapter 2
    nabrasivaem
    na ventylator
    36 — Vitalii Malakhovskyi, BetterMe

    View Slide

  37. 37 — Vitalii Malakhovskyi, BetterMe

    View Slide

  38. maintainability
    38 — Vitalii Malakhovskyi, BetterMe

    View Slide

  39. 1
    39 — Vitalii Malakhovskyi, BetterMe

    View Slide

  40. mocks mocks mocks mocks mocks mocks
    mocks mocks mocks mocks mocks mocks
    mocks mocks mocks mocks mocks mocks
    mocks mocks mocks mocks mocks mocks
    mocks mocks mocks mocks mocks mocks
    mocks mocks mocks mocks mocks mocks
    40 — Vitalii Malakhovskyi, BetterMe

    View Slide

  41. !
    write each mock manually
    41 — Vitalii Malakhovskyi, BetterMe

    View Slide

  42. !
    use library to unify writing mocks
    42 — Vitalii Malakhovskyi, BetterMe

    View Slide

  43. !
    use codegen to generate mocks
    43 — Vitalii Malakhovskyi, BetterMe

    View Slide

  44. 44 — Vitalii Malakhovskyi, BetterMe

    View Slide

  45. 2
    45 — Vitalii Malakhovskyi, BetterMe

    View Slide

  46. complex tests
    46 — Vitalii Malakhovskyi, BetterMe

    View Slide

  47. describe("ImagesService") {
    context("when asking for image") {
    beforeEach { /* setup sut */ }
    it("should fetch data") { ... }
    context("but image is not cached") {
    beforeEach { /* stub fetching */ }
    it("should download image") { ... }
    context("and image download succeed") {
    beforeEach { /* stub downloading */ }
    it("should store data") { ... }
    }
    }
    }
    }
    47 — Vitalii Malakhovskyi, BetterMe

    View Slide

  48. quick - hides complexity
    48 — Vitalii Malakhovskyi, BetterMe

    View Slide

  49. let's imagine how simple
    test will look like without
    quick
    49 — Vitalii Malakhovskyi, BetterMe

    View Slide

  50. fetch failed -> download succeed -> store failed
    //given
    /* stub fetch */
    /* stub download */
    /* stub store */
    /* setup sut */
    //when
    ...
    //then
    ...
    50 — Vitalii Malakhovskyi, BetterMe

    View Slide

  51. func testPaymentQueue_when_oneTransactionForEachState_onePayment_noRestorePurchases_oneCompleteTransactions_then_correctCallbacksCalled() {
    // setup
    let spy = PaymentQueueSpy()
    let paymentQueueController = PaymentQueueController(paymentQueue: spy)
    let purchasedProductIdentifier = "com.SwiftyStoreKit.product1"
    let failedProductIdentifier = "com.SwiftyStoreKit.product2"
    let restoredProductIdentifier = "com.SwiftyStoreKit.product3"
    let deferredProductIdentifier = "com.SwiftyStoreKit.product4"
    let purchasingProductIdentifier = "com.SwiftyStoreKit.product5"
    let transactions = [
    makeTestPaymentTransaction(productIdentifier: purchasedProductIdentifier, transactionState: .purchased),
    makeTestPaymentTransaction(productIdentifier: failedProductIdentifier, transactionState: .failed),
    makeTestPaymentTransaction(productIdentifier: restoredProductIdentifier, transactionState: .restored),
    makeTestPaymentTransaction(productIdentifier: deferredProductIdentifier, transactionState: .deferred),
    makeTestPaymentTransaction(productIdentifier: purchasingProductIdentifier, transactionState: .purchasing)
    ]
    var paymentCallbackCalled = false
    let testPayment = makeTestPayment(productIdentifier: purchasedProductIdentifier) { result in
    paymentCallbackCalled = true
    if case .purchased(let payment) = result {
    XCTAssertEqual(payment.productId, purchasedProductIdentifier)
    } else {
    XCTFail("expected purchased callback with product id")
    }
    }
    var completeTransactionsCallbackCalled = false
    let completeTransactions = CompleteTransactions(atomically: true) { payments in
    completeTransactionsCallbackCalled = true
    XCTAssertEqual(payments.count, 3)
    XCTAssertEqual(payments[0].productId, failedProductIdentifier)
    XCTAssertEqual(payments[1].productId, restoredProductIdentifier)
    XCTAssertEqual(payments[2].productId, deferredProductIdentifier)
    }
    // run
    paymentQueueController.completeTransactions(completeTransactions)
    paymentQueueController.startPayment(testPayment)
    paymentQueueController.paymentQueue(SKPaymentQueue(), updatedTransactions: transactions)
    paymentQueueController.paymentQueueRestoreCompletedTransactionsFinished(SKPaymentQueue())
    // verify
    XCTAssertTrue(paymentCallbackCalled)
    XCTAssertTrue(completeTransactionsCallbackCalled)
    }
    51 — Vitalii Malakhovskyi, BetterMe

    View Slide

  52. tests - heath indicator for
    your app design
    52 — Vitalii Malakhovskyi, BetterMe

    View Slide

  53. app design is good if it's
    covered with tests
    53 — Vitalii Malakhovskyi, BetterMe

    View Slide

  54. app design is good if it's
    easy to cover with tests
    54 — Vitalii Malakhovskyi, BetterMe

    View Slide

  55. 3
    55 — Vitalii Malakhovskyi, BetterMe

    View Slide

  56. let's change something in downloading
    - verify m(fetch)
    - m(fetch) success -> data
    - m(fetch) fail -> verify m(download)
    - m(fetch) fail -> m(download) fail -> error
    - m(fetch) fail -> m(download) success -> verify m(store)
    - m(fetch) fail -> m(download) success -> m(store) fail -> error
    - m(fetch) fail -> m(download) success -> m(store) success -> data
    56 — Vitalii Malakhovskyi, BetterMe

    View Slide

  57. !!!!!!
    57 — Vitalii Malakhovskyi, BetterMe

    View Slide

  58. !!"!!!
    58 — Vitalii Malakhovskyi, BetterMe

    View Slide

  59. !!"###
    59 — Vitalii Malakhovskyi, BetterMe

    View Slide

  60. change some - brake all
    1)
    2)
    3)
    4)
    5)
    6)
    7)
    60 — Vitalii Malakhovskyi, BetterMe

    View Slide

  61. hard to maintain, hard to
    add new features, hard
    cover it with tests
    61 — Vitalii Malakhovskyi, BetterMe

    View Slide

  62. expensive
    62 — Vitalii Malakhovskyi, BetterMe

    View Slide

  63. chapter 3
    solution
    63 — Vitalii Malakhovskyi, BetterMe

    View Slide

  64. what is the
    root of the
    problem?
    64 — Vitalii Malakhovskyi, BetterMe

    View Slide

  65. do {
    let fetched = try read(url)
    completion(.success(fetched))
    }
    catch {
    cancel = makeRequest(url).perform { [weak self] result in
    switch result {
    case .success(let value):
    ...
    case .failure(let error):
    ...
    }
    }
    }
    65 — Vitalii Malakhovskyi, BetterMe

    View Slide

  66. do {
    let fetched = try read(url) //side effect
    completion(.success(fetched)) //logic
    }
    catch { // logic, like if else //side effect
    cancel = makeRequest(url).perform { [weak self] result in
    switch result {
    case .success(let value): //logic
    ...
    case .failure(let error): //logic
    ...
    }
    }
    }
    66 — Vitalii Malakhovskyi, BetterMe

    View Slide

  67. 67 — Vitalii Malakhovskyi, BetterMe

    View Slide

  68. ⬥ is responsible for the
    different code paths
    68 — Vitalii Malakhovskyi, BetterMe

    View Slide

  69. the more ⬥ we have, the
    more code paths we need
    to test
    69 — Vitalii Malakhovskyi, BetterMe

    View Slide

  70. ̄ makes our code
    difficult to test, as they
    need mocks
    70 — Vitalii Malakhovskyi, BetterMe

    View Slide

  71. separate logic from
    effects > manage logic &
    manage side effects
    71 — Vitalii Malakhovskyi, BetterMe

    View Slide

  72. functional core,
    imperative shell
    In fact, it dates back to 2012, when Gary
    Bernhardt’s talked about it in Boundaries.
    72 — Vitalii Malakhovskyi, BetterMe

    View Slide

  73. 73 — Vitalii Malakhovskyi, BetterMe

    View Slide

  74. ELM
    74 — Vitalii Malakhovskyi, BetterMe

    View Slide

  75. logic
    75 — Vitalii Malakhovskyi, BetterMe

    View Slide

  76. in previous chapter
    /* setup sut */
    /* stub fetch */
    /* stub download */
    /* stub store */
    preparation of concrete state
    76 — Vitalii Malakhovskyi, BetterMe

    View Slide

  77. state
    77 — Vitalii Malakhovskyi, BetterMe

    View Slide

  78. let's imagine our service as state machine
    enum State {
    case initial
    case checking(URL)
    case downloading(URL)
    case finished(URL, Data)
    case failed
    }
    78 — Vitalii Malakhovskyi, BetterMe

    View Slide


  79. state shouldn't have impossible state
    enum State {
    case initial
    case checking(URL)
    case downloading(URL)
    case finished(URL, Data)
    case failed(Error)
    }
    79 — Vitalii Malakhovskyi, BetterMe

    View Slide

  80. hard setup becomes trivial
    /* setup sut */
    /* stub fetching */
    /* stub downloading */
    /* stub storing */
    vs
    let state = State.downloading(url)
    80 — Vitalii Malakhovskyi, BetterMe

    View Slide

  81. state mutates with event
    81 — Vitalii Malakhovskyi, BetterMe

    View Slide

  82. side effects does
    something, events
    describes what happens
    82 — Vitalii Malakhovskyi, BetterMe

    View Slide

  83. side effects - all about implementation
    details
    do {
    let fetched = try read(url)
    completion(.success(fetched))
    }
    catch {
    cancel = makeRequest(url).perform { [weak self] result in
    switch result {
    case .success(let value): ...
    case .failure(let error): ...
    }
    }
    83 — Vitalii Malakhovskyi, BetterMe

    View Slide

  84. side effects
    - dependency injection
    - frameworks
    - async behavior
    - protocols
    84 — Vitalii Malakhovskyi, BetterMe

    View Slide

  85. dependency injection - weak incapsulation
    instument
    3-rd party framework - unwanted details
    async behavior - harder to test/
    understand
    85 — Vitalii Malakhovskyi, BetterMe

    View Slide

  86. event is data, data is boundaries
    - does not depends on frameworks
    - has no execution
    - data cannot be async
    - better interface
    86 — Vitalii Malakhovskyi, BetterMe

    View Slide

  87. let's focus on what happens, not how
    enum Event {
    case fetch(URL)
    case fetchSucceed(Data)
    case fetchFailed(Error)
    case download(URL)
    case downloadSucceed(Data)
    case downloadFailed(Error)
    ...
    }
    87 — Vitalii Malakhovskyi, BetterMe

    View Slide

  88. (state, event) -> state
    88 — Vitalii Malakhovskyi, BetterMe

    View Slide

  89. function call or result handling becomes a
    data
    func reduce(state: State, event: Event) -> State {
    switch (state, event) {
    case (.downloading(let url), .downloadSucceed(let data)):
    return .finished(url, data)
    case (.downloading, .downloadFailed(let error)):
    return .failed(error)
    ...
    89 — Vitalii Malakhovskyi, BetterMe

    View Slide

  90. real test
    let url = URL.random()
    let data = Data.random()
    let state = reduce(.downloading(url), with: .downloadSucceed(data))
    XCTAssertEqual(state, .finished(url, data))
    90 — Vitalii Malakhovskyi, BetterMe

    View Slide

  91. simple test
    - does not brake if implementation changes
    - breaks if logic changes in concrete place
    91 — Vitalii Malakhovskyi, BetterMe

    View Slide

  92. !!"!!!
    92 — Vitalii Malakhovskyi, BetterMe

    View Slide

  93. !!"!!!
    93 — Vitalii Malakhovskyi, BetterMe

    View Slide

  94. logic = state
    and events
    94 — Vitalii Malakhovskyi, BetterMe

    View Slide

  95. what about
    effects?
    95 — Vitalii Malakhovskyi, BetterMe

    View Slide

  96. somebody needs to
    interpret state in terms
    of side effects
    96 — Vitalii Malakhovskyi, BetterMe

    View Slide

  97. imperative shell
    - chaos
    - async
    - frameworks
    - dependency injection
    implementation details
    97 — Vitalii Malakhovskyi, BetterMe

    View Slide

  98. observer
    listens for state updates,
    posts events
    98 — Vitalii Malakhovskyi, BetterMe

    View Slide

  99. state effect
    idle nothing
    downloading download
    storing store
    failed nothing
    succeed nothing
    99 — Vitalii Malakhovskyi, BetterMe

    View Slide

  100. state -> observer
    or
    state -> presenter -> controller
    100 — Vitalii Malakhovskyi, BetterMe

    View Slide

  101. class EffectsProducer {
    func handle(state: State, produce: @escaping (Event?) -> ()) {
    switch state {
    case .downloading(let url):
    makeRequest(url).perform { [weak self] data in
    if let data = try? data.get() {
    produce(.downloadSucceed(data))
    } else {
    produce(.downloadFailed)
    }
    }
    ...
    101 — Vitalii Malakhovskyi, BetterMe

    View Slide

  102. to test observer we might need
    - mock
    - stub
    - spy
    - fake
    102 — Vitalii Malakhovskyi, BetterMe

    View Slide

  103. well isolated tests
    logic effect
    1)

    1)
    2)

    2)
    3)

    3)
    4)

    4)
    5)

    5)
    6)

    6)
    7)

    7)
    103 — Vitalii Malakhovskyi, BetterMe

    View Slide

  104. not independent but
    has great resuability
    104 — Vitalii Malakhovskyi, BetterMe

    View Slide

  105. whole picture
    -> state -> observer -> event ->
    105 — Vitalii Malakhovskyi, BetterMe

    View Slide

  106. unidirectional
    architecture
    106 — Vitalii Malakhovskyi, BetterMe

    View Slide

  107. missing part - store
    - owns state
    - mutates state (reduce function)
    - notifies observers about update
    107 — Vitalii Malakhovskyi, BetterMe

    View Slide

  108. chapter 4
    conclusion
    108 — Vitalii Malakhovskyi, BetterMe

    View Slide

  109. what is hard
    - integrate into non-data driven project
    - multiple stores synchronization
    - side effects sync with logic
    109 — Vitalii Malakhovskyi, BetterMe

    View Slide

  110. don't do that if
    - in production project if you just heard about it
    - you have multiple stores with relations
    - your logic is simple
    110 — Vitalii Malakhovskyi, BetterMe

    View Slide

  111. balance
    111 — Vitalii Malakhovskyi, BetterMe

    View Slide

  112. to recap
    - found maintainability problem
    - manage side effects, manage logic
    - how to keep tests maintainable
    - how tests tell you when your code stinks
    112 — Vitalii Malakhovskyi, BetterMe

    View Slide

  113. logic &
    effects
    write
    code
    write
    tests
    maintain
    code
    maintain
    tests
    mixed ✅ +/- ❌ ❌
    separate +/- ✅ ✅ ✅
    113 — Vitalii Malakhovskyi, BetterMe

    View Slide

  114. 114 — Vitalii Malakhovskyi, BetterMe

    View Slide

  115. purpleshirted
    Vitalii Malakhovskyi
    VitaliyMal
    vmalakhovskiy
    115 — Vitalii Malakhovskyi, BetterMe

    View Slide

  116. links
    116 — Vitalii Malakhovskyi, BetterMe

    View Slide

  117. examples
    117 — Vitalii Malakhovskyi, BetterMe

    View Slide

  118. q & a
    118 — Vitalii Malakhovskyi, BetterMe

    View Slide