How to enforce relationship between Kotlin classes

Viewed 150

Im new to Kotlin and investigating what is/isnt possible

I have a use case as follows:-

As a technical exercise I am attempting to model remote API requests and responses, and enforce relationships between them

My goal is to be able to declare the relationship between Requests and Responses in a clear and succinct way at the top of a Class. This will 1). document the API calls made by this Class, 2). Enforce the relationship so that Request1 can only produce Response1

Pseudo code:-

Requests {
  Request1 -> Response1   
  Request2 -> Response2
  ...   
  RequestN -> ResponseN
}

I have defined two interfaces Request & Response and employ them as follows:-

interface Request {
    fun <T> response(data : T): Lazy<Response>
}

interface Response

data class Request1(val request: String) : Request {
    data class Response1(val output: String) : Response
    override fun <T> response(data: T): Lazy<Response> {
        return lazy { Response1(data as String) }
    }
}

data class Request2(val request: Long) : Request {
    data class Response2(val output: Double) : Response
    override fun <T> response(data: T): Lazy<Response> {
        return lazy { Response2(data as Double) }
    }
}

I have a Controller class that makes the API calls as follows:-

class Controller {
    fun call(request: Request): Lazy<Response> {
        return when (request) {
            is Request1 -> request.response("Testing Data")
            is Request2 -> request.response(Math.PI)
            else -> TODO()
        }
    }
}

Using the above data classes I can enforce that Request1 is linked to only Response1 and also specify the response data type wrapped by each Response.

Although the above classes provide the functionality and adhere to these rules, they are verbose.

Is there a more succinct approach I could employ to obtain the desired result.

The reason I require this is I am looking for "Self Documenting" code, where a developer can view the definition of Request/Response pairs and association rules and clearly see what is intended.

For example: A developer looking at the final Request definitions can clearly see that Response1 with be generated by Request1. I also want to enforce that Response1 can only ever be produced from Request1.

My example above is simplified, as in "The Real World" the data wrapped by each Response will be sourced from the actual API request call, I have illustrated with "Hard Coded".

I would much rather define Request1 and Response1 on a single line if possible.

UPDATE

I have refactored my original classes as follows:-

interface Request<ResponseData> {
    fun response(data: ResponseData): Lazy<Response>
}

interface Response

sealed class Requests<T> : Request<T> {
    data class Request1(val request: String) : Requests<String>() {
        inner class Response1(val output: String) : Response

        override fun  response(data: String): Lazy<Response> {
            return lazy { Response1(data) }
        }
    }

    data class Request2(val request: Long) : Requests<Double>() {
        inner class Response2(val output: Double) : Response

        override fun response(data: Double): Lazy<Response> {
            return lazy { Response2(data) }
        }
    }
}


class Controller {
    fun <T> call(request: Request<T>): Lazy<Response> {
        return when (request) {
            is Requests.Request1 -> request.response("Testing Data")
            is Requests.Request2 -> request.response(Math.PI)
            else -> TODO()
        }
    }
}

While this version of my code has many benefits from the original, one feature I am still not happy with is that each Request/Response declaration is still quite verbose, e.g. it requires 5 lines of code. Is there an approach I can employ to make each Request/Response pair declaration more succinct?, e.g. take up fewer lines of code.

UPDATE II

Im attempting to refactor my sealed class above so that the overridden function response is defined in the outer sealed class.

interface Request<ResponseData> {
    fun response(data: ResponseData): Lazy<Response>
}

interface Response

sealed class Requests<T> : Request<T> {
    data class Request1(val request: String) : Requests<String>() {
        inner class Response1(val output: String) : Response
    }

    data class Request2(val request: Long) : Requests<Double>() {
        inner class Response2(val output: Double) : Response
    }

    override fun response(data: T): Lazy<Response> {
       return lazy { // What implementation goes here??? // }
    }
}

Is this approach possible? How do I refer to the individual concrete ResponseN classes in the outer sealed class?

4 Answers

Maybe your example is simplified from what you're actually doing, but I don't see the purpose of the Response interface, or the need for separate Request implementations to achieve what your code does:

data class Request<T>(val request: String, val responseType: KClass<out T>) {
    fun response(data : T) = lazy { data }
}

class Controller {
    fun <T: Any> call(request: Request<T>): Lazy<T> {
        @Suppress("UNCHECKED_CAST")
        return when (request.responseType) {
            String::class -> request.response("Testing Data" as T)
            Double::class -> request.response(Math.PI as T)
            else -> TODO()
        }
    }
}

It's kind of an odd use of Lazy though, since you are wrapping a pre-computed value.

My goal is to be able to declare the relationship between Requests and Responses in a clear and succinct way at the top of a Class. This will 1). document the API calls made by this Class, 2). Enforce the relationship so that Request1 can only produce Response1

A great way to enforce the relationships is to separate the interface and implementation levels. Currently you have your interface defined as

interface Request {
    fun <T> response(data : T): Lazy<Response>
}

And it does not tell you that the response can vary. It's high level and then you define actual relations in your implementation.

I suggest to decouple relations and the implementation by moving the relations to the interface level.

Here is my suggestion. Forgive me if something does not compile, I'm writing the code from my head, I want to communicate the design ideas and you may have to change some pseudocode.

Let's start with the interface:

interface Response
interface Request // I see that you are using primitive types for requests, so you don't need the interface. But in a real world scenario your requests will probably be more complex than primitive types and then it will make sense to wrap them in this interface. It also makes the code easier to understand - a string can be anything, while a Request is definitely a request.

// This is an interface that actually performs a request, so makes sense to name it in an actionable way
interface Requester<T, M> {
    fun <in T: Request, out M: Response> request(data : T): Lazy<M>
}

This declaration tells you that there are different kinds of requests and responses and that there are some relations, but do not say what relations are yet.

Then I would declare the responses and requests implementations in a separate place to keep this code short and to the point

class Request1(val input: String) : Request
class Request2(val input: Double) : Request

class Response1(val output: String) : Response
class Response2(val output: Double) : Response

Then you declare the actual relations

interface Requester1: Requester<Request1, Response1>
interface Requester2: Requester<Request2, Response2>

At this point you have a file that clearly communicates the relation without any implementation details.

This is you final interface code, that solves your request for 1). document the API calls made by this Class, 2). Enforce the relationship so that Request1 can only produce Response1 ⬇️

interface Response
interface Request

interface Requester {
    fun <in T: Request, out M: Response> request(data : T): Lazy<M>
}

interface Requester1: Requester<Request1, Response1>
interface Requester2: Requester<Request2, Response2>

Then you can do the implementation in a separate place to keep the interface clean and easy to understand.

sealed class Requests {
    data class RequesterImpl1(val request: String) : Requests, Requester1 {
        override fun request(data: Request1): Lazy<Response1> {
            return lazy { Response1(data) }
        }
    }

    data class RequesterImpl2(val request: Long) : Requests, Requester2 {
         override fun request(data: Double2): Lazy<Response2> {
            return lazy { Response2(data) }
        }
    }
}

Another approach:

data class Box<T, V>(val req: T, val rsp: V)

interface Interaction<RequestT, ResponseT> {
    val req: RequestT
    fun exec(): Box<RequestT, ResponseT>
}

sealed class Interactions<RequestT, ResponseT> : Interaction<RequestT, ResponseT> {
    class Interaction1(override val req: String) : Interaction<String, String> {
        override fun exec() = Box(req, "by")
    }

    class Interaction2(override val req: Long) : Interaction<Long, Double> {
        override fun exec() = Box(req, 1.0)
    }
}

fun main() {
    val interaction1 = Interactions.Interaction1("hi")
    val interaction2 = Interactions.Interaction2(42)

    println(interaction1.exec())   // Box(req=hi, rsp=by)
    println(interaction2.exec())   // Box(req=42, rsp=1.0)
}



This is the current design I am using

fun doNothing(): Unit = Unit

interface Interaction<Input, Output> {

    interface Response<Output> : Interaction<Unit, Output> {
        val output: Output
    }

    interface Request<Input, Output> : Interaction<Input, Output> {
        val input: Input
        fun react(output: Output): Response<Output>
    }
}

sealed class Interactions<I, O> : Interaction<I, O> {

    data class RequestOne(override val input: String) : Interaction.Request<String, Long> {
        internal data class ResponseOne(override val output: Long) : Interaction.Response<Long>

        override fun react(output: Long): Interaction.Response<Long> = ResponseOne(output)
    }

    data class RequestTwo(override val input: CustomInput) : Interaction.Request<CustomInput, CustomOutput> {
        internal data class ResponseTwo(override val output: CustomOutput) : Interaction.Response<CustomOutput>

        override fun react(output: CustomOutput): Interaction.Response<CustomOutput> = ResponseTwo(output)
    }

    data class RequestThree(override val input: Unit = doNothing()) : Interaction.Request<Unit, CustomOutputTwo> {
        internal data class ResponseThree(override val output: CustomOutputTwo) : Interaction.Response<CustomOutputTwo>

        override fun react(output: CustomOutputTwo): Interaction.Response<CustomOutputTwo> = ResponseThree(output)
    }

    data class RequestFour(override val input: Unit = doNothing()) : Interaction.Request<Unit, Unit> {
        internal data class ResponseFour(override val output: Unit = doNothing()) : Interaction.Response<Unit>

        override fun react(output: Unit): Interaction.Response<Unit> = ResponseFour()
    }

    data class RequestFive(override val input: CustomInputTwo) : Interaction.Request<CustomInputTwo, Unit> {
        internal data class ResponseFive(override val output: Unit = doNothing()) : Interaction.Response<Unit>

        override fun react(output: Unit): Interaction.Response<Unit> = ResponseFive()
    }
}

I believe this approach enforces the relationships I require between individual Requests and their associated Response types.

The features of this design I would like to improve on is the use of Unit when defining the Response interface.

Also I cannot see a way to improve on the sealed class Interactions<I, O> : Interaction<I, O> {...}, as I never use the Generic I & O

I would also like to be able to define a single fun react(output: Output): Response<Output> within the parent sealed class Interactions instead of having to implement this function in each inner Action data class, however I do not think that is possible.

Related