Kotlin reify is not getting the actual instance type

43 views Asked by At

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?

1

There are 1 answers

0
Sweeper On BEST ANSWER

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 {
        // ...
    }
}