Search code examples
javakotlingenericsdesign-patternsstrategy-pattern

Type mismatch in Kotlin when using strategy pattern with generics


I need to implement a backend system which accepts multiple payment methods, and then process them according to their type.

I used Strategy Pattern, but I can't seem to make it work when I initialize the service because of Type Mismatch. I'm probably missing or misunderstanding something with the Generics in kotlin.

 val gateways =
        listOf(
            CreditGateway(),
            PayPalGateway()
        )
 val api = PaymentAPI(gateways) <-- error: Type mismatch 
 api.authorizeFunds(PayPalModel(...))

 Required:
 List<IPaymentService<IPaymentModel>>
 Found:
 List<IPaymentService<{CreditModel & PayPalModel}>>

The code implementation is:

interface IPaymentModel

class CreditModel : IPaymentModel
class PayPalModel : IPaymentModel

interface IPaymentService<in T> where T: IPaymentModel {
    suspend fun authorizeFunds(model: T)
    suspend fun appliesTo(type: IPaymentModel): Boolean
}

class CreditGateway : IPaymentService<CreditModel> {
    override suspend fun authorizeFunds(model: CreditModel) {
      /// implementation
    }
    override suspend fun appliesTo(type: IPaymentModel): Boolean {
        return type is CreditModel
    }
}

class PayPalGateway : IPaymentService<PayPalModel> {
    override suspend fun authorizeFunds(model: PayPalModel) {
      /// implementation
    }
    override suspend fun appliesTo(type: IPaymentModel): Boolean {
        return type is PayPalModel
    }
}

interface IPaymentStrategy<T: IPaymentService<IPaymentModel>> {
    suspend fun <T: IPaymentModel> authorizeFunds(model: T)
}

class PaymentAPI(
    private val paymentServices: List<IPaymentService<IPaymentModel>>
): IPaymentStrategy<IPaymentModel> {

    override suspend fun <T : IPaymentModel> authorizeFunds(model: T) {
        findService(model)?.authorizeFunds(model)
    }
    
    private fun <T : IPaymentModel> findService(model: T) ????
}

Solution

  • First, IPaymentStrategy<IPaymentModel> is not a valid type. IPaymentStrategy<T> requires T to be a IPaymentService, not an IPaymentModel. If you want this to be valid, then IPaymentStrategy should be declared like this:

    interface IPaymentStrategy<in T: IPaymentModel> {
        // authorizeFunds should not be generic
        suspend fun authorizeFunds(model: T)
    }
    

    Note that if all the strategies you are going to write are IPaymentStrategy<IPaymentModel>s, it doesn't need to be generic.

    Second, you need to find a way to determine whether a payment service is applicable to a given IPaymentModel, so that you can find a suitable service in findService. You cannot directly check its type parameter, because of type erasure. You could use appliesTo, which would require findService to suspend, because appliesTo suspends. Alternatively, add some other property to IPaymentService that you can check.

    With that out of the way, you can now write PaymentAPI. The type of paymentServices should be List<IPaymentService<*>> so that you can have a heterogeneous list of services.

    class PaymentAPI(
        private val paymentServices: List<IPaymentService<*>>
    ): IPaymentStrategy<IPaymentModel>
    

    Now PaymentAPI(gateways) would compile.

    For findService, you can implement it like this:

    private suspend fun findService(model: IPaymentModel): IPaymentService<IPaymentModel>? {
        for (service in paymentServices) {
            if (service.appliesTo(model)) {
                return service as IPaymentService<IPaymentModel>
            }
        }
        return null
    }
    
    override suspend fun authorizeFunds(model: IPaymentModel) {
        findService(model)?.authorizeFunds(model)
    }
    

    The unchecked cast as IPaymentService<IPaymentModel> is necessary because of type erasure. If you accidentally cast to a payment service that doesn't take that type of model, it would throw an exception at findService(model)?.authorizeFunds(model).

    The strategy pattern might well be over-complicating this, but I cannot say for sure without looking at the bigger picture.