Реализация наблюдаемых свойств, которые также могут сериализоваться в Kotlin

0

Вопрос

Я пытаюсь создать класс, в котором определенные значения можно наблюдать, но также можно сериализовать.

Это, очевидно, работает, и сериализация работает, но она очень шаблонна-приходится добавлять сеттер для каждого отдельного поля и вручную вызывать change(...) внутри каждого сеттера:

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" }) правильно выводит

changing value2
{"value2":"test2"}

Я попытался представить делегатов:

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" }) правильно запускает обнаружение изменений, но оно не сериализуется:

changing value1
{}

Если я перейду от наблюдаемого к 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)
    }
}

результат тот же:

changing blah!
{}

Аналогично для делегатов.

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

выходы:

changing value4
{}

Делегаты, похоже, просто не работают с сериализацией Kotlin

Какие еще существуют варианты наблюдения за изменениями свойства без нарушения его сериализации, которые также будут работать на других платформах (KotlinJS, KotlinJVM, Android,...)?

1

Лучший ответ

2

Сериализация и десериализация делегатов Kotlin не поддерживается kotlinx.serialization на данный момент.
На GitHub есть открытая проблема #1578, касающаяся этой функции.

В соответствии с проблемой вы можете создать промежуточный объект передачи данных, который будет сериализован вместо исходного объекта. Также вы могли бы написать пользовательский сериализатор для поддержки сериализации делегатов Kotlin, что, по-видимому, еще более шаблонно, а затем написать пользовательские геттеры и сеттеры, как предложено в вопросе.


Объект Передачи Данных

Сопоставляя исходный объект с простым объектом передачи данных без делегатов, вы можете использовать механизмы сериализации по умолчанию. Это также имеет приятный побочный эффект для очистки классов моделей данных от аннотаций, специфичных для платформы, таких как @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())
}

Это дает следующий результат:

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

Пользовательский тип данных

Если можно изменить тип данных, вы можете написать класс переноса, который прозрачно сериализуется (de). Что-то вроде следующего может сработать.

@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())
}

Что дает следующий результат:

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

Однако вы теряете информацию о том, какое свойство было изменено, так как у вас нет легкого доступа к имени поля. Кроме того, вам придется изменить свои структуры данных, как упоминалось выше, и это может быть нежелательно или даже невозможно. Кроме того, пока это работает только для строк, хотя можно было бы сделать его более универсальным. Кроме того, для начала требуется много стандартных шаблонов. Однако на сайте вызовов вам просто нужно ввести фактическое значение в вызов, чтобы obs. Я использовал следующий шаблон, чтобы заставить его работать.

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

В настоящее время я придерживаюсь аналогичного подхода, но мне кажется, что это могло бы быть лучше. Я сделал еще один шаг вперед, создав метод monitoredString, который возвращает строку MonitoredString, и поскольку функция имеет к этому доступ, мне не нужно передавать onChange, я могу просто связать его с onChange из этого. Недостатком наличия наблюдаемого класса "состояние", а затем класса передачи данных, который может быть сериализован, является дублирование полей модели. Кажется, единственное хорошее решение, которое достигает того, что я хочу сделать, - это аннотировать с помощью @Что-то, а затем генерировать шаблон с помощью KSP.
Jan Vladimir Mostert

На других языках

Эта страница на других языках

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