Realizacja obserwowanych właściwości, które również mogą сериализоваться w Kotlin

0

Pytanie

Próbuję utworzyć klasę, w której określone wartości można obserwować, ale również można serializować.

To oczywiście działa, i szeregowania działa, ale jest bardzo stereotypowy-trzeba dodać setter dla każdego pola i ręcznie wywoływać change(...) wewnątrz każdego seter:

interface Observable {

    fun change(message: String) {
        println("changing $message")
    }
}

@Serializable
class BlahVO : Observable {

    var value2: String = ""
        set(value) {
            field = value
            change("value2")
        }

    fun toJson(): String {
        return Json.encodeToString(serializer(), this)
    }
}

println(BlahVO().apply { value2 = "test2" }) poprawnie wyświetla

changing value2
{"value2":"test2"}

Próbowałem wyobrazić delegatów:

interface Observable {

    fun change(message: String) {
        println("changing $message")
    }

    
    @Suppress("ClassName")
    class default<T>(defaultValue: T) {

        private var value: T = defaultValue

        operator fun getValue(observable: Observable, property: KProperty<*>): T {
            return value
        }

        operator fun setValue(observable: Observable, property: KProperty<*>, value: T) {
            this.value = value
            observable.change(property.name)
        }

    }

}

@Serializable
class BlahVO : Observable {

    var value1: String by Observable.default("value1")

    fun toJson(): String {
        return Json.encodeToString(serializer(), this)
    }
}

println(BlahVO().apply { value1 = "test1" }) prawidłowo rozpoczyna wykrywanie zmian, ale nie jest serializowany:

changing value1
{}

Jeśli przejdę od obserwowanego do ReadWriteProperty,

interface Observable {

    fun change(message: String) {
        println("changing $message")
    }

    fun <T> look(defaultValue: T): ReadWriteProperty<Observable, T> {
        return OP(defaultValue, this)
    }

    class OP<T>(defaultValue: T, val observable: Observable) : ObservableProperty<T>(defaultValue) {
        override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
            super.setValue(thisRef, property, value)
            observable.change("blah!")
        }
    }
}

@Serializable
class BlahVO : Observable {

    var value3: String by this.look("value3")

    fun toJson(): String {
        return Json.encodeToString(serializer(), this)
    }
}

wynik jest ten sam:

changing blah!
{}

Podobnie dla delegatów.

var value4: String by Delegates.vetoable("value4", {
        property: KProperty<*>, oldstring: String, newString: String ->
    this.change(property.name)
    true
})

wyjścia:

changing value4
{}

Delegaci, jak się wydaje, po prostu nie działają z сериализацией Kotlin

Jakie jeszcze są możliwości monitorowania zmian właściwości bez naruszenia jego serializacji, które również będą działać na innych platformach (KotlinJS, KotlinJVM, Android,...)?

1

Najlepsza odpowiedź

2

Szeregowania i podczas deserializacji delegatów Kotlin nie jest obsługiwany kotlinx.serialization na ten moment.
Na GitHub jest otwarta problem #1578, dotyczące tej funkcji.

Zgodnie z tym problemem można utworzyć tymczasowy obiekt transmisji danych, który będzie serializowany zamiast oryginalnego obiektu. Również można napisać własny serializacja do obsługi serializacji delegatów Kotlin, że, podobno, jeszcze bardziej banalna, a następnie napisz do użytkownika zarówno gettery jak i setery, jak zaproponowano w sprawie.


Obiekt Transmisji Danych

Porównując oryginalny obiekt z prostym obiektem transmisji danych bez delegatów, można użyć mechanizmy serializacji domyślnie. To również ma przyjemny efekt uboczny do czyszczenia klas modeli danych od adnotacji, specyficznych dla platformy, takich jak @Serializable.

class DataModel {
    var observedProperty: String by Delegates.observable("initial") { property, before, after ->
        println("""Hey, I changed "${property.name}" from "$before" to "$after"!""")
    }

    fun toJson(): String {
        return Json.encodeToString(serializer(), this.toDto())
    }
}

fun DataModel.toDto() = DataTransferObject(observedProperty)

@Serializable
class DataTransferObject(val observedProperty: String)

fun main() {
    val data = DataModel()
    println(data.toJson())
    data.observedProperty = "changed"
    println(data.toJson())
}

To daje następujący wynik:

{"observedProperty":"initial"}
Hey, I changed "observedProperty" from "initial" to "changed"!
{"observedProperty":"changed"}

Typ danych zdefiniowany przez użytkownika

Jeśli można zmienić typ danych, możesz napisać klasa migracji, który w sposób przejrzysty serializowany (de). Coś takiego jak poniżej może się udać.

@Serializable
class ClassWithMonitoredString(val monitoredProperty: MonitoredString) {
    fun toJson(): String {
        return Json.encodeToString(serializer(), this)
    }
}

fun main() {
    val monitoredString = obs("obsDefault") { before, after ->
        println("""I changed from "$before" to "$after"!""")
    }
    
    val data = ClassWithMonitoredString(monitoredString)
    println(data.toJson())
    data.monitoredProperty.value = "obsChanged"
    println(data.toJson())
}

Co daje następujący wynik:

{"monitoredProperty":"obsDefault"}
I changed from "obsDefault" to "obsChanged"!
{"monitoredProperty":"obsChanged"}

Jednak tracisz informacje o tym, jaka właściwość została zmieniona, ponieważ nie masz łatwego dostępu do nazwy pola. Ponadto, będziesz musiał zmienić swoje struktury danych, jak wspomniano powyżej, i to może być niepożądane lub nawet niemożliwe. Ponadto, podczas gdy to działa tylko dla wierszy, choć można było zrobić go bardziej uniwersalnym. Ponadto, na początku wymaga wiele standardowych szablonów. Jednak na miejscu połączeń wystarczy wpisać rzeczywistą wartość w wyzwanie, aby obs. Użyłem następującego szablonu, aby zmusić go do pracy.

typealias OnChange = (before: String, after: String) -> Unit

@Serializable(with = MonitoredStringSerializer::class)
class MonitoredString(initialValue: String, var onChange: OnChange?) {
    var value: String = initialValue
        set(value) {
            onChange?.invoke(field, value)

            field = value
        }

}

fun obs(value: String, onChange: OnChange? = null) = MonitoredString(value, onChange)

object MonitoredStringSerializer : KSerializer<MonitoredString> {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("MonitoredString", PrimitiveKind.STRING)

    override fun serialize(encoder: Encoder, value: MonitoredString) {
        encoder.encodeString(value.value)
    }

    override fun deserialize(decoder: Decoder): MonitoredString {
        return MonitoredString(decoder.decodeString(), null)
    }
}
2021-11-24 18:19:41

Obecnie używam podobnego podejścia, ale wydaje mi się, że to mogło by być lepiej. Zrobiłem jeszcze jeden krok do przodu, tworząc metodę monitoredString, która zwraca ciąg znaków MonitoredString, i ponieważ funkcja ma do tego dostęp, nie muszę wysyłać onChange, mogę po prostu połączyć go z onChange z tego. Wadą posiadania obserwowanego klasy "stan", a następnie klasy transmisji danych, który może być serializowany, jest powielanie pól modelu. Wydaje się, że jedyne dobre rozwiązanie, które dociera do tego, co chcę zrobić, to opisywanie za pomocą @coś, a następnie wygenerować szablon za pomocą KSP.
Jan Vladimir Mostert

W innych językach

Ta strona jest w innych językach

Русский
..................................................................................................................
Italiano
..................................................................................................................
Română
..................................................................................................................
한국어
..................................................................................................................
हिन्दी
..................................................................................................................
Français
..................................................................................................................
Türk
..................................................................................................................
Česk
..................................................................................................................
Português
..................................................................................................................
ไทย
..................................................................................................................
中文
..................................................................................................................
Español
..................................................................................................................
Slovenský
..................................................................................................................