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) ????
}
First,
IPaymentStrategy<IPaymentModel>is not a valid type.IPaymentStrategy<T>requiresTto be aIPaymentService, not anIPaymentModel. If you want this to be valid, thenIPaymentStrategyshould be declared like this: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 infindService. You cannot directly check its type parameter, because of type erasure. You could useappliesTo, which would requirefindServiceto suspend, becauseappliesTosuspends. Alternatively, add some other property toIPaymentServicethat you can check.With that out of the way, you can now write
PaymentAPI. The type ofpaymentServicesshould beList<IPaymentService<*>>so that you can have a heterogeneous list of services.Now
PaymentAPI(gateways)would compile.For
findService, you can implement it like this: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 atfindService(model)?.authorizeFunds(model).The strategy pattern might well be over-complicating this, but I cannot say for sure without looking at the bigger picture.