Sunday, May 28, 2023

Chain of Responsibility Design Pattern with Kotlin

 Chain Of Responsibility

Chain of responsibility I'm a horrible software architect, and I don't like to speak with people. Hence, while sitting at home, I wrote a small web application. If a developer has a question, he shouldn't approach me directly, oh no. He'll need to send me a proper request through this system, and I shall answer him only if I deem this request worthy. A filter chain is a very common concept in web servers. Usually, when a request reaches to you, it's expected that: Its parameters are already validated The user is already authenticated, if possible User roles and permissions are known, and the user is authorized to perform an action So, the code I initially wrote looked something like this:

fun handleRequest(r: Request) {

    // Validate

    if (r.email.isEmpty() || r.question.isEmpty()) {

return

    }

     // Authenticate

    // Make sure that you know whos is this user

    if (r.email.isKnownEmail()) {

        return

    }

     // Authorize

    // Requests from juniors are automatically ignored by architects

    if (r.email.isJuniorDeveloper()) {

        return

    }

 println("I don't know. Did you check StackOverflow?")

A bit messy, but it works. Then I noticed that some developers decide they can send me two questions at once. Gotta add some more logic to this function. But wait, I'm an architect, after all. Isn't there a better way to delegate this? This time, we won't learn new Kotlin tricks, but use those that we already learned. We could start with implementing an interface such as this one:

interface Handler {

    fun handle(request: Request): Response

We never discussed what my response to one of the developers looked like. That's because I keep my chain of responsibility so long and complex that usually, they tend to solve problems by themselves. I've never had to answer one of them, quite frankly. But at least we know what their requests look like: 

data class Request(val email: String, val question: String)

data class Response(val answer: String) 

Then we could do it the Java way, and start implementing each piece of logic inside its own handler: 

 class BasicValidationHandler(private val next: Handler) : Handler {

    override fun handle(request: Request): Response {

        if (request.email.isEmpty() || request.question.isEmpty()) {

            throw IllegalArgumentException()

        }

 return next.handle(request)

    }

}

Other filters would look very similar to this one. We can compose them in any order we want: 

val req = Request("developer@company.com", "Who broke my build?")

 val chain = AuthenticationHandler(

                BasicValidationHandler(

                    FinalResponseHandler()))

 val res = chain.handle(req)

 println(res)  

But I won't even ask you the rhetoric question this time about better ways. Of course there's a better way, we're in the Kotlin world now. And we've seen the usage of functions in the previous section. So, we'll define a function for that task: 

typealias Handler = (request: Request) -> Response 

Why have a separate class and interface for something that receives a request and returns a response in a nutshell: 

val authentication = fun(next: Handler) =

    fun(request: Request): Response {

        if (!request.email.isKnownEmail()) {

            throw IllegalArgumentException()

        }

return next(request)

    } 

Here, authentication is a function that literally receives a function and returns a function. Again, we can compose those functions: 

val req = Request("developer@company.com", "Why do we need Software Architects?")

 val chain = basicValidation(authentication(finalResponse()))

 val res = chain(req)

 println(res) 

It's up to you which method you choose. Using interfaces is more explicit, and would better suit you if you're creating your own library or framework that others may want to extend. Using functions is more concise, and if you just want to split your code in a more manageable way, it may be the better choice.

Code Example :

package designpatterns.chainOfResponsibility



typealias Handler = (request: Request) -> Response

fun main(args: Array<String>) {

val req = Request("developer@company.com", "Why do we need Software
                                   Architects?")

val chain = basicValidation(authentication(finalResponse()))

val res = chain(req)

println(res)
}


val basicValidation = fun(next: Handler): Handler {
return fun(request: Request): Response {
if (request.email.isEmpty() || request.question.isEmpty()) {
throw IllegalArgumentException()
}
return next(request)
}
}

val authentication = fun(next: Handler) =
fun(request: Request): Response {
if (!request.email.isKnownEmail()) {
throw IllegalArgumentException()
}
return next(request)
}

val finalResponse = fun() = fun(_: Request) = Response("Try git-blame.")


private fun String.isKnownEmail(): Boolean {
return true
}


data class Request(val email: String, val question: String)

data class Response(val answer: String)


Output :
Response(answer=Try git-blame.)


package designpatterns.not


fun handleRequest(r: Request) {
// Validate
if (r.email.isEmpty() || r.question.isEmpty()) {
return
}

// Authenticate
// Make sure that you know whos is this user
if (r.email.isKnownEmail()) {
return
}

// Authorize
// Requests from juniors are automatically ignored by architects
if (r.email.isJuniorDeveloper()) {
return
}

println("I don't know. Did you check StackOverflow?")
}

private fun String.isJuniorDeveloper(): Boolean {
return false
}

private fun String.isKnownEmail(): Boolean {
return true
}


data class Request(val email: String, val question: String)
package designpatterns.chainOfResponsibility.java


fun main(args: Array<String>) {
val req = Request("developer@company.com",
"Why do we need Software Architects?")

val chain = AuthenticationHandler(
BasicValidationHandler(
FinalResponseHandler()))

val res = chain.handle(req)

println(res)
}


class BasicValidationHandler(private val next: Handler) : Handler {
override fun handle(request: Request): Response {
if (request.email.isEmpty() || request.question.isEmpty()) {
throw IllegalArgumentException()
}

return next.handle(request)
}
}

class AuthenticationHandler(private val next: Handler) : Handler {
override fun handle(request: Request): Response {
if (!request.email.isKnownEmail()) {
throw IllegalArgumentException()
}

return next.handle(request)
}
}

class FinalResponseHandler : Handler {
override fun handle(request: Request) = Response("Read documentation.")
}

private fun String.isJuniorDeveloper(): Boolean {
return true
}

private fun String.isKnownEmail(): Boolean {
return true
}

interface Handler {
fun handle(request: Request): Response
}


data class Request(val email: String, val question: String)

data class Response(val answer: String)

Output :

Response(answer=Read documentation.)


No comments: