From 6c9a39ae4405afb3987dd0963052420c9a978dc5 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Wed, 28 Mar 2018 09:40:21 +0100 Subject: [PATCH] DOCS: Serialization roundtrip removes mutability explanation (#2888) --- docs/source/serialization.rst | 79 +++++++++++++++++++ .../serialization/amqp/RoundTripTests.kt | 60 ++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/RoundTripTests.kt diff --git a/docs/source/serialization.rst b/docs/source/serialization.rst index de984b9b18..5a26d81cec 100644 --- a/docs/source/serialization.rst +++ b/docs/source/serialization.rst @@ -442,6 +442,85 @@ associates it with the actual member variable. fun getStatesToConsume() = states } +Mutable Containers +`````````````````` + +Because Java fundamentally provides no mechanism by which the mutability of a class can be determined this presents a +problem for the serialization framework. When reconstituting objects with container properties (lists, maps, etc) we +must chose whether to create mutable or immutable objects. Given the restrictions, we have decided it is better to +preserve the immutability of immutable objects rather than force mutability on presumed immutable objects. + +.. note:: Whilst we could potentially infer mutability empirically, doing so exhaustively is impossible as it's a design + decision rather than something intrinsic to the JVM. At present, we defer to simply making things immutable on reconstruction + with the following workarounds provided for those who use them. In future, this may change, but for now use the following + examples as a guide. + +For example, consider the following: + +.. sourcecode:: kotlin + + data class C(val l : MutableList) + + val bytes = C(mutableListOf ("a", "b", "c")).serialize() + val newC = bytes.deserialize() + + newC.l.add("d") + +The call to ``newC.l.add`` will throw an ``UnsupportedOperationException``. + +There are several workarounds that can be used to preserve mutability on reconstituted objects. Firstly, if the class +isn't a Kotlin data class and thus isn't restricted by having to have a primary constructor. + +.. sourcecode:: kotlin + + class C { + val l : MutableList + + @Suppress("Unused") + constructor (l : MutableList) { + this.l = l.toMutableList() + } + } + + val bytes = C(mutableListOf ("a", "b", "c")).serialize() + val newC = bytes.deserialize() + + // This time this call will succeed + newC.l.add("d") + +Secondly, if the class is a Kotlin data class, a secondary constructor can be used. + +.. sourcecode:: kotlin + + data class C (val l : MutableList){ + @ConstructorForDeserialization + @Suppress("Unused") + constructor (l : Collection) : this (l.toMutableList()) + } + + val bytes = C(mutableListOf ("a", "b", "c")).serialize() + val newC = bytes.deserialize() + + // This will also work + newC.l.add("d") + +Thirdly, to preserve immutability of objects (a recommend design principle - Copy on Write semantics) then mutating the +contents of the class can be done by creating a new copy of the data class with the altered list passed (in this example) +passed in as the Constructor parameter. + +.. sourcecode:: kotlin + + data class C(val l : List) + + val bytes = C(listOf ("a", "b", "c")).serialize() + val newC = bytes.deserialize() + + val newC2 = newC.copy (l = (newC.l + "d")) + +.. note:: If mutability isn't an issue at all then in the case of data classes a single constructor can + be used by making the property var instead of val and in the ``init`` block reassigning the property + to a mutable instance + Enums ````` diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/RoundTripTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/RoundTripTests.kt new file mode 100644 index 0000000000..f42823a862 --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/RoundTripTests.kt @@ -0,0 +1,60 @@ +package net.corda.nodeapi.internal.serialization.amqp + +import net.corda.core.serialization.ConstructorForDeserialization +import org.assertj.core.api.Assertions +import org.junit.Test + +class RoundTripTests { + @Test + fun mutableBecomesImmutable() { + data class C(val l : MutableList) + val factory = testDefaultFactoryNoEvolution() + val bytes = SerializationOutput(factory).serialize(C(mutableListOf ("a", "b", "c"))) + val newC = DeserializationInput(factory).deserialize(bytes) + + Assertions.assertThatThrownBy { + newC.l.add("d") + }.isInstanceOf(UnsupportedOperationException::class.java) + } + + @Test + fun mutableStillMutable() { + class C { + val l : MutableList + + @Suppress("Unused") + constructor (l : MutableList) { + this.l = l.toMutableList() + } + } + val factory = testDefaultFactoryNoEvolution() + val bytes = SerializationOutput(factory).serialize(C(mutableListOf ("a", "b", "c"))) + val newC = DeserializationInput(factory).deserialize(bytes) + + newC.l.add("d") + } + + @Test + fun mutableStillMutable2() { + data class C (val l : MutableList){ + @ConstructorForDeserialization + @Suppress("Unused") + constructor (l : Collection) : this (l.toMutableList()) + } + + val factory = testDefaultFactoryNoEvolution() + val bytes = SerializationOutput(factory).serialize(C(mutableListOf ("a", "b", "c"))) + val newC = DeserializationInput(factory).deserialize(bytes) + + newC.l.add("d") + } + + @Test + fun mutableBecomesImmutable4() { + data class C(val l : List) + val factory = testDefaultFactoryNoEvolution() + val bytes = SerializationOutput(factory).serialize(C(listOf ("a", "b", "c"))) + val newC = DeserializationInput(factory).deserialize(bytes) + val newC2 = newC.copy (l = (newC.l + "d")) + } +} \ No newline at end of file