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

Building Robust Software, Episode 2

Building Robust Software, Episode 2

Presented at https://www.meetkt.org on 21 October 2021

The first episode of this series touched upon leveraging the Kotlin type system, adopting ideas from the functional programming world, leveraging the robustness principle, and failing fast to build robust software.

In this session, we'll focus on the thing that makes software move the word — side-effects. Side-effects are what make tangible changes to the real world with which the software is interacting. These changes could be,

Making network calls
Saving your wishlist on your favorite shopping website
Rendering graphics on the screen so that your favorite video game character can jump over nefarious enemies, collect gold coins, and save the prince or princess from the supervillain.

And naturally, when your program has side-effects that interact with the real world, things can go wrong in so many ways because the world is not a perfect place to live in. What happens when your database is not available, or your disk is full? Your program will throw exceptions because it failed to accomplish the mission you initially set it to.

Exceptions can be painful to deal with. So, we will look into what they are, the implications of using them to decide the control flow of your program, and what patterns you can use to deal with them elegantly.

Finally, we'll finish by looking into how you can design systems and model boundaries between components/classes/functions, therefore, building systems with good testability. This topic will also set the tone for our next session on writing tests!

Ragunath Jawahar

October 21, 2021
Tweet

More Decks by Ragunath Jawahar

Other Decks in Programming

Transcript

  1. Ragunath Jawahar @ragunathjawahar
    https://ragunath.xyz
    Building Robust Software
    Episode 2: Exceptions, side-e
    ff
    ects and boundaries

    View full-size slide

  2. Why?
    • People

    • Code

    • Process

    • Tooling

    View full-size slide

  3. Why?
    • People

    • Code

    • Process

    • Tooling

    View full-size slide

  4. Code
    • Less bureaucracy

    • Simple ideas

    • Starting point to hone your craft

    View full-size slide

  5. Recap
    • Robustness principle + tweaked version

    • Algebraic data types with Kotlin’s sealed classes

    • Total & partial functions

    • Using types,

    • To prevent defensive programming overhead

    • To avoid primitive obsession

    View full-size slide

  6. Episode 1
    https://bit.ly/30zwgTy

    View full-size slide

  7. Pendulum swings

    View full-size slide

  8. //
    Primitive obsession


    val password: String = "super secret stuff!"


    PasswordValidator.validate(password)


    //
    Encapsulated type


    val password: Password = Password("super secret stuff!")


    password.validate(password)

    View full-size slide

  9. //
    Primitive obsession


    val password: String = "super secret stuff!"


    PasswordValidator.validate(password)


    //
    Encapsulated type


    val password: Password = Password("super secret stuff!")


    password.validate(password)

    View full-size slide

  10. //
    Primitive obsession


    val password: String = "super secret stuff!"


    PasswordValidator.validate(password)


    //
    Encapsulated type


    val password: Password = Password("super secret stuff!")


    password.validate(password)

    View full-size slide

  11. //
    Primitive obsession


    val password: String = "super secret stuff!"


    PasswordValidator.validate(password)


    //
    Encapsulated type


    val password: Password = Password("super secret stuff!")


    password.validate(password)

    View full-size slide

  12. Pendulum swings example
    Primitive obsession
    • Built-in operators (+, -, !,
    & &
    ,
    ||
    , [], etc.,)

    • value or inline classes

    • ORM and serialisation/deserialisation libraries

    • Don’t have enough clarity about the problem domain

    View full-size slide

  13. https://docs.oracle.com/javase/tutorial/essential/exceptions/de
    fi
    nition.html
    The term exception is shorthand for the
    phrase “exceptional event.”

    View full-size slide

  14. Treat exceptions as first class citizens.

    View full-size slide

  15. Exceptions
    Anti-patterns

    View full-size slide

  16. try {


    / /
    make a network call


    } catch (e: IOException) {


    / /
    Gulp
    !!
    🥤


    } catch (e: HttpRetryException) {


    / /
    Delicious 😋


    }
    1. Swallowing exceptions

    View full-size slide

  17. try {


    / /
    make a network call


    } catch (e: Exception) {


    showSomethingWentWrong()


    }
    2. Catch-all

    View full-size slide

  18. private fun isNumber(candidate: String): Boolean {


    return try {


    Integer.parseInt(candidate)


    true


    } catch (e: NumberFormatException) {


    false


    }


    }
    3. Using exceptions for control flow

    View full-size slide




  19. Result "xyz.ragunath.benchmark.ReturnValues.returnValueTest":


    224747599.794 ±(99.9%) 1251327.222 ops/s [Average]


    (min, avg, max) = (220755995.007, 224747599.794, 227672182.229), stdev = 1670486.041


    CI (99.9%): [223496272.572, 225998927.016] (assumes normal distribution)





    Result "xyz.ragunath.benchmark.StackTraces.stackTraceTest":


    5830.292 ±(99.9%) 31.441 ops/s [Average]


    (min, avg, max) = (5676.655, 5830.292, 5889.489), stdev = 41.973


    CI (99.9%): [5798.851, 5861.733] (assumes normal distribution)



    View full-size slide




  20. Result "xyz.ragunath.benchmark.ReturnValues.returnValueTest":


    224747599.794 ±(99.9%) 1251327.222 ops/s [Average]


    (min, avg, max) = (220755995.007, 224747599.794, 227672182.229), stdev = 1670486.041


    CI (99.9%): [223496272.572, 225998927.016] (assumes normal distribution)





    Result "xyz.ragunath.benchmark.StackTraces.stackTraceTest":


    5830.292 ±(99.9%) 31.441 ops/s [Average]


    (min, avg, max) = (5676.655, 5830.292, 5889.489), stdev = 41.973


    CI (99.9%): [5798.851, 5861.733] (assumes normal distribution)



    View full-size slide




  21. Result "xyz.ragunath.benchmark.ReturnValues.returnValueTest":


    224747599.794 ±(99.9%) 1251327.222 ops/s [Average]


    (min, avg, max) = (220755995.007, 224747599.794, 227672182.229), stdev = 1670486.041


    CI (99.9%): [223496272.572, 225998927.016] (assumes normal distribution)





    Result "xyz.ragunath.benchmark.StackTraces.stackTraceTest":


    5830.292 ±(99.9%) 31.441 ops/s [Average]


    (min, avg, max) = (5676.655, 5830.292, 5889.489), stdev = 41.973


    CI (99.9%): [5798.851, 5861.733] (assumes normal distribution)



    View full-size slide




  22. Result "xyz.ragunath.benchmark.ReturnValues.returnValueTest":


    224747599.794 ±(99.9%) 1251327.222 ops/s [Average]


    (min, avg, max) = (220755995.007, 224747599.794, 227672182.229), stdev = 1670486.041


    CI (99.9%): [223496272.572, 225998927.016] (assumes normal distribution)





    Result "xyz.ragunath.benchmark.StackTraces.stackTraceTest":


    5830.292 ±(99.9%) 31.441 ops/s [Average]


    (min, avg, max) = (5676.655, 5830.292, 5889.489), stdev = 41.973


    CI (99.9%): [5798.851, 5861.733] (assumes normal distribution)



    View full-size slide




  23. Result "xyz.ragunath.benchmark.ReturnValues.returnValueTest":


    224747599.794 ±(99.9%) 1251327.222 ops/s [Average]


    (min, avg, max) = (220755995.007, 224747599.794, 227672182.229), stdev = 1670486.041


    CI (99.9%): [223496272.572, 225998927.016] (assumes normal distribution)





    Result "xyz.ragunath.benchmark.StackTraces.stackTraceTest":


    5830.292 ±(99.9%) 31.441 ops/s [Average]


    (min, avg, max) = (5676.655, 5830.292, 5889.489), stdev = 41.973


    CI (99.9%): [5798.851, 5861.733] (assumes normal distribution)



    View full-size slide




  24. Result "xyz.ragunath.benchmark.ReturnValues.returnValueTest":


    224747599.794 ±(99.9%) 1251327.222 ops/s [Average]


    (min, avg, max) = (220755995.007, 224747599.794, 227672182.229), stdev = 1670486.041


    CI (99.9%): [223496272.572, 225998927.016] (assumes normal distribution)





    Result "xyz.ragunath.benchmark.StackTraces.stackTraceTest":


    5830.292 ±(99.9%) 31.441 ops/s [Average]


    (min, avg, max) = (5676.655, 5830.292, 5889.489), stdev = 41.973


    CI (99.9%): [5798.851, 5861.733] (assumes normal distribution)



    View full-size slide

  25. for (candidate in potentialNumbers) {


    if (isNumber(candidate)) {


    println("It's a number!")


    } else {


    println("NaN")


    }


    }
    3. Using exceptions for control flow (contd…)

    View full-size slide

  26. assertThat(false).isTrue()
    Tests

    View full-size slide

  27. Exception in thread "main" expected to be true


    at CanaryTestKt.main(CanaryTest.kt:15)


    at CanaryTestKt.main(CanaryTest.kt)
    Tests

    View full-size slide

  28. if (balance < requestedAmount) {


    throw Exception("Meh… 😒")


    }
    4. Throwing Exception

    View full-size slide

  29. Exception handling in Kotlin
    • No checked exceptions

    • try is an expression

    View full-size slide

  30. var guardDogResult: Result


    try {


    guardDogResult = getGuardDog()


    } catch (e: NoGuardDogException) {


    guardDogResult = Result.failure(e)


    }
    try expression

    View full-size slide

  31. val guardDogResult: Result = try {


    getGuardDog()


    } catch (e: NoGuardDogException) {


    Result.failure(e)


    }
    try expression

    View full-size slide

  32. Exception handling patterns

    View full-size slide

  33. 1. Tester-Doer pattern
    • Tester - a member that is used to test a condition

    • Doer - a member that performs a potentially throwing operation

    View full-size slide

  34. java.util.EmptyStackException


    at java.base/java.util.Stack.peek(Stack.java:102)


    at java.base/java.util.Stack.pop(Stack.java:84)


    at MainKt.main(Main.kt:8)


    at MainKt.main(Main.kt)

    View full-size slide

  35. if (stack.isNotEmpty()) {


    stack.pop()


    }

    View full-size slide

  36. 2. Try-Parse pattern
    • Unvalidated input data

    • tryParse - veri
    fi
    es if the input is valid and can be parsed

    • parse - performs the actual parsing

    View full-size slide

  37. if (SerialNumber.tryParse(possiblySerialNumber)) {


    val serialNumber = SerialNumber.parse(possiblySerialNumber)


    save(serialNumber)


    }

    View full-size slide

  38. 3. Converting exceptions to values
    • Non-trivial exceptional conditions

    • Exception has domain meaning

    • Has to be communicated to the consumer

    • Use Kotlin’s sealed classes, data classes and object to provide meaning

    View full-size slide

  39. Registering a new user from a client
    • Successfully creates a user account

    • Email already registered

    • Validation errors

    • Connection errors

    • 5xx server errors

    • Unknown errors

    View full-size slide

  40. Registering a new user from a client
    • Successfully creates a user account

    • Email already registered

    • Validation errors

    • Connection errors

    • 5xx server errors

    • Unknown errors

    View full-size slide

  41. SignupResponse.kt
    sealed class SignupResponse {


    data class AccountCreated(val userId: String) : SignupResponse()


    data class EmailAlreadyRegistered(val registrationEmail: String) : SignupResponse()


    data class InputValidationFailed(val errors: List) : SignupResponse()


    object ConnectionError : SignupResponse()


    data class ServerError(val statusCode: Int) : SignupResponse()


    data class UnknownError(val statusCode: Int, val responseBody: String?) : SignupResponse()


    }

    View full-size slide

  42. Usage
    val response = accountsApi.signup("John Doe", "[email protected]")


    when (response) {


    is AccountCreated
    ->
    saveUserId(response.userId)


    is EmailAlreadyRegistered
    -
    >
    showEmailAlreadyRegistered(response.registrationEmail)


    is InputValidationFailed
    ->
    showValidationErrors(response.errors)


    ConnectionError
    ->
    showCheckConnectionMessage()


    is ServerError, is UnknownError
    ->
    showTryAgainInSometimeMessage()


    }

    View full-size slide

  43. Exceptions summary
    • Exception handling summary

    • Kotlin’s try expression

    • Tester-Doer pattern

    • Try-Parse pattern

    • Converting exceptions to values

    View full-size slide

  44. Objective
    • Find attendees that have RSVP’d “Yes” or “Maybe”

    • Remind them about an upcoming event

    View full-size slide

  45. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    val attendees = dao.getAttendees()


    val potentialAttendees = attendees


    .filter { it.rsvp
    ==
    Rsvp.YES
    |
    |
    it.rsvp
    ==
    Rsvp.MAYBE }


    potentialAttendees


    .onEach { mailer.mail(it) }


    }


    }

    View full-size slide

  46. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    val attendees = dao.getAttendees()


    val potentialAttendees = attendees


    .filter { it.rsvp
    ==
    Rsvp.YES
    |
    |
    it.rsvp
    ==
    Rsvp.MAYBE }


    potentialAttendees


    .onEach { mailer.mail(it) }


    }


    }

    View full-size slide

  47. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    val attendees = dao.getAttendees()


    val potentialAttendees = attendees


    .filter { it.rsvp
    ==
    Rsvp.YES
    |
    |
    it.rsvp
    ==
    Rsvp.MAYBE }


    potentialAttendees


    .onEach { mailer.mail(it) }


    }


    }

    View full-size slide

  48. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    val attendees = dao.getAttendees()


    val potentialAttendees = attendees


    .filter { it.rsvp
    ==
    Rsvp.YES
    |
    |
    it.rsvp
    ==
    Rsvp.MAYBE }


    potentialAttendees


    .onEach { mailer.mail(it) }


    }


    }

    View full-size slide

  49. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    val attendees = dao.getAttendees()


    val potentialAttendees = attendees


    .filter { it.rsvp
    ==
    Rsvp.YES
    |
    |
    it.rsvp
    ==
    Rsvp.MAYBE }


    potentialAttendees


    .onEach { mailer.mail(it) }


    }


    }

    View full-size slide

  50. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    val attendees = dao.getAttendees()


    val potentialAttendees = attendees


    .filter { it.rsvp
    ==
    Rsvp.YES
    |
    |
    it.rsvp
    ==
    Rsvp.MAYBE }


    potentialAttendees


    .onEach { mailer.mail(it) }


    }


    }

    View full-size slide

  51. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    val attendees = dao.getAttendees()


    val potentialAttendees = attendees


    .filter { it.rsvp
    ==
    Rsvp.YES
    |
    |
    it.rsvp
    ==
    Rsvp.MAYBE }


    potentialAttendees


    .onEach { mailer.mail(it) }


    }


    }

    View full-size slide

  52. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    val attendees = dao.getAttendees()


    val potentialAttendees = attendees


    .filter { it.rsvp
    ==
    Rsvp.YES
    |
    |
    it.rsvp
    ==
    Rsvp.MAYBE }


    potentialAttendees


    .onEach { mailer.mail(it) }


    }


    }

    View full-size slide

  53. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    val attendees = dao.getAttendees()


    val potentialAttendees = attendees


    .filter { it.rsvp
    ==
    Rsvp.YES
    |
    |
    it.rsvp
    ==
    Rsvp.MAYBE }


    potentialAttendees


    .onEach { mailer.mail(it) }


    }


    }

    View full-size slide

  54. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    val attendees = dao.getAttendees()


    val potentialAttendees = attendees


    .filter { it.rsvp
    ==
    Rsvp.YES
    |
    |
    it.rsvp
    ==
    Rsvp.MAYBE }


    potentialAttendees


    .onEach { mailer.mail(it) }


    }


    }

    View full-size slide

  55. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    val attendees = dao.getAttendees()


    val potentialAttendees = attendees


    .filter { it.rsvp
    ==
    Rsvp.YES
    |
    |
    it.rsvp
    ==
    Rsvp.MAYBE }


    potentialAttendees


    .onEach { mailer.mail(it) }


    }


    }

    View full-size slide

  56. Values as boundaries

    View full-size slide

  57. fun List.attending(): List {


    return filter { it.rsvp
    ==
    Rsvp.YES
    |
    |
    it.rsvp
    ==
    Rsvp.MAYBE }


    }

    View full-size slide

  58. fun List.attending(): List {


    return filter { it.rsvp
    ==
    Rsvp.YES
    |
    |
    it.rsvp
    ==
    Rsvp.MAYBE }


    }

    View full-size slide

  59. fun List.attending(): List {


    return filter { it.rsvp
    ==
    Rsvp.YES
    |
    |
    it.rsvp
    ==
    Rsvp.MAYBE }


    }

    View full-size slide

  60. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    dao.getAttendees()


    .attending()


    .onEach { mailer.mail(it) }


    }


    }

    View full-size slide

  61. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    dao.getAttendees()


    .attending()


    .onEach { mailer.mail(it) }


    }


    }

    View full-size slide

  62. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    dao.getAttendees()


    .attending()


    .onEach { mailer.mail(it) }


    }


    }

    View full-size slide

  63. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    dao.getAttendees()


    .attending()


    .onEach { mailer.mail(it) }


    }


    }

    View full-size slide

  64. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    dao.getAttendees()


    .attending()


    .onEach { mailer.mail(it) }


    }


    }

    View full-size slide

  65. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    dao.getAttendees()


    .attending()


    .onEach { mailer.mail(it) }


    }


    }

    View full-size slide

  66. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    dao.getAttendees()


    .attending()


    .onEach { mailer.mail(it) }


    }


    }

    View full-size slide

  67. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    dao.getAttendees()


    .attending()


    .onEach { mailer.mail(it) }


    }


    }

    View full-size slide

  68. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    dao.getAttendees()


    .attending()


    .onEach { mailer.mail(it) }


    }


    }

    View full-size slide

  69. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    dao.getAttendees()


    .attending()


    .onEach { mailer.mail(it) }


    }


    }

    View full-size slide

  70. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    dao.getAttendees()


    .attending()


    .onEach { mailer.mail(it) }


    }


    }

    View full-size slide

  71. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    dao.getAttendees()


    .attending()


    .onEach { mailer.mail(it) }


    }


    }

    View full-size slide

  72. Testing (Isolated for logic & Integrated for effects)
    • No attendees

    • All RSVP’d “Yes”

    • All RSVP’d “No”

    • All RSVP’d “Maybe”

    • Mix of “Yes”, “No”, “Maybe”
    • Query database, Send email

    View full-size slide

  73. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    val attendees = dao.getAttendees()


    val potentialAttendees = attendees


    .filter { it.rsvp
    ==
    Rsvp.YES
    |
    |
    it.rsvp
    ==
    Rsvp.MAYBE }


    potentialAttendees


    .onEach { mailer.mail(it) }


    }


    }
    Testing 😱

    View full-size slide

  74. Testing (Isolated for logic & Integrated for effects)
    • No attendees

    • All RSVP’d “Yes”

    • All RSVP’d “No”

    • All RSVP’d “Maybe”

    • Mix of “Yes”, “No”, “Maybe”
    • Query database, Send email

    View full-size slide

  75. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    dao.getAttendees()


    .attending()


    .onEach { mailer.mail(it) }


    }


    }
    Testing 🥳

    View full-size slide

  76. Core
    • Path +

    • Dependencies -

    • Isolated tests
    Shell
    • Path -

    • Dependencies +

    • Integrated tests

    View full-size slide

  77. https://github.com/redgreenio/
    fl
    uid

    View full-size slide

  78. Value as boundaries in the wild
    • HTTP

    • IPC

    • RPC

    • Actor-based systems

    • Event-driven systems

    • Message queues

    • Event streams

    View full-size slide

  79. Architectures
    • Functional core & imperative shell

    • Hexagonal architecture

    • Ports & adapters

    • Redux

    View full-size slide

  80. References
    • https://docs.oracle.com/javase/tutorial/essential/exceptions/index.html

    • https://kotlinlang.org/docs/exceptions.html

    • https://radio-weblogs.com/0122027/stories/2003/04/01/JavasCheckedExceptionsWereAMistake.html

    • https://www.artima.com/articles/the-trouble-with-checked-exceptions

    • https://www.destroyallsoftware.com/talks/boundaries

    • https://github.com/spotify/mobius

    • https://github.com/redgreenio/fluid

    View full-size slide

  81. Questions?


    @ragunathjawahar • https:
    /
    /
    ragunath.xyz

    View full-size slide