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. 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
  2. // Primitive obsession val password: String = "super secret stuff!"

    PasswordValidator.validate(password) // Encapsulated type val password: Password = Password("super secret stuff!") password.validate(password)
  3. // Primitive obsession val password: String = "super secret stuff!"

    PasswordValidator.validate(password) // Encapsulated type val password: Password = Password("super secret stuff!") password.validate(password)
  4. // Primitive obsession val password: String = "super secret stuff!"

    PasswordValidator.validate(password) // Encapsulated type val password: Password = Password("super secret stuff!") password.validate(password)
  5. // Primitive obsession val password: String = "super secret stuff!"

    PasswordValidator.validate(password) // Encapsulated type val password: Password = Password("super secret stuff!") password.validate(password)
  6. 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
  7. try { / / make a network call } catch

    (e: IOException) { / / Gulp !! 🥤 } catch (e: HttpRetryException) { / / Delicious 😋 } 1. Swallowing exceptions
  8. try { / / make a network call } catch

    (e: Exception) { showSomethingWentWrong() } 2. Catch-all
  9. private fun isNumber(candidate: String): Boolean { return try { Integer.parseInt(candidate)

    true } catch (e: NumberFormatException) { false } } 3. Using exceptions for control flow
  10. … 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) …
  11. … 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) …
  12. … 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) …
  13. … 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) …
  14. … 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) …
  15. … 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) …
  16. for (candidate in potentialNumbers) { if (isNumber(candidate)) { println("It's a

    number!") } else { println("NaN") } } 3. Using exceptions for control flow (contd…)
  17. var guardDogResult: Result<Dog> try { guardDogResult = getGuardDog() } catch

    (e: NoGuardDogException) { guardDogResult = Result.failure(e) } try expression
  18. val guardDogResult: Result<Dog> = try { getGuardDog() } catch (e:

    NoGuardDogException) { Result.failure(e) } try expression
  19. 1. Tester-Doer pattern • Tester - a member that is

    used to test a condition • Doer - a member that performs a potentially throwing operation
  20. 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
  21. 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
  22. Registering a new user from a client • Successfully creates

    a user account • Email already registered • Validation errors • Connection errors • 5xx server errors • Unknown errors
  23. Registering a new user from a client • Successfully creates

    a user account • Email already registered • Validation errors • Connection errors • 5xx server errors • Unknown errors
  24. 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<ValidationError>) : SignupResponse() object ConnectionError : SignupResponse() data class ServerError(val statusCode: Int) : SignupResponse() data class UnknownError(val statusCode: Int, val responseBody: String?) : SignupResponse() }
  25. 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() }
  26. Exceptions summary • Exception handling summary • Kotlin’s try expression

    • Tester-Doer pattern • Try-Parse pattern • Converting exceptions to values
  27. 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) } } }
  28. 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) } } }
  29. 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) } } }
  30. 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) } } }
  31. 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) } } }
  32. 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) } } }
  33. 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) } } }
  34. 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) } } }
  35. 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) } } }
  36. 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) } } }
  37. 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) } } }
  38. class EventManager( private val dao: EventDao, private val mailer: EventMailer

    ) { fun sendReminder() { dao.getAttendees() .attending() .onEach { mailer.mail(it) } } }
  39. class EventManager( private val dao: EventDao, private val mailer: EventMailer

    ) { fun sendReminder() { dao.getAttendees() .attending() .onEach { mailer.mail(it) } } }
  40. class EventManager( private val dao: EventDao, private val mailer: EventMailer

    ) { fun sendReminder() { dao.getAttendees() .attending() .onEach { mailer.mail(it) } } }
  41. class EventManager( private val dao: EventDao, private val mailer: EventMailer

    ) { fun sendReminder() { dao.getAttendees() .attending() .onEach { mailer.mail(it) } } }
  42. class EventManager( private val dao: EventDao, private val mailer: EventMailer

    ) { fun sendReminder() { dao.getAttendees() .attending() .onEach { mailer.mail(it) } } }
  43. class EventManager( private val dao: EventDao, private val mailer: EventMailer

    ) { fun sendReminder() { dao.getAttendees() .attending() .onEach { mailer.mail(it) } } }
  44. class EventManager( private val dao: EventDao, private val mailer: EventMailer

    ) { fun sendReminder() { dao.getAttendees() .attending() .onEach { mailer.mail(it) } } }
  45. class EventManager( private val dao: EventDao, private val mailer: EventMailer

    ) { fun sendReminder() { dao.getAttendees() .attending() .onEach { mailer.mail(it) } } }
  46. class EventManager( private val dao: EventDao, private val mailer: EventMailer

    ) { fun sendReminder() { dao.getAttendees() .attending() .onEach { mailer.mail(it) } } }
  47. class EventManager( private val dao: EventDao, private val mailer: EventMailer

    ) { fun sendReminder() { dao.getAttendees() .attending() .onEach { mailer.mail(it) } } }
  48. class EventManager( private val dao: EventDao, private val mailer: EventMailer

    ) { fun sendReminder() { dao.getAttendees() .attending() .onEach { mailer.mail(it) } } }
  49. class EventManager( private val dao: EventDao, private val mailer: EventMailer

    ) { fun sendReminder() { dao.getAttendees() .attending() .onEach { mailer.mail(it) } } }
  50. 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
  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) } } } Testing 😱
  52. 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
  53. class EventManager( private val dao: EventDao, private val mailer: EventMailer

    ) { fun sendReminder() { dao.getAttendees() .attending() .onEach { mailer.mail(it) } } } Testing 🥳
  54. Core • Path + • Dependencies - • Isolated tests

    Shell • Path - • Dependencies + • Integrated tests
  55. Value as boundaries in the wild • HTTP • IPC

    • RPC • Actor-based systems • Event-driven systems • Message queues • Event streams