en ru de
Victor Kropp

Object Builders in idiomatic Kotlin

CC-BY-SA · flickr.com/photos/loozrboy/7218773508/

Kotlin supports functions and constructors with named and optional (default) arguments. These features help make object construction clear but don’t help to avoid huge towers of nested constructors. It is also almost impossible to have any conditionally set properties.

data class Article(
  val name: String,
  val text: String,
  val author: Author,
  val comment: Comment
)

val article = Article(
  "Kotlin 1.1 released!",
  "Lorem ipsum dolor sit amet",
  Organization("JetBrains"),
  Comment("Hooray!", Person("John Doe"))
)

Looks nice, however mixing named and positional arguments is not allowed, which doesn’t make construction of complex objects any easier.

Some time ago I created a Java library with a collection of builders for entities defined in Schema.org. This is how typical object builder looks like:

final Article article = SchemaOrg.article()
        .name("Kotlin 1.1 released!")
        .text("Lorem ipsum dolor sit amet")
        .author(organization().name("JetBrains").build())
        .comment(
                comment().text("Hooray!").author(person().name("John Doe").build()).build()
        ).build();

It is such a common pattern, that there is a @FreeBuilder Java library that generates such builders from interfaces. And Groovy has a similar feature built in.

Everybody is used to this pattern, but it still has some downsides:

I propose the following syntax in Kotlin to build the same object:

val article = article {
    name = "Kotlin 1.1 released!"
    text = "Lorem ipsum dolor sit amet"
    author = organization { name = "JetBrains" }
    comment {
        text = "Hooray!"
        author { name = "John Doe" }
    }
}

What advantages does this code have over its Java counterpart?

Looks great? Hell yes! What is even better is that it takes only a few lines of code to implement it!

class ArticleBuilder {
  var name: String
  var text: String
  var author: Author
  var comment: Comment
  fun build() = Article(name, text, author, comment)
}

// and a convenience function to create builder:

fun article(builder: ArticleBuilder.() -> Unit) = ArticleBuilder().apply(builder).build()

The only problem with the code above is that it doesn’t compile unfortunately, because properties must be initialized. There are several possible workarounds:

class ArticleBuilder {
  private val values = mutableMapOf<String,Any>()
  var name: String by values
  
  var comment: Comment by values
}

In this case all type casts are done behind the scenes. This is my preferred approach actually. And if you need this object only to serialize it later to JSON, which basically is just a dictionary, it is very efficient.

When Kotlin adds support for write-only properties, it would be possible to remove getters from var properties making builders another few bytes smaller.

Nested builders are very simple too.

var comment: Comment by values
fun comment(builder: CommentBuilder.() -> Unit) {
  comment = CommentBuilder().apply(builder).build()
}

Voilà!

You can try out builders implemented this way in my jsonld-metadata library, which implements full Schema.org vocabulary. It is available on BinTray as org.schema:jsonld-metadata and org.schema:jsonld-metadata-kotlin.

To be continued.

kotlinprogramming
Send

Subscribe to all blog posts via RSS, follow me @kropp and join my Telegram channel.