1️⃣: We often use interfaces to define a contract for a set of our classes in Object Oriented Programming. Sometimes the implementing classes are
data classes 📦 that have powerful methods like copy and equals.
The copy function allows us to copy an object while altering some of its properties.
2️⃣: It is also common to declare properties as val in order to make them immutable. Furthermore, not only data but also regular classes
(why wouldn't I make every eligible class a data class?)
may implement the interface and those classes do not have the copy method by default.
1️⃣-2️⃣: These yield to (immutable) classes that conform to a common interface, without the possibility
of handling them uniformly (i.e. without the need to know their actual (sub)types when dealing with the interface).
This leads to retrieving the actual (sub)types of the objects each time and either
invoking the copy method or modifying them in some other way.
We seek 👀 for a solution that allows us to update the properties of our objects in an easier manner, avoiding boilerplate and without the need of knowing their actual types. We want to define a method on the interface that lets us update its properties. We focus on the interface itself rather than the implementing classes: if the subtypes would really imply totally different flows (e.g. flows that operate on the subtype specific properties too), then the Visitor pattern 🤠 should be taken into consideration as detailed here. Thus, the problem formulation is really similar to this.
ℹ️ As we will see, the proposed solution does not work if
- the
copymethod is defined on the interface - the implementing
dataclass' properties are the same as the declared props of the interface
Spoiler
❗ Even though the data class defines a copy method, we cannot mark it as an override of the one defined on the interface:
Function 'copy' generated for the data class has default values for parameters, and conflicts with member of supertype 'Fruit'
The Fruit interface declares the name, color and taste properties as val, and the implementing classes
extend this model with an appropriate property for demonstration: the Apple 🍎, Banana 🍌 and Tomato 🍅 classes,
the former two are data classes, while the latter is a regular one.
In the following subsections we will update the name property of our Fruit objects, but keep in mind that any other property of
the interface could be updated in a similar manner.
src/main/kotlin/com/copy/iface/separate and run src/main/kotlin/com/copy/iface/separate/MainSeparate.kt.
See src/main/kotlin/com/copy/iface/separate/modifier/ModifierMethods.kt.
We can define a conditional expression with multiple branches using Kotlin's
when construct (it's useful to combine it with the is and !is operators).
We differentiate between the types of the objects and call the copy method (for Apple and Banana) or
create a new object (for Tomato). Whenever we want to modify the name of the Fruit we have to get its actual subtype
(this is what we would like to avoid), unless
we put this method into a dedicated service (but then what about the possibly modifiable other props of Fruit?).
So we cannot really deal with a Fruit as Fruit.
Furthermore, this approach violates the O (Open-closed principle: software entities should be open for extension, closed for modification) of the
SOLID principles 📚: a new branch should be added to the
when expressions every time we create a new subtype of Fruit (this wouldn't happen often, but the possibility is there).
See src/main/kotlin/com/copy/iface/separate/modifier/FruitModifier.kt and its implementations.
A similar solution to the one above (1.1) is to define a FruitModifier
interface and implement it with Fruit subtype specific subclasses.
The modifyName method delegates to the copy method of the data classes or calls the constructor of Tomato.
This approach would mean that a new Modifier class should be created for each new subtype of Fruit
and also a new method should be added to FruitModifier and implemented in its subclasses whenever we want to modify another property of Fruit.
src/main/kotlin/com/copy/iface/common and run src/main/kotlin/com/copy/iface/common/MainCommon.kt.
We may define the copy method on the interface: this method takes all of the Fruit properties as arguments and returns a new Fruit object.
An override of this either calls the copy method (Banana) originating from the data class or the constructor (Apple and Tomato).
This method should have default values for all of its parameters, so that it can be called with an arbitrary number of arguments.
ℹ️ The implementing data classes will have two copy methods: one from the interface and one from the data class.
Notice that the name of the method is up to us: the chosen copy name is suitable because its purpose is the same as the copy method of the data classes.
It's important to note here that these methods should be defined only once ♻️, and their implementation is very easy: they simply delegate to the appropriate object creation.
Now we may use the copy method defined on the Fruit interface (notice the Fruit type declaration) as follows:
val apple: Fruit = Apple()
val modifiedApple: Fruit = apple.copy(name = "Modified ${apple.name}")💡 Play a bit with the Tomato class: add the data modifier to it and see what happens.
Spoiler
❗ The IDE complains about the copy method that takes all primary constructor arguments as parameters:
Conflicting overloads: public final fun copy(name: String = ..., color: String = ..., taste: Taste = ..., didIKnowThatItsAFruit: Boolean = ...): Tomato defined in com.copy.iface.common.fruit.Tomato
💡 As it was mentioned in the Problem formulation, the copy method of the implementing data classes cannot
override the one defined on the interface. Replace the Apple class with the following:
data class Apple(
override val name: String = "Apple",
override val color: String = "Red",
override val taste: Taste = Taste.SWEET,
) : FruitFunction 'copy' generated for the data class has default values for parameters, and conflicts with member of supertype 'Fruit'
We cannot use the copy method of the data class as an implementation of the one defined on the interface since it lacks the override modifier.
Because of this, the proposed solution would not work for data classes that have the same properties as the interface.
See src/main/kotlin/com/copy/iface/common/modifier/ModifierMethods.kt.
There are three different methods that modify the name property of the Fruit objects defined in this class,
but they all ultimately call the copy method of the Fruit interface. There are certain scenarios where
they may or may not be used, the programmer has to decide which one to use in a specific case.
:exclamation: As we can see the IDE complains that there is an unchecked cast in the modifyNameWithGenericParam method:
Unchecked cast: Fruit to T
T to be the subtype of Fruit with <T : Fruit>, in my opinion we can disregard this warning.
See src/main/kotlin/com/copy/iface/common/modifier/FruitModifier.kt and its implementation.
Similarly to 1.2 we can define a FruitModifier interface, but this time without the unnecessary
type parameter T. This should be implemented by only one class, the FruitModifierImpl that delegates to the copy method of the Fruit interface.
With this solution we reduced ⬇️ the number of classes and methods, however we still have to create a new method in
FruitModifierImpl whenever we want to modify another property of Fruit, but this is what we wanted all along:
to treat the Fruit objects uniformly.
✅ This repository shows how to implement the copy method for an interface in order to handle these objects uniformly,
without the need to retrieve their actual types and to reduce boilerplate and duplicated code. Also, the same can be done
for abstract classes.
- Unchecked cast for Kotlin Collections
- Unchecked cast at generic Kotlin factory
- Handling unchecked cast warning in Kotlin
- Copyable interface without arguments in Kotlin
- Open-closed principle
See classes in src/main/kotlin/com/copy/nested and compare src/main/kotlin/com/copy/nested/arrow/MainArrow.kt with
src/main/kotlin/com/copy/nested/regular/MainRegular.kt.
A simple demonstration how the Arrow library can make one's life easier when trying to update nested properties.