Go Back

Kotlin | Extension Functions, make any class have what you wish for

Posted by Simar Paul Singh on 2018-09-05

Kotlin allows us to extend a class with new functionality at compile-time without having to inherit from any parent class using a extension functions.

In Java, only mechanizm to extend a class at compile-time is to extend from another known class having the functionality (non-private methods / attributes) available to the inheriting classes. There are many options extend functionality of instance by encapsulating it at run-time, using design patterns such as Decorator, Proxy, instance of check and casting. We are not going to discuss them here.

Kotlin supports both extension functions and extension properties. You can find more on these special declarations called extensions on Kotlin’s official documentation here.

We will take a simple and useful example, very common class, JsonNode from fasterxml.jackson. . Transforming JsonNode (tree style) object, is not convenient as modifying a Map<String, Any>. However, ObjectMapper comes with built in functions to convert JsonNode from / to a Map<String, Any>. We will extend its functionality of JsonNode to allow transforming of a JsonNode instance by setting properties as we would do on a Map<String, Any> object.

Let us magically extend JsonNode class with two extension functions.

  1. JsonNode.toMap(): MutableMap<String, Any>, which receives an instance JsonNode as (this) and can convert the JsonNode tree into Map<String, Any> using already available ObjectMapper functionality.
  2. JsonNode.transform(fn: MutableMap<String,Any>.()-> Unit) :JsonNode, which receives an instance of JsonNode (jsonNode) as (this) and a Function / Unit (fn) as an an argument to set properties on Map<String, Any> as its receiver.

See the implementation below. When jsonNode.transform(fn) is called, it first uses JsonNode.toMap() extension function previously defined to create a Map<String, Any> (map) from instance of JsonNode (jsonNode) it was called upon, and then calls the map.fn() [aka. map.also(fn)], where (fn) was passed in as the unit function applying all the modifications to the (map) built from (jsonNode) and finally return the resulting Map<String, Any> converting it to a JsonNode again using the ObjectMapper

  private fun jsonNode(): JsonNode = mapper.readValue(
       "name": "Simar",
       "country": "Canada"
      """) // triple quoted for multi-line strings

  fun JsonNode.transform(fn: MutableMap<String, Any>.() -> Unit): JsonNode =
          toMap().also(fn).let { _mapper_.valueToTree(_it_) } fun JsonNode.toMap(): MutableMap<String, Any> =
    (_mapper_.readValue(StringReader(toString())) as MutableMap<String, Any>)

  public fun tesJsonTransformation()
    val jsonNode = jsonNode();
    assertEquals("Simar", jsonNode.get("name").textValue())
    assertEquals("Canada", jsonNode.get("country").textValue())
    val newJsonNode: JsonNode = jsonNode.transfrorm({ set("name", "Paul")
    assertEquals("Paul", newJsonNode.get("name").textValue())