I have the following data classes:
sealed class ExampleDto
object Type1ExampleDto : ExampleDto()
object Type2ExampleDto : ExampleDto()
data class Type3ExampleDto(val name: Int, val age: Int) : ExampleDto()
data class Type4ExampleDto(val name: Int, val age: Int) : ExampleDto()
data class Type5ExampleDto(val email: String) : ExampleDto()
data class Type6ExampleDto(val name: Int, val age: Int, val email: String) : ExampleDto()
In particular, Type3ExampleDto, Type4ExampleDto and Type6ExampleDto share some common fields but it's important for my business logic to distinguish between types (i.e. even if Type3ExampleDto and Type4ExampleDto are identical, I have to know if I'm in the type3 or type4 case).
In one of my method I have the following call:
when (type) {
is Type3ExampleDto -> myMethod(type.vote, type.text)
is Type4ExampleDto -> myMethod(type.vote, type.text)
is Type6ExampleDto -> myMethod(type.vote, type.text)
else -> null
}
I find very ugly that I'm doing the same operation in all 3 cases and repeating the same line...
It makes sense to made Type3ExampleDto, Type4ExampleDto and Type6ExampleDto an implementation of some kind of interface just because only in this point I'm doing this kind of ugly repetition?
If all three dtos implement the following interface
interface MyInterface{
fun getVote() : Int
fun getText() : String
}
I can write:
if (type is MyInterface) {
myMethod(type.getVote(), type.getText())
}
So, it's acceptable to create this interface just to solve this isolated repetition?
Thanks
Note you can do it much more cleanly like this:
interface NameAndAgeDto {
val name: Int
val age: Int
}
data class Type3ExampleDto(override val name: Int, override val age: Int) : ExampleDto(), NameAndAgeDto
if (type is NameAndAgeDto) {
myMethod(type.name, type.age)
}
Whether it's "acceptable" is opinion. Looks fine to me.
You may change your model to have your logic based on behaviour instead of inheritance.
This way of modelling is based on principles of (but ain't exactly) Strategy Design Pattern.
interface HasName {
val name: String
}
interface HasAge {
val age: Int
}
interface HasEmail {
val email: String
}
object Type1
object Type2
data class Type3(
override val name: String,
override val age: Int
) : HasName, HasAge
data class Type4(
override val name: String,
override val age: Int
) : HasName, HasAge
data class Type5(
override val email: String
) : HasEmail
data class Type6(
override val name: String,
override val age: Int,
override val email: String
) : HasName, HasAge, HasEmail
// Then you can pass any object to it.
fun main(obj: Any) {
// Koltin type-casts it nicely to both interfaces.
if (obj is HasName && obj is HasAge) {
myMethod(text = obj.name, vote = obj.age)
}
}
fun myMethod(vote: Int, text: String) {
}
If you still want all the types to belong to some parent type, you can use marker interface (without any methods).
interface DTO
interface HasName {
val name: String
}
interface HasAge {
val age: Int
}
interface HasEmail {
val email: String
}
object Type1 : DTO
object Type2 : DTO
data class Type3(
override val name: String,
override val age: Int
) : HasName, HasAge, DTO
data class Type4(
override val name: String,
override val age: Int
) : HasName, HasAge, DTO
data class Type5(
override val email: String
) : HasEmail, DTO
data class Type6(
override val name: String,
override val age: Int,
override val email: String
) : HasName, HasAge, HasEmail, DTO
// Here, it is DTO instead of Any
fun main(obj: DTO) {
if (obj is HasName && obj is HasAge) {
myMethod(text = obj.name, vote = obj.age)
}
}
And use sealed class instead of marker interface if you need classes as enum.
In that case, when over sealed class is exhaustive with all options without null ->.
Related
I am creating a data class in kotlin as such
data class User(val name: String, val age: Int)
{
constructor(name: String, age: Int, size: String): this(name, age) {
}
}
In my main function, I can access the objects as such:
fun main(){
val x = User("foo", 5, "M")
println(x.name)
println(x.age)
println(x.size) // does not work
}
My problem is that I can't get access to size.
What I am trying to do is, create a data class where top level params are the common items that will be accessed, and in the constructors, have additional params that fit certain situations. The purpose is so that I can do something like
// something along the lines of
if (!haveSize()){
val person = User("foo", 5, "M")
} else {
val person = User("foo", 5)
}
}
Any ideas?
In Kotlin you do not need separate constructors for defining optional constructor params. You can define them all in a single constructor with default values or make them nullable, like this:
data class User(val name: String, val age: Int, val size: String = "M")
fun main(){
val x = User("foo", 5, "L")
val y = User("foo", 5)
println(x.size) // "L" from call site
println(y.size) // "M" from default param
}
You can not access size variable, because this is from secondary construct, but we have alternative variant.
data class User(var name: String, var age: Int) {
var size: String
init {
size = "size"
}
constructor(name: String, age: Int, size: String) : this(name, age) {
this.size = size
}
}
In short, you want to have one property that can be one of a limited number of options. This could be solved using generics, or sealed inheritance.
Generics
Here I've added an interface, MountDetails, with a generic parameter, T. There's a single property, val c, which is of type T.
data class User(
val mountOptions: MountOptions,
val mountDetails: MountDetails<*>,
)
data class MountOptions(
val a: String,
val b: String
)
interface MountDetails<T : Any> {
val c: T
}
data class MountOneDetails(override val c: Int) : MountDetails<Int>
data class MountTwoDetails(override val c: String) : MountDetails<String>
Because the implementations MountDetails (MountOneDetails and MountTwoDetails) specify the type of T to be Int or String, val c can always be accessed.
fun anotherCaller(user: User) {
println(user.mountOptions.a)
println(user.mountOptions.b)
println(user.mountDetails)
}
fun main() {
val mt = MountOptions("foo", "bar")
val mountOneDetails = MountOneDetails(111)
anotherCaller(User(mt, mountOneDetails))
val mountTwoDetails = MountTwoDetails("mount two")
anotherCaller(User(mt, mountTwoDetails))
}
Output:
foo
bar
MountOneDetails(c=111)
foo
bar
MountTwoDetails(c=mount two)
Generics have downsides though. If there are lots of generic parameters it's messy, and it can be difficult at runtime to determine the type of classes thanks to type-erasure.
Sealed inheritance
Since you only have a limited number of mount details, a much neater solution is sealed classes and interfaces.
data class User(val mountOptions: MountOptions)
sealed interface MountOptions {
val a: String
val b: String
}
data class MountOneOptions(
override val a: String,
override val b: String,
val integerData: Int,
) : MountOptions
data class MountTwoOptions(
override val a: String,
override val b: String,
val stringData: String,
) : MountOptions
The benefit here is that there's fewer classes, and the typings are more specific. It's also easy to add or remove an additional mount details, and any exhaustive when statements will cause a compiler error.
fun anotherCaller(user: User) {
println(user.mountOptions.a)
println(user.mountOptions.b)
// use an exhaustive when to determine the actual type
when (user.mountOptions) {
is MountOneOptions -> println(user.mountOptions.integerData)
is MountTwoOptions -> println(user.mountOptions.stringData)
// no need for an 'else' branch
}
}
fun main() {
val mountOne = MountOneOptions("foo", "bar", 111)
anotherCaller(User(mountOne))
val mountTwo = MountTwoOptions("foo", "bar", "mount two")
anotherCaller(User(mountTwo))
}
Output:
foo
bar
111
foo
bar
mount two
This is really the "default values" answer provided by Hubert Grzeskowiak adjusted to your example:
data class OneDetails(val c: Int)
data class TwoDetails(val c: String)
data class MountOptions(val a: String, val b: String)
data class User(
val mountOptions: MountOptions,
val detailsOne: OneDetails? = null,
val detailsTwo: TwoDetails? = null
)
fun main() {
fun anotherCaller(user: User) = println(user)
val mt = MountOptions("foo", "bar")
val one = OneDetails(1)
val two = TwoDetails("2")
val switch = "0"
when (switch) {
"0" -> anotherCaller(User(mt))
"1" -> anotherCaller(User(mt, detailsOne = one))
"2" -> anotherCaller(User(mt, detailsTwo = two))
"12" -> anotherCaller(User(mt, detailsOne = one, detailsTwo = two))
else -> throw IllegalArgumentException(switch)
}
}
I have a class ExpenseDto
data class ExpenseDto(
val id: Int,
val name: String,
val aggregationA: ExpenseAggregationA,
val aggregationB: ExpenseAggregationB,
val aggregationC: ExpenseAggregationC,
)
And all its associations have the same fields. And what are the best practies that can be applied here to universalize it
data class ExpenseAggregationA(
val id: Int,
val text: String? = null
)
data class ExpenseAggregationB(
val id: Int,
val text: String? = null
)
data class ExpenseAggregationC(
val id: Int,
val text: String? = null
)
data classes can inherit from a sealed class.
sealed class ExpenseAggParent (val id: Int, val text: String? = null) {
data class ExpAggA( override val id: Int, override ...)
data class ...
data class ...
}
Besides that, sometimes I like to have "property unifier" interfaces, especially when refactoring a legacy code with classes with the same semantics but different names of the properties:
interface HasId(val id: Int)
data class ExpenseAggregationX(
override val id: Int,
...
): HasId
data class ExpenseAggregationY: HasId {
val someOtherNameButStillId: Int
override val id: Int
get() = this.someOtherNameButStillId
}
And, of course, you can use the good old interface, but for that, you may re-use the above approach:
interface ExpenseAggregation: HasId, HasText
data class ExpenseAggregationA(
override val id: Int,
...
): ExpenseAggregation
// Other approach for classes with other existing names, see above
data class ExpenseAggregationB: ExpenseAggregation {
...
}
This is close to a mix-in approach which Kotlin does not currently support directly, but for completeness, there are delegations usable if your class is rather a service than a DTO.
Consider I have a base of an open hierarchy where all the properties are delegated to a single map:
interface JsonExternalizable {
fun serialize(): MutableMap<String, JsonNode>
}
abstract class AbstractJsonExternalizable protected constructor(private val map: Map<String, Any?>) : JsonExternalizable {
override fun serialize(): MutableMap<String, JsonNode> {
// ...
}
}
Now a sample immutable implementation (I want to have an additional secondary constructor where the list of arguments would "mirror" the type's corresponding properties):
open class Bean1 protected constructor(map: Map<String, Any?>) : AbstractJsonExternalizable(map) {
constructor(property1: String,
property2: Int,
property3: Boolean) : this(mapOf(Bean1::property1 to property1,
Bean1::property2 to property2,
Bean1::property3 to property3).mapKeys { it.key.name })
val property1: String by map
val property2: Int by map
val property3: Boolean by map
}
Things get worse for more "distant" descendants, as I have to list every supertype property (along with the own properties) in the secondary constructor:
class Bean3 private constructor(map: Map<String, Any?>) : Bean1(map) {
val propertyX: Array<Byte> by map
constructor(property1: String,
property2: Int,
property3: Boolean,
propertyX: Array<Byte>) : this(mapOf(Bean3::property1 to property1,
Bean3::property2 to property2,
Bean3::property3 to property3,
Bean3::propertyX to propertyX).mapKeys { it.key.name })
}
Is there a more elegant way to express the same idea, with less boilerplate code?
I have the following abstract class:
abstract class AbstractBook {
abstract val type: String
abstract val privateData: Any
abstract val publicData: Any
}
and the following class which inherits the AbstactBook class:
data class FantasyBook (
override val type: String = "FANTASY",
override val privateData: FantasyBookPrivateData,
override val publicData: FantasyBookPublicData
) : AbstractBook()
And then there is this class which should include data from any type of AbstractBook:
data class BookState(
val owner: String,
val bookData: AbstractBook,
val status: String
)
If I have an instance of BookState, how do I check which type of Book it is and then access the according FantasyBookPrivateData, and FantasyBookPublicData variables?
I hope I described my issue well & thanks in advance for any help!
What you describe is a sealed class:
sealed class Book<T, K> {
abstract val type: String
abstract val privateData: T
abstract val publicData: K
data class FantasyBook(
override val type: String = "FANTASY",
override val privateData: String,
override val publicData: Int) : Book<String, Int>()
}
and in your data class you can do pattern matching like this:
data class BookState(
val owner: String,
val bookData: Book<out Any, out Any>,
val status: String) {
init {
when(bookData) {
is Book.FantasyBook -> {
val privateData: String = bookData.privateData
}
}
}
}
to access your data in a type-safe manner. This solution also makes type redundant since you have that information in the class itself.
I agree with #Marko Topolnik that this seems like a code smell, so you might want to rethink your design.
interface AbstractBook<T , U> {
val privateData: T
val publicData: U
}
data class FantasyBook (
override val privateData: FantasyBookPrivateData,
override val publicData: FantasyBookPublicData
) : AbstractBook<FantasyBookPrivateData , FantasyBookPublicData>
data class BookState(
val owner: String,
val bookData: AbstractBook<*, *>,
val status: String
)
if(bookState.bookData is FantasyBook) {
// Do stuff
}
Creating a type variable is a weak type language writing style. You should use generic class.
We have an abstract Java class (which we can't modify) called AbstractClass that we want to implement in Kotlin. A requirement is the Kotlin implementation is serializable/deserializable to JSON using vanilla Jackson Databind. This has lead us to the following implementation:
class MyClass(private val data: MyClassData? = null) : AbstractClass<MyClassData>(MyClass::class.java, "1") {
data class MyClassData(var name: String = "", var age: Int = 0) : AbstractData
override fun getData(): MyClassData? {
return data
}
}
This class will always be used from Java and currently you can instantiate it like this (Java):
MyClass myClass = new MyClass(new MyClassData("John Doe", 25));
But we'd prefer to instantiate it like this instead:
MyClass myClass = new MyClass("John Doe", 25);
I can of course change the Kotlin code to something like this:
class MyClass(#JsonIgnore private var name: String = "", #JsonIgnore private var age: Int = 0) : AbstractClass<MyClassData>(MyClass::class.java, "1") {
data class MyClassData(var name: String = "", var age: Int = 0) : AbstractData
private var data : MyClassData? = null
init {
data = MyClassData(name, age)
}
override fun getData(): MyClassData? {
return data
}
}
but this is very verbose and kind of defeats the purpose of using Kotlin.
What I think I'd like to do is something like this (pseudo code):
class MyClass(private val data: MyClassData? = null by MyClassData) : AbstractClass<MyClassData>(MyClass::class.java, "1") {
data class MyClassData(var name: String = "", var age: Int = 0) : AbstractData
override fun getData(): MyClassData? {
return data
}
}
(note the by MyClassData in the MyClass constructor which obviously doesn't work)
I.e. I'd like to somehow destruct or delegate the constructor of MyClass to take the same arguments as MyClassData without duplicating them. Is this something you can do in Kotlin or is there another way to solve it without adding too much code?
I think your main concerns are: (a) concise external API, (b) clean internal state (for Jackson)
Secondary Constructor
This is pretty lean:
class MyClass internal constructor(private val data: MyClassData)
: AbstractClass<MyClass>(MyClass::class.java, "1") {
data class MyClassData(var name: String, var age: Int) : AbstractData
constructor(name: String, age: Int) : this(MyClassData(name, age))
override fun getData(): MyClassData? = data
}
Simple API without creating extra fields (though I think this syntax is misleading):
val myClass = MyClass("John Doe", 25)
Pass-Thru Params & Initializer:
This was my first idea: directly pull out the outer class' params (though I now think the secondary constructor is nicer since it doesn't pollute the outer class):
class MyClass(#JsonIgnore private val name: String, #JsonIgnore private val age: Int)
: AbstractClass<MyClass>(MyClass::class.java, "1") {
data class MyClassData(var name: String, var age: Int) : AbstractData
private val data = MyClassData(this#MyClass.name, this#MyClass.age)
override fun getData(): MyClassData? = data
}
...again, same API:
val myClass = MyClass("John Doe", 25)
Self-Factory
This approach has potentially more descriptive syntax:
class MyClass(private val data : MyClassData)
: AbstractClass<MyClass>(MyClass::class.java, "1") {
data class MyClassData(var name: String, var age: Int) : AbstractData
companion object Factory {
fun create(name: String, age: Int) = MyClass(MyClassData(name, age))
}
override fun getData(): MyClassData? = data
}
This can be called like this:
val myClass = MyClass.Factory.create("John Doe", 25)
Conceptual Syntax: Structuring (DOES NOT EXIST)
I kind of like the idea of a 'structuring' syntax for method arguments that would group inputs into an object (the opposite of destructuring); a bit like varargs (i.e. syntactic sugar):
class MyClass(
private val data: (name: String, age: Int) : MyClassData(name, age)
) { ... }
This could be called in either of two ways:
val myClass1 = MyClass(MyClassData("John Doe", 25))
val myClass2 = MyClass("John Doe", 25)
In practice it's a rare requirement, and easily manageable with explicit overloads for just a few extra chars, so I don't think it will ever happen.