Search code examples
kotlingenericsreflectiondomain-driven-designreify

Kotlin reify is not getting the actual instance type


I'm trying to implement a ValueObject base class using Kotlin and DDD. As any value object must consider all attributes in the equals method, I decided to implement a single method using reflection, so no subclass will need to implement it.

My ValueObject class is as follows:

abstract class ValueObject {
    override fun equals(other: Any?) = when {
        this === other -> true
        other !is ValueObject -> false
        else -> deepEquals(other)
    }

    inline fun <reified T : Any> deepEquals(other: T): Boolean {
        val fields = T::class.java.declaredFields

        println(T::class)

        for (field in fields) {
            field.isAccessible = true
            val thisValue = field.get(this)
            val otherValue = field.get(other)
            if (thisValue != otherValue && (thisValue == null || !thisValue.equals(otherValue))) {
                return false
            }
        }
        return true
    }

    override fun hashCode() = hashFromReflection()

    private fun hashFromReflection(): Int {
        val fields = this::class.java.declaredFields
        var result = 17

        for (field in fields) {
            field.isAccessible = true
            val value = field.get(this)
            result = 31 * result + (value?.hashCode() ?: 0)
        }

        return result
    }

    protected abstract fun validate() : Notification
}

I implemented the following code to test:

class PhoneNumber (val code: String, val number: String) : ValueObject() {
    override fun validate(): Notification {
        return Notification()
    }
}

fun main() {
    val phoneNumber = PhoneNumber("33", "w")
    val other = PhoneNumber("3", "w")
    println(phoneNumber == other)
    println(phoneNumber.deepEquals(other))
}

However, the result is weird. If I call deepEquals directly, it works fine. But if I use == operator, it considers T as ValueObject (not PhoneNumber), finds no declaredFields, and gives me a wrong result:

class br.all.domain.common.ValueObject
true
class br.all.domain.common.PhoneNumber
false

What I'm doing wrong?


Solution

  • You seem to be overcomplicating this with reified. You can just get the Class of objects by doing this::class.java or other::class.java. There is no need for reified.

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        // note that you should also check this::class.java != other::class.java!
        if (other == null || this::class.java != other::class.java) return false
    
        val fields = other::class.java.declaredFields
    
        for (field in fields) {
            field.isAccessible = true
            val thisValue = field.get(this)
            val otherValue = field.get(other)
            if (thisValue != otherValue && (thisValue == null || !thisValue.equals(otherValue))) {
                return false
            }
        }
        return true
    }
    

    The reason why your code doesn't work is because there is no type information about the specific subclass of ValueObject when you call deepEquals(other) in equals. The reified type parameter is simply passed as deepEquals<ValueObject>(other).

    Also, you seem to be reinventing data classes. Consider doing this with data classes instead.

    abstract class ValueObject {
        protected abstract fun validate(): Notification
    }
    
    data class PhoneNumber (val code: String, val number: String) : ValueObject() {
        override fun validate(): Notification {
            // ...
        }
    }