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 Slide

  2. Why?
    • People

    • Code

    • Process

    • Tooling

    View Slide

  3. Why?
    • People

    • Code

    • Process

    • Tooling

    View Slide

  4. Code
    • Less bureaucracy

    • Simple ideas

    • Starting point to hone your craft

    View 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 Slide

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

    View Slide

  7. Pendulum swings

    View 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 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 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 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 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 Slide

  13. Exceptions

    View Slide

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

    View Slide

  15. Treat exceptions as first class citizens.

    View Slide

  16. Exceptions
    Anti-patterns

    View Slide

  17. try {


    / /
    make a network call


    } catch (e: IOException) {


    / /
    Gulp
    !!
    🥤


    } catch (e: HttpRetryException) {


    / /
    Delicious 😋


    }
    1. Swallowing exceptions

    View Slide

  18. try {


    / /
    make a network call


    } catch (e: Exception) {


    showSomethingWentWrong()


    }
    2. Catch-all

    View Slide

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


    return try {


    Integer.parseInt(candidate)


    true


    } catch (e: NumberFormatException) {


    false


    }


    }
    3. Using exceptions for control flow

    View 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 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 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 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 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 Slide




  25. 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 Slide

  26. View Slide

  27. for (candidate in potentialNumbers) {


    if (isNumber(candidate)) {


    println("It's a number!")


    } else {


    println("NaN")


    }


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

    View Slide

  28. Tests

    View Slide

  29. assertThat(false).isTrue()
    Tests

    View Slide

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


    at CanaryTestKt.main(CanaryTest.kt:15)


    at CanaryTestKt.main(CanaryTest.kt)
    Tests

    View Slide

  31. if (balance < requestedAmount) {


    throw Exception("Meh… 😒")


    }
    4. Throwing Exception

    View Slide

  32. Exception handling in Kotlin
    • No checked exceptions

    • try is an expression

    View Slide

  33. var guardDogResult: Result


    try {


    guardDogResult = getGuardDog()


    } catch (e: NoGuardDogException) {


    guardDogResult = Result.failure(e)


    }
    try expression

    View Slide

  34. val guardDogResult: Result = try {


    getGuardDog()


    } catch (e: NoGuardDogException) {


    Result.failure(e)


    }
    try expression

    View Slide

  35. Exception handling patterns

    View Slide

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

    • Doer - a member that performs a potentially throwing operation

    View Slide

  37. stack.pop()

    View Slide

  38. 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 Slide

  39. if (stack.isNotEmpty()) {


    stack.pop()


    }

    View Slide

  40. 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 Slide

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


    val serialNumber = SerialNumber.parse(possiblySerialNumber)


    save(serialNumber)


    }

    View Slide

  42. 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 Slide

  43. 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 Slide

  44. 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 Slide

  45. 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 Slide

  46. 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 Slide

  47. Exceptions summary
    • Exception handling summary

    • Kotlin’s try expression

    • Tester-Doer pattern

    • Try-Parse pattern

    • Converting exceptions to values

    View Slide

  48. Boundaries

    View Slide

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

    • Remind them about an upcoming event

    View 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 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 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 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 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 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 Slide

  56. 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 Slide

  57. 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 Slide

  58. 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 Slide

  59. 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 Slide

  60. 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 Slide

  61. Values as boundaries

    View Slide

  62. fun List.attending(): List {


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


    }

    View Slide

  63. fun List.attending(): List {


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


    }

    View Slide

  64. fun List.attending(): List {


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


    }

    View Slide

  65. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    dao.getAttendees()


    .attending()


    .onEach { mailer.mail(it) }


    }


    }

    View Slide

  66. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    dao.getAttendees()


    .attending()


    .onEach { mailer.mail(it) }


    }


    }

    View Slide

  67. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    dao.getAttendees()


    .attending()


    .onEach { mailer.mail(it) }


    }


    }

    View Slide

  68. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    dao.getAttendees()


    .attending()


    .onEach { mailer.mail(it) }


    }


    }

    View Slide

  69. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    dao.getAttendees()


    .attending()


    .onEach { mailer.mail(it) }


    }


    }

    View Slide

  70. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    dao.getAttendees()


    .attending()


    .onEach { mailer.mail(it) }


    }


    }

    View Slide

  71. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    dao.getAttendees()


    .attending()


    .onEach { mailer.mail(it) }


    }


    }

    View Slide

  72. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    dao.getAttendees()


    .attending()


    .onEach { mailer.mail(it) }


    }


    }

    View Slide

  73. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    dao.getAttendees()


    .attending()


    .onEach { mailer.mail(it) }


    }


    }

    View Slide

  74. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    dao.getAttendees()


    .attending()


    .onEach { mailer.mail(it) }


    }


    }

    View Slide

  75. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    dao.getAttendees()


    .attending()


    .onEach { mailer.mail(it) }


    }


    }

    View Slide

  76. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    dao.getAttendees()


    .attending()


    .onEach { mailer.mail(it) }


    }


    }

    View Slide

  77. 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 Slide

  78. 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 Slide

  79. 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 Slide

  80. class EventManager(


    private val dao: EventDao,


    private val mailer: EventMailer


    ) {


    fun sendReminder() {


    dao.getAttendees()


    .attending()


    .onEach { mailer.mail(it) }


    }


    }
    Testing 🥳

    View Slide

  81. Core
    • Path +

    • Dependencies -

    • Isolated tests
    Shell
    • Path -

    • Dependencies +

    • Integrated tests

    View Slide

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

    View Slide

  83. Value as boundaries in the wild
    • HTTP

    • IPC

    • RPC

    • Actor-based systems

    • Event-driven systems

    • Message queues

    • Event streams

    View Slide

  84. Architectures
    • Functional core & imperative shell

    • Hexagonal architecture

    • Ports & adapters

    • Redux

    View Slide

  85. 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 Slide

  86. Questions?


    @ragunathjawahar • https:
    /
    /
    ragunath.xyz

    View Slide