Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save nodinosaur/4473a8978d460cec4ec2f6731d3c624d to your computer and use it in GitHub Desktop.
Save nodinosaur/4473a8978d460cec4ec2f6731d3c624d to your computer and use it in GitHub Desktop.
data class UserInfo(val id: String)
data class UserInfoDto(var id: String?)
// Open Inheritance
interface IViewState { }
class Idle: IViewState
class Loading(val percent: Int): IViewState
// Closed inheritance:
// Limit possible states and make illegal states unrepresentable
sealed class ViewState: AndThen<ViewState> {
class Idle: ViewState()
class Loading(val percent: Int): ViewState()
class Success(val userInfo: UserInfo): ViewState()
class Error(val message: String): ViewState()
override fun <U> andThen(func: (ViewState) -> AndThen<U>) =
func.invoke(this)
override fun value() = this
}
// Encode behaviour in types
interface AndThen<T> {
fun <U> andThen(func: (T) -> AndThen<U>): AndThen<U>
fun value(): T
}
sealed class Result<T>: AndThen<Result<T>> {
class SuccessWithValue<T>(val result: T): Result<T>()
class Success<T>(): Result<T>()
class Failure<T>(val error: Exception): Result<T>()
override fun <U> andThen(func: (Result<T>) -> AndThen<U>) =
func.invoke(this)
override fun value() = this
}
// Move validation to edges
sealed class UserInBusinessLayer: AndThen<UserInBusinessLayer> {
class ValidUser(val value:UserInfo): UserInBusinessLayer()
class InvalidUser(val value:UserInfoDto): UserInBusinessLayer()
class AbsentUser(val error:String): UserInBusinessLayer()
override fun <U> andThen(func: (UserInBusinessLayer) -> AndThen<U>) =
func.invoke(this)
override fun value() = this
}
// And now everything together
fun main(args: Array<String>) {
val finalResult =
doAction()
.andThen { validate(it) }
.andThen { toUiState(it) }
.value()
System.out.println(printValue(finalResult));
}
// Create response from network
//fun doAction(): Result<UserInfoDto> = Result.Failure(RuntimeException("Fail!"))
//fun doAction(): Result<UserInfoDto> = Result.Success()
fun doAction(): Result<UserInfoDto> = Result.SuccessWithValue(UserInfoDto("George"))
// Make sure the user is valid
fun validate(value: Result<UserInfoDto>): UserInBusinessLayer =
when (value) {
is Result.SuccessWithValue<UserInfoDto> -> {
val id: String? = value.result.id
if (id != null)
UserInBusinessLayer.ValidUser(UserInfo(id))
else
UserInBusinessLayer.InvalidUser(value.result)
}
is Result.Success<*> ->
UserInBusinessLayer.AbsentUser("No user")
is Result.Failure<*> ->
UserInBusinessLayer.AbsentUser(value.error.message!!)
}
// Transform into a UI state
fun toUiState(user: UserInBusinessLayer): ViewState =
when (user) {
is UserInBusinessLayer.ValidUser ->
ViewState.Success(user.value)
is UserInBusinessLayer.InvalidUser ->
ViewState.Error("Invalid User")
is UserInBusinessLayer.AbsentUser ->
ViewState.Error(user.error)
}
// Helper to transform state into display string
fun printValue(viewState: ViewState): String =
when (viewState) {
is ViewState.Idle -> "Idle"
is ViewState.Loading -> "Loading"
is ViewState.Success -> viewState.userInfo.toString()
is ViewState.Error -> viewState.message
}

Behaviour in kotlin gives you a map of execution called Sealed Classes.

Basically a Sealed Class is a way of defining an inheritance scheme that is closed.

Your only allowed a series of inherenting classes from a single type that gives you a lot of power to do things.

The next part of the talk and I'll just go with the example

ok so the first thing is I was talking about data types and the data qualifiers so I have here two different classes that are both of them are two different parts of different layers of the application so you have UserInfo which is on your Business Logic Layer that contains an ID which is of type String and then you have the UserInfoDTO which is the class that you're using in your databases the class that you're using on your network responses which is mutable which is a var and it also has a nullable type for String so this means that there is a point of failure in this UserInfoDTO that you want to deal with and you also - yep

So we'll go back to that in a second. How do we make that single point of failure that point of failure into something that is easy for you to deal with? So just a quick reminder: an Open Inheritance Scheme is just basically you just create an interface or an abstract base class and you have a series of classes that inherit from there, so you just thinking like Command Pattern you thinking Strategy pattern but actually this predates it for like by decades. But that's ok, so the problem with Open Inheritance of course is that is you are not assured that you have covered all possible cases because there could be always somebody that extends your current system and that means that you get no guarantees for free so what Kotlin is giving you is a Closed Inheritance, that means that you are only limited to all the inheritance types that are the defined in this file and none else which means that when the compiler goes to them it knows exactly that there has to be four different ones and that gives you some guarantees that we're going to talk about any second that are really really good for the development.

so the example that are having here right now I call ViewState in what I do as I limit the possible States and making those illegal States unrepresentable so let's think for a second that I have some kind of network request when I just go and fetch the user info with UserInfoDTO and at the other end of the line I have to display that you something information like a big profile picture or something of the sorts and for that we have some kind of intermediate state where we're previously before we even load the user info when we're loading it from the network when I have a success and we have to display the information and we have an error so something went wrong in the network so we're saying explicitly these are the four possible states that our UI can be in. You can think about it like a state machine kind of thing. So you model that in this kind of inheritance scheme which is ok and each one of them contains some related information that is important for our UI display.

Ignore this for a second because.

We can encode the behaviour what I was saying before we have the data classes and we have the Inheritance Scheme and now we have to encode the way that the execution flows in we can do that with some constructs I'm not going to go in depth with but I am going to provide one of them right now which is a simple AndThen, AndThen is an interface that just says I'm single function AndThen to which you provide another function from the current Class or the current Type into another AndThen don't over think about it will see usage in a second.

So we have another different Sealed Class for our network result so let's say for one second that it our network result could be a success with a value so we went into the network and we came back with the User we have a single success which means maybe we got a 403 or something that is it just means that we successfully went to the network we came back with no result or we have a failure and the failure contains the exception and this Result implements this AndThen for itself so that means that whenever somebody calls AndThen we just called the passing function and return ourselves so that means that you can change with ourselves all the time and we have the value which is just a way of exiting that chain when we do that by just returning a current value.

So next time part I was talking about in the previous slide as moving the validation to the edges so we are receiving this User and we want us early as possible to be able to tell if the User is correct or not and rather than having a single validation method or having a way of like the failure point is not exactly what where we want it, what we do is have a data representation of a ValidUser and an InvalidUser so same case we just gave create another Sealed Class of ValidUser, InvalidUser and for a ValidUser to return the correct value in for the InvalidUser the failure point so we know exactly what failed about it.

And yep let's put everything together so in the end we have a single main function in the main function what it does is it just does the action just goes to the network fetches the value. AndThen validates that the User response that it received was correct. AndThen changes it into one of the possible states of the UI which is either Loading, Success and Error in this case would be Success and Error. AndThen finally what we going to do with that final result is just simply print it out on the screen. We just do a representation of like a String for each one of the types.

So, what we do is we do an action we go to the network we say hey he comes back in this case were mocking it with a result with a success with a value with the UserInfoDTO for that is just my name we have the AndThen into the validation and in this case the validation means that we're exploding we are dereferencing this original Result type and this is the first time that we're going to see it you use when which is kind of like pattern matching but not exactly because it allows you only to do if checks against the Type that you're referencing so in the case of a result you just say if it's a success with a value if it's a single success if it is a failure and the compiler is going to check for you that every time you do a when you have to cover all possible types.

If doesn't cover them is just gonna not going to compile at all. is going to give you a problem so why do we do for validation he just simply check if the string received on the UserInfoDTO is valid, if the string is valid we change into a ValidUser if it's not valid then we send an InvalidUser which makes for a function that simply takes one input and it puts a value and there is no state to this there's nothing special happening with just we're just passing data around we're just modifying the data and then passing the data around.

so the next AndThen is once we get ValidUser or InvalidUser would just make that into a Success or a Failure single exit point and finally we're just print the value.

so let me put this Kotlin has a nice compiler online

So we have exactly the same thing no tricks we can file it and it just says hey because the action that I'm taking is a Success with a value with the UserInfoDTO Paco this is going to be correct what happens if I come with an incorrect value?

OK, it is an InvalidUser we're fine with that, what happens if I come with a failure?

Hey, it says InvalidUser this is not what I was expecting so I have a new case in here which means that the failure something went wrong and probably means that instead of an InvalidUser I have an AbsentUser so I just go with this and I just say AbsentUser and because I have the type checking on the fly which I can turn off and I can just compile and I'm going to get the same it's going to tell me that the one expression for the validation for the UI state does not cover all possible cases which means I have to add one extra case in here as going to be AbsentUser and when we get an AbsentUser we're gonna get String an Error which is a String

I just get so whenever I get an Error I just get user.error. run it again he still says InvalidUser of what happened so the compiler hasn't checked for every single case and we have to go back and check AndThen we're doing valuation when it gets to UI state it should come correctly. AndThen I realised that whenever I have a failure and returning an InvalidUser and I'm not expecting an InvalidUser I'm expecting an AbsentUser and for this AbsentUser say we have to pass value.error.message!!

and we have a failure now so you're giving the compiler assurance that your covering all possible cases all possible flow of your execution by the same time it doesn't check for you like your business logic so you still have to do some testing around it.

so back into the presentation quick recap Sealed Classes in Kotlin it's a Closed Inheritance scheme it requires you to dereference the type before you're able to access each one of the fields inside of it and you do that by using when and ifs it allows you to use data classes and implement interfaces

Java8 version that looks similarish but instead of using the whens and the pattern matching it uses something called church encoding it doesn't really matter just passed functions inside of for each one of the possible values inside the inheritance of schemes and it will give you the correct answer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment