I have a challenge for you guys: I have to create...
# announcements
m
I have a challenge for you guys: I have to create a domain model for a PaymentMethod. From the API I receive 2 values for each payment method: • id • transaction cost Let's say I have 3 different types of payment methods: • PayPal (id=1) • CreditCard (id=2) • Cash (id=3) I want to have subclasses for each payment method. The ID that is received from the API should be mapped to an instance of the matching subclass. I want to have subclasses because each payment method has some static data connected to it, that is defined client-sided (e.g. name, etc). If API returns an unknown ID, it can be ignored. Ultimately I would like to have a function in companion object of PaymentMethod:
Copy code
sealed class PaymentMethod {
  abstract val transactionCost: BigDecimal
  
  data class PayPal(...) : PaymentMethod
  data class CreditCard(...) : PaymentMethod
  data class Cash(...) : PaymentMethod

  companion object { 
    fun create(id: Long, transactionCost: BigDecimal) : PaymentMethod {
      // TODO
    }
  }
}
Also I want to have a single source of truth for defining which ID belongs to which subclass (only define it once). Would like to hear some tips on how to implement this. If you think there are better approaches (except for modifying API) than what I want, feel free to suggest.
j
You could use an
enum class
to own the mapping, and if you need reverse lookup there are some tidy ideas here: https://stackoverflow.com/questions/37794850/effective-enums-in-kotlin-with-reverse-lookup Might depend whether there are other fields on the different types of transactions to take care of.
m
You could do some reflection trickery 😛 Not very pretty though. This is working:
Copy code
sealed class PaymentMethod(val id: Long) {

    abstract val transactionCost: BigDecimal
    
    private interface PaymentMethodCompanion {
        val id: Long
        fun create(transactionCost: BigDecimal): PaymentMethod
    }

    data class PayPal(override val transactionCost: BigDecimal) : PaymentMethod(id) {
        companion object : PaymentMethodCompanion {
            override val id: Long = 1
            override fun create(transactionCost: BigDecimal) = PayPal(transactionCost)
        }
    }

    data class CreditCard(override val transactionCost: BigDecimal) : PaymentMethod(id) {
        companion object : PaymentMethodCompanion {
            override val id: Long = 2
            override fun create(transactionCost: BigDecimal) = CreditCard(transactionCost)
        }
    }

    data class Cash(override val transactionCost: BigDecimal) : PaymentMethod(id) {
        companion object : PaymentMethodCompanion {
            override val id: Long = 3
            override fun create(transactionCost: BigDecimal) = Cash(transactionCost)
        }
    }

    companion object {
        fun create(id: Long, transactionCost: BigDecimal): PaymentMethod {
            return PaymentMethod::class.sealedSubclasses
                .mapNotNull { it.companionObjectInstance as PaymentMethodCompanion? }
                .find { it.id == id }
                ?.create(transactionCost)
                ?: throw NoSuchElementException("No payment method with id $id")
        }
    }
}
1
But the easiest is probably to have a
PaymentMethodType
as an enum besides the
PaymentMethod
class. Something like this:
Copy code
enum class PaymentMethodType(val id: Long) {
    PayPal(1),
    CreditCard(2),
    Cash(3)
}

data class PaymentMethod(val type: PaymentMethodType, val transactionCost: BigDecimal) {
    companion object {
        fun create(id: Long, transactionCost: BigDecimal): PaymentMethod {
            val type = PaymentMethodType.values().find { it.id == id }
                ?: throw NoSuchElementException("No payment method type with id $id")

            return PaymentMethod(type, transactionCost)
        }
    }
}
m
The reflection option with companion object interfaces @marstran made, seems like the best option to me.
you don't really need to give it the create function though
Copy code
sealed class PaymentMethod(val id: Long) {
    abstract val transactionCost: BigDecimal
    private interface PaymentMethodCompanion {
        val id: Long
    }

    data class PayPal(override val transactionCost: BigDecimal) : PaymentMethod(id) {
        companion object : PaymentMethodCompanion {
            override val id: Long = 1
        }
    }

    data class CreditCard(override val transactionCost: BigDecimal) : PaymentMethod(id) {
        companion object : PaymentMethodCompanion {
            override val id: Long = 2
        }
    }

    data class Cash(override val transactionCost: BigDecimal) : PaymentMethod(id) {
        companion object : PaymentMethodCompanion {
            override val id: Long = 3
        }
    }

    companion object {
        fun create(id: Long, transactionCost: BigDecimal): PaymentMethod? {
            return PaymentMethod::class.sealedSubclasses
                .find { (it.companionObjectInstance as PaymentMethodCompanion).id == id }
                ?.primaryConstructor
                ?.call(transactionCost)
        }
    }
}
m
But then all the subclasses has to have the same constructor, always.
m
That is true, might have looked too specifically at this situation
either way, it might look clunky, but the companion object interface method has my preference because the companion object of a sealed subclass is all static data, just like an enum class instance would have. They can be made functionally indentical, which is exactly what you want. Because sealed subclasses have the same guarantee for instances as an enum, you can model them in the same way.
m
It doesn't recognize
companionObjectInstance