Best way to implement multiple functions for different type arguments without making the number of implementations grow exponentially?

I have this class:

data class BluetoothCommand(
    val characteristic: DeviceUUIDs.Characteristic,
    override val code: Int,
    val commandArgument: CommandArgument<*>,
    val description: String,
    private val successResponse: BluetoothSuccessResponse = BluetoothSuccessResponse.Default,
    val errorResponse: BluetoothErrorResponse = BluetoothErrorResponse.Default,
    val msgSize: Int? = null,
    val hidden: Boolean = false,
    val debug: Boolean = false
) : WithCode {
    constructor(
        characteristic: DeviceUUIDs.Characteristic,
        code: WithCode,
        commandArgument: CommandArgument<*>,
        description: String,
        successResponse: BluetoothSuccessResponse = BluetoothSuccessResponse.Default,
        errorResponse: BluetoothErrorResponse = BluetoothErrorResponse.Default,
        msgSize: Int? = null,
        hidden: Boolean = false,
        debug: Boolean = false
    ) : this(characteristic, code.code, commandArgument, description, successResponse, errorResponse, msgSize, hidden, debug)

    fun successResponse(): BluetoothSuccessResponse {
        return msgSize?.let { size ->
            if (successResponse is BluetoothSuccessResponse.Bytes) {
                BluetoothSuccessResponse.Bytes { msg, args ->
                    if (msg.size != size) errorSnackBar("Malformed message: it has length ${msg.size} bytes, expected $msgSize bytes!")
                    else with(successResponse.bytesBluetoothResponse) { processData(msg, args) }
                }
            } else null
        } ?: successResponse
    }
}

I would like msgSize to be either an Int or a lambda taking an Int and returning a Boolean. Normally, I would simply declare two functions with different signatures, but in this case, I’ve already done that to have code be either an Int or a WithCode object. If I did the same thing again, I would end up with 4 versions of the same constructor. I could create a new object which takes an Int or a (Int) -> Boolean and use that, but I have a big list of BluetoothCommands hard coded and I’m trying to make the process of writing said list as easy as possible: having a bunch of new objects representing a simple Int would be a bit annoying. What can I do?

I don’t have a Kotlin-specific solution, but common OOP solves this kind of problem.

We can either use a builder pattern / chain of calls: BluetoothCommand.builder().code(...).msgSize().build(). Or we can receive only a single type which is the least requiring or is the most generic, so we can easily represent any other type as this one. For example, for me receiving WithCode only to use its code property is unnecessary complication. Let’s receive Int. I’m not sure about the msgSize, because Int and (Int) -> Boolean feels a little incompatible, but if you mean that our lambda is queried for valid msgSize values, then again: (Int) -> Boolean is a common “supertype”, so we can receive this type and if we have an integer already, we can pass it like this: msgSize = value::equals. This approach is pretty similar to the strategy pattern.

3 Likes

Having one variable or parameter taking one value among different types is generally made in modern languages like Kotlin or Swift by Enumeration with associated value.

In Kotlin the principle is wore by worn by a pattern using sealed class

In your situation you want a value holding either an Int or an (Int) -> Boolean

So you can create a type

sealed class MsgType {
    data class Direct(val size: Int):  MsgType()
    data class Code(val lambda: (Int) -> Boolean): MsgType()
}

Now your constructor can expose a param (eg) msgSize: MsgType

And you can take action on msgSize value using this pattern

when (msgSize) {
    is Direct -> do stuff with `size`
    is Code -> do stuff with `lambda(a value)`
}
2 Likes