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

Railway Oriented Programming with Kotlin

Railway Oriented Programming with Kotlin

Many examples in Kotlin assume that you are always on the "happy path". But real-world applications must deal with a lot of types of errors: validation, network, and others.

So, how do you handle all of this?

Marcello Galhardo

June 05, 2019
Tweet

More Decks by Marcello Galhardo

Other Decks in Programming

Transcript

  1. Button clicks send Action to ViewModel ViewModel invokes UseCase to

    validate and canonicalise Action data UseCase ask Repository to update user data
  2. Button clicks send Action to ViewModel ViewModel invokes UseCase to

    validate and canonicalise Action data UseCase ask Repository to update user data Returns result to UI
  3. Action.kt sealed class Action { data class UpdateUser( val id:

    UserId, val name: String, val email: String ) : Action() }
  4. Action.kt sealed class Action { data class UpdateUser( val id:

    UserId, val name: String, val email: String ) : Action() }
  5. Action.kt sealed class Action { data class UpdateUser( val id:

    UserId, val name: String, val email: String ) : Action() }
  6. View.kt // Action dispatcher in a View okButton.setOnClickListener { val

    name = nameTextField.value val email = emailTextField.value val action = Action.UpdateUser(userId, name, email) viewModel.dispatch(action) }
  7. UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result<User> { validate(action) val

    canonicalizedEmail = canonicalizeEmail(action) val user = repository.getUserById(id).copy( name = action.name, email = canonicalizedEmail ) repository.update(user) return Result.success(user) }
  8. UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result<User> { validate(action) val

    canonicalizedEmail = canonicalizeEmail(action) val user = repository.getUserById(id).copy( name = action.name, email = canonicalizedEmail ) repository.update(user) return Result.success(user) }
  9. UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result<User> { validate(action) val

    canonicalizedEmail = canonicalizeEmail(action) val user = repository.getUserById(id).copy( name = action.name, email = canonicalizedEmail ) repository.update(user) return Result.success(user) }
  10. UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result<User> { validate(action) val

    canonicalizedEmail = canonicalizeEmail(action) val user = repository.getUserById(id).copy( name = action.name, email = canonicalizedEmail ) repository.update(user) return Result.success(user) }
  11. UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result<User> { validate(action) val

    canonicalizedEmail = canonicalizeEmail(action) val user = repository.getUserById(id).copy( name = action.name, email = canonicalizedEmail ) repository.update(user) return Result.success(user) }
  12. UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result<User> { validate(action) val

    canonicalizedEmail = canonicalizeEmail(action) val user = repository.getUserById(id).copy( name = action.name, email = canonicalizedEmail ) repository.update(user) return Result.success(user) }
  13. Functions // ValidateUser.kt fun validate(action: Action.UpdateUser): Boolean // CanonicalizeEmail.kt fun

    canonicalizeEmail(action: Action.UpdateUser): String // UserRepository.kt @Throws(Exception::class) fun Repository.getUserById(id: UserId): User? @Throws(Exception::class) fun Repository.update(user: User)
  14. Functions // ValidateUser.kt fun validate(action: Action.UpdateUser): Boolean // CanonicalizeEmail.kt fun

    canonicalizeEmail(action: Action.UpdateUser): String // UserRepository.kt @Throws(Exception::class) fun Repository.getUserById(id: UserId): User? @Throws(Exception::class) fun Repository.update(user: User)
  15. Functions // ValidateUser.kt fun validate(action: Action.UpdateUser): Boolean // CanonicalizeEmail.kt fun

    canonicalizeEmail(action: Action.UpdateUser): String // UserRepository.kt @Throws(Exception::class) fun Repository.getUserById(id: UserId): User? @Throws(Exception::class) fun Repository.update(user: User)
  16. Functions // ValidateUser.kt fun validate(action: Action.UpdateUser): Boolean // CanonicalizeEmail.kt fun

    canonicalizeEmail(action: Action.UpdateUser): String // UserRepository.kt @Throws(Exception::class) fun Repository.getUserById(id: UserId): User? @Throws(Exception::class) fun Repository.update(user: User)
  17. “As a user I want to update my name and

    email address… …and see error messages when something goes wrong!"
  18. “As a user I want to update my name and

    email address… …and see error messages when something goes wrong!"
  19. Button clicks send Action to ViewModel ViewModel invokes UseCase to

    validate and canonicalise Action data UseCase ask Repository to update user data Returns result to UI
  20. Button clicks send Action to ViewModel ViewModel invokes UseCase to

    validate and canonicalise Action data UseCase ask Repository to update user data Returns result to UI
  21. Name is blank or email not valid Button clicks send

    Action to ViewModel ViewModel invokes UseCase to validate and canonicalise Action data UseCase ask Repository to update user data Returns result to UI
  22. Name is blank or email not valid Button clicks send

    Action to ViewModel ViewModel invokes UseCase to validate and canonicalise Action data UseCase ask Repository to update user data Returns result to UI
  23. Name is blank or email not valid User not found

    Button clicks send Action to ViewModel ViewModel invokes UseCase to validate and canonicalise Action data UseCase ask Repository to update user data Returns result to UI
  24. UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result<User> { validate(action) val

    canonicalizedEmail = canonicalizeEmail(action) val user = repository.getUserById(id).copy( name = action.name, email = canonicalizedEmail ) repository.update(user) return Result.success(user) }
  25. UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result<User> { validate(action) val

    canonicalizedEmail = canonicalizeEmail(action) val user = repository.getUserById(id).copy( name = action.name, email = canonicalizedEmail ) repository.update(user) return Result.success(user) }
  26. UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result<User> { val isValid

    = validate(action) if (!isValid) { return Result.error(UserNotValidException()) } val canonicalizedEmail = canonicalizeEmail(action) val user = repository.getUserById(id).copy( name = action.name, email = canonicalizedEmail ) repository.update(user) return Result.success(user) }
  27. UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result<User> { val isValid

    = validate(action) if (!isValid) { return Result.error(UserNotValidException()) } val canonicalizedEmail = canonicalizeEmail(action) val user = repository.getUserById(id).copy( name = action.name, email = canonicalizedEmail ) repository.update(user) return Result.success(user) }
  28. UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result<User> { val isValid

    = validate(action) if (!isValid) { return Result.error(UserNotValidException()) } val canonicalizedEmail = canonicalizeEmail(action) val user = repository.getUserById(id).copy( name = action.name, email = canonicalizedEmail ) repository.update(user) return Result.success(user) }
  29. UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result<User> { val isValid

    = validate(action) if (!isValid) { return Result.error(UserNotValidException()) } val canonicalizedEmail = canonicalizeEmail(action) var user = null try { user = repository.getUserById(id).copy( name = action.name, email = canonicalizedEmail ) repository.update(user) } catch (error: Exception) { return Result.error(error) } return Result.success(user) }
  30. UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result<User> { val isValid

    = validate(action) if (!isValid) { return Result.error(UserNotValidException()) } val canonicalizedEmail = canonicalizeEmail(action) var user = null try { user = repository.getUserById(id).copy( name = action.name, email = canonicalizedEmail ) repository.update(user) } catch (error: Exception) { return Result.error(error) } return Result.success(user) }
  31. • Each use case will be equivalent to a single

    function • The function will return a sum type with two cases: Success or Failure • The use case function will be built from a series of smaller functions, each representing one step in a data flow • The errors from each step will be combined into a single failure
  32. • Each use case will be equivalent to a single

    function • The function will return a sum type with two cases: Success, Failure • The use case function will be built from a series of smaller functions, each representing one step in a data flow • The errors from each step will be combined into a single failure
  33. • Each use case will be equivalent to a single

    function • The function will return a sum type with two cases: Success, Failure • The use case function will be built from a series of smaller functions, each representing one step in a data flow • The errors from each step will be combined into a single failure
  34. • Each use case will be equivalent to a single

    function • The function will return a sum type with two cases: Success, Failure • The use case function will be built from a series of smaller functions, each representing one step in a data flow • Each step will be combined into a single result
  35. bit.do/kotlin-std-result sealed class Result<Value> { data class Success( val value:

    Value ): Result<Value> data class Failure( val error: Exception ): Result<Nothing> }
  36. bit.do/kotlin-std-result sealed class Result<Value> { data class Success( val value:

    Value ): Result<Value> data class Failure( val error: Exception ): Result<Nothing> }
  37. bit.do/kotlin-std-result sealed class Result<Value> { data class Success( val value:

    Value ): Result<Value> data class Failure( val error: Exception ): Result<Nothing> }
  38. bit.do/kotlin-std-result sealed class Result<Value> { data class Success( val value:

    Value ): Result<Value> data class Failure( val error: String ): Result<Nothing> }
  39. public inline fun <R, T> Result<T>.map(transform: (value: T) -> R):

    Result<R> { return when { isSuccess -> Result.success(transform(value as T)) else -> Result(value) } } bit.do/kotlin-std-result
  40. public inline fun <R, T> Result<T>.map(transform: (value: T) -> R):

    Result<R> { return when { isSuccess -> Result.success(transform(value as T)) else -> Result(value) } } bit.do/kotlin-std-result
  41. public inline fun <R, T> Result<T>.map(transform: (value: T) -> R):

    Result<R> { return when { isSuccess -> Result.success(transform(value as T)) else -> Result(value) } } bit.do/kotlin-std-result
  42. public inline fun <R, T> Result<T>.map(transform: (value: T) -> R):

    Result<R> { return when { isSuccess -> Result.success(transform(value as T)) else -> Result(value) } } bit.do/kotlin-std-result
  43. public inline fun <R, T> Result<T>.flatMap( transform: (value: T) ->

    Result<R> ): Result<R> { return when { isSuccess -> transform(value as T) else -> Result(value) } } bit.do/kotlin-std-result
  44. public inline fun <R, T> Result<T>.flatMap( transform: (value: T) ->

    Result<R> ): Result<R> { return when { isSuccess -> transform(value as T) else -> Result(value) } } bit.do/kotlin-std-result
  45. public inline fun <R, T> Result<T>.flatMap( transform: (value: T) ->

    Result<R> ): Result<R> { return when { isSuccess -> transform(value as T) else -> Result(value) } } bit.do/kotlin-std-result
  46. public inline fun <R, T> Result<T>.flatMap( transform: (value: T) ->

    Result<R> ): Result<R> { return when { isSuccess -> transform(value as T) else -> Result(value) } } bit.do/kotlin-std-result
  47. ViewModel invokes UseCase to validate and canonicalise Action data Failure

    UseCase ask Repository to update user data ? Success
  48. Success ViewModel invokes UseCase to validate and canonicalise Action data

    UseCase ask Repository to update user data Failure Success Failure ?
  49. Success ViewModel invokes UseCase to validate and canonicalise Action data

    Failure UseCase ask Repository to update user data Failure Success
  50. GetUserById.kt fun getUserById(input: Action.UpdateUser): Result<User> { val user = repository.getUserByIdOrNull(input.id)

    return when { user == null -> Result.failure("User not found.") else -> Result.success(user) } }
  51. NameNotBlank.kt fun nameNotBlank(user: User) = when { user.name == ""

    -> Result.failure("Name must not be blank.") else -> Result.success(user) }
  52. NameIsBelowMaxLength.kt fun nameIsBelowMaxLength(user: User) = when { user.name.length > 50

    -> Result.failure("Name must not be longer than 50 chars.") else -> Result.success(user) }
  53. EmailNotBlank.kt fun emailNotBlank(user: User) = when { user.email == ""

    -> Result.failure("Email must not be blank.") else -> Result.success(user) }
  54. UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result<User> { return getUserById(action.id)

    .map { user -> user.copy(name = action.name) } .map { user -> user.copy(email = action.email) } .map(::nameNotBlank) .map(::nameIsBelowMaxLength) .map(::emailNotBlank) .map(::canonicalizeEmail) .map(::saveUser) }
  55. UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result<User> { return getUserById(action.id)

    .map { user -> user.copy(name = action.name) } .map { user -> user.copy(email = action.email) } .map(::nameNotBlank) .map(::nameIsBelowMaxLength) .map(::emailNotBlank) .map(::canonicalizeEmail) .map(::saveUser) }
  56. UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result<User> { return getUserById(action.id)

    .map { user -> user.copy(name = action.name) } .map { user -> user.copy(email = action.email) } .map(::nameNotBlank) .map(::nameIsBelowMaxLength) .map(::emailNotBlank) .map(::canonicalizeEmail) .map(::saveUser) }
  57. UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result<User> { return getUserById(action.id)

    .map { user -> user.copy(name = action.name) } .map { user -> user.copy(email = action.email) } .flatMap(::nameNotBlank) .flatMap(::nameIsBelowMaxLength) .flatMap(::emailNotBlank) .map(::canonicalizeEmail) .map(::saveUser) }
  58. UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result<User> { return getUserById(action.id)

    .map { user -> user.copy(name = action.name) } .map { user -> user.copy(email = action.email) } .flatMap(::nameNotBlank) .flatMap(::nameIsBelowMaxLength) .flatMap(::emailNotBlank) .flatMap(::canonicalizeEmail) .map(::saveUser) }
  59. UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result<User> { return getUserById(action.id)

    .map { user -> user.copy(name = action.name) } .map { user -> user.copy(email = action.email) } .flatMap(::nameNotBlank) .flatMap(::nameIsBelowMaxLength) .flatMap(::emailNotBlank) .flatMap(::canonicalizeEmail) .flatMap(::saveUser) }
  60. UpdateUser.kt fun updateUser( action: Action.UpdateUser ): Result<User> { return getUserById(action.id)

    .map { user -> user.copy(name = action.name) } .map { user -> user.copy(email = action.email) } .flatMap(::nameNotBlank) .flatMap(::nameIsBelowMaxLength) .flatMap(::emailNotBlank) .flatMap(::canonicalizeEmail) .flatMap(::saveUser) }
  61. ViewModel.kt fun dispatch(action: Action) { useCase.invoke() .onSuccess { // Do

    something cool! }.onFailure { // Ops, something went wrong… } }
  62. ViewModel.kt fun dispatch(action: Action) { updateUser(action) .onSuccess { // Do

    something cool! }.onFailure { // Ops, something went wrong… } }
  63. ViewModel.kt fun dispatch(action: Action) { updateUser(action) .onSuccess { value ->

    // Do something cool! }.onFailure { // Ops, something went wrong… } }
  64. ViewModel.kt fun dispatch(action: Action) { updateUser(action) .onSuccess { value ->

    // Do something cool! }.onFailure { error -> // Ops, something went wrong… } }
  65. • A strategy for handling errors in a functional way

    • Design for errors: unhappy paths are requirements too • Do not use Exception handling for Control Flow. “Do or do not, there is no try”
  66. • A strategy for handling errors in a functional way

    • Design for errors: unhappy paths are requirements too • Do not use Exception handling for Control Flow. “Do or do not, there is no try”
  67. • A strategy for handling errors in a functional way

    • Design for errors: unhappy paths are requirements too • Do not use Exception handling for Control Flow “Do or do not, there is no try”
  68. • A strategy for handling errors in a functional way

    • Design for errors: unhappy paths are requirements too • Do not use Exception handling for Control Flow “Do or do not, there is no try.”