dirk.dittert
04/29/2025, 8:37 AMdata class Column(
private val _csvName: String,
val someOtherInfo: SomeInfo) {
val csvName: String = _csvName.uppercase()
init {
if (!csvName.startsWith("A")) {
throw IllegalArgumentException("Noooo!")
}
}
How do you properly write documentation for that without scattering the information in several places? After all _csvName
is just a technical artifact that is required
due to Kotlin's syntax.
Here is what I mean:
/**
* Some Explanation
*
* @param _csvName placeholder to be able to transform the name to uppercase. See following description of [csvName]
* @param someOtherInfo Some other flags.
*/
data class Column(
How do you handle something like that in your projects?hfhbd
04/29/2025, 8:43 AMhfhbd
04/29/2025, 8:44 AMdirk.dittert
04/29/2025, 8:45 AMdirk.dittert
04/29/2025, 8:46 AMhfhbd
04/29/2025, 8:47 AMdirk.dittert
04/29/2025, 8:48 AMdirk.dittert
04/29/2025, 8:48 AMCLOVIS
04/29/2025, 9:08 AMdata class
is very often overused. Currently, your code:
Column("foo", …) == Column("FOO", …)
is false
, which I'm pretty sure isn't what you want.
A few techniques, depending on what you're doing;
A. Regular class and property initialization
class Column(
csvName: String
) {
val csvName = csvName.uppercase()
}
B. Private constructor
class Column private constructor(
val column: String,
) {
constructor(column: String) : this(column.uppercase())
}
The main downside is that both constructors need to have a different signature. You can workaround this by using a ignored: Unit = Unit
last parameter in the private constructor, which won't have any performance impact and won't be visible in your API, but it's still not beautiful.
Alternatively, you can put the secondary constructor as a factory method in the companion object, which is the same at the callsite with a static import.
Note that this technique does allow you to make Column
a data class
, and equals
and hashCode
will behave the way you expect.
C. Value class
If you have these kinds of "strings that must be uppercase" in many places, creating a dedicated type can solve this.
@JvmInline value class UppercaseString private constructor(
val value: String,
) {
override fun toString() = value
companion object {
fun UppercaseString(value: String) = UppercaseString(value.uppercase())
}
}
class Column(
val name: UppercaseString,
)
This is slightly more verbose on the callsite (unless you create a helper factory) but it is more typesafe and explicit to readers. It removes the complexity from Column
, ensures you never have to verify multiple times, and allows to make Column
a data class
.dirk.dittert
04/29/2025, 9:22 AMColumn("foo", …) != Column("FOO", …)
. That is, because the generated equals
uses the private _csvName
instead, isn't it? Thanks for pointing that subtle bug out.CLOVIS
04/29/2025, 9:37 AMCLOVIS
04/29/2025, 9:39 AMdata class
as "something that is identified by the contents of the primary constructor". If you have conversion functions to access the data, it's a code smell that the wrong data is in the constructor. Either it shouldn't be a data class
(most DTOs probably shouldn't be data classes, it's not like we ever use ==
on them), or the conversion should happen before the primary constructor (either in a secondary constructor or in a factory function)Michael de Kaste
04/29/2025, 2:36 PM@ConsistentCopyVisibility
data class AgbCode private constructor(
val value: String,
) {
init {
require(value.matches(REGEX)) { "Ongeldige AGB-code" }
}
companion object {
private val REGEX = Regex("""\d{8}""")
fun validate(code: String): AgbCode? = if (code.matches(REGEX)) AgbCode(code) else null
}
}