$30 off During Our Annual Pro Sale. View Details »

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
  2. Why? • People • Code • Process • Tooling

  3. Why? • People • Code • Process • Tooling

  4. Code • Less bureaucracy • Simple ideas • Starting point

    to hone your craft
  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
  6. Episode 1 https://bit.ly/30zwgTy

  7. Pendulum swings

  8. // Primitive obsession val password: String = "super secret stuff!"

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

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

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

    PasswordValidator.validate(password) // Encapsulated type val password: Password = Password("super secret stuff!") password.validate(password)
  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
  13. Exceptions

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

    phrase “exceptional event.”
  15. Treat exceptions as first class citizens.

  16. Exceptions Anti-patterns

  17. try { / / make a network call } catch

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

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

    true } catch (e: NumberFormatException) { false } } 3. Using exceptions for control flow
  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) …
  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) …
  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) …
  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) …
  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) …
  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) …
  26. None
  27. for (candidate in potentialNumbers) { if (isNumber(candidate)) { println("It's a

    number!") } else { println("NaN") } } 3. Using exceptions for control flow (contd…)
  28. Tests

  29. assertThat(false).isTrue() Tests

  30. Exception in thread "main" expected to be true at CanaryTestKt.main(CanaryTest.kt:15)

    at CanaryTestKt.main(CanaryTest.kt) Tests
  31. if (balance < requestedAmount) { throw Exception("Meh… 😒") } 4.

    Throwing Exception
  32. Exception handling in Kotlin • No checked exceptions • try

    is an expression
  33. var guardDogResult: Result<Dog> try { guardDogResult = getGuardDog() } catch

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

    NoGuardDogException) { Result.failure(e) } try expression
  35. Exception handling patterns

  36. 1. Tester-Doer pattern • Tester - a member that is

    used to test a condition • Doer - a member that performs a potentially throwing operation
  37. stack.pop()

  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)

  39. if (stack.isNotEmpty()) { stack.pop() }

  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
  41. if (SerialNumber.tryParse(possiblySerialNumber)) { val serialNumber = SerialNumber.parse(possiblySerialNumber) save(serialNumber) }

  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
  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
  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
  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<ValidationError>) : SignupResponse() object ConnectionError : SignupResponse() data class ServerError(val statusCode: Int) : SignupResponse() data class UnknownError(val statusCode: Int, val responseBody: String?) : SignupResponse() }
  46. Usage val response = accountsApi.signup("John Doe", "john@example.org") when (response) {

    is AccountCreated -> saveUserId(response.userId) is EmailAlreadyRegistered - > showEmailAlreadyRegistered(response.registrationEmail) is InputValidationFailed -> showValidationErrors(response.errors) ConnectionError -> showCheckConnectionMessage() is ServerError, is UnknownError -> showTryAgainInSometimeMessage() }
  47. Exceptions summary • Exception handling summary • Kotlin’s try expression

    • Tester-Doer pattern • Try-Parse pattern • Converting exceptions to values
  48. Boundaries

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

    • Remind them about an upcoming event
  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) } } }
  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) } } }
  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) } } }
  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) } } }
  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) } } }
  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) } } }
  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) } } }
  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) } } }
  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) } } }
  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) } } }
  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) } } }
  61. Values as boundaries

  62. fun List<Attendee>.attending(): List<Attendee> { return filter { it.rsvp == Rsvp.YES

    | | it.rsvp == Rsvp.MAYBE } }
  63. fun List<Attendee>.attending(): List<Attendee> { return filter { it.rsvp == Rsvp.YES

    | | it.rsvp == Rsvp.MAYBE } }
  64. fun List<Attendee>.attending(): List<Attendee> { return filter { it.rsvp == Rsvp.YES

    | | it.rsvp == Rsvp.MAYBE } }
  65. class EventManager( private val dao: EventDao, private val mailer: EventMailer

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

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

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

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

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

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

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

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

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

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

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

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

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

    Shell • Path - • Dependencies + • Integrated tests
  82. https://github.com/redgreenio/ fl uid

  83. Value as boundaries in the wild • HTTP • IPC

    • RPC • Actor-based systems • Event-driven systems • Message queues • Event streams
  84. Architectures • Functional core & imperative shell • Hexagonal architecture

    • Ports & adapters • Redux
  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
  86. Questions? @ragunathjawahar • https: / / ragunath.xyz