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 full-size slide

  2. pain
    2 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  6. managed
    side effects
    6 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

  7. malakhovskyi
    vitalii
    7 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

  8. 8 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

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

    View full-size slide

  10. maintainability
    10 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

  11. ?
    11 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  14. chapter 1
    problem
    15 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

  15. business logic
    16 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  20. 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 full-size slide

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

    View full-size slide

  22. dependencies
    23 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

  23. mocks
    24 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

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

    View full-size slide

  25. async behavior
    26 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  28. 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 full-size slide

  29. 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 full-size slide

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

    View full-size slide

  31. 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 full-size 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") { ... }
    }
    }
    }
    }
    33 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

  33. everything looks fine
    34 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  36. 37 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

  37. maintainability
    38 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

  38. 1
    39 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

  39. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  43. 44 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

  44. 2
    45 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

  45. complex tests
    46 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

  46. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  50. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  54. 3
    55 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

  55. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  61. expensive
    62 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

  62. chapter 3
    solution
    63 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

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

    View full-size slide

  64. 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 full-size slide

  65. 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 full-size slide

  66. 67 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  71. 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 full-size slide

  72. 73 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

  73. ELM
    74 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

  74. logic
    75 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

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

    View full-size slide

  76. state
    77 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

  77. 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 full-size slide


  78. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  82. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  85. 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 full-size slide

  86. 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 full-size slide

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

    View full-size slide

  88. 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 full-size slide

  89. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  100. 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 full-size slide

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

    View full-size slide

  102. well isolated tests
    logic effect
    1)

    1)
    2)

    2)
    3)

    3)
    4)

    4)
    5)

    5)
    6)

    6)
    7)

    7)
    103 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  105. unidirectional
    architecture
    106 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

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

    View full-size slide

  107. chapter 4
    conclusion
    108 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

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

    View full-size slide

  109. 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 full-size slide

  110. balance
    111 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

  111. 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 full-size slide

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

    View full-size slide

  113. 114 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

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

    View full-size slide

  115. links
    116 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

  116. examples
    117 — Vitalii Malakhovskyi, BetterMe

    View full-size slide

  117. q & a
    118 — Vitalii Malakhovskyi, BetterMe

    View full-size slide