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>
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.