[FIXED] Kotlin/Java: Add multiple items to a Builder class that only allows one .add() at a time

Issue

Problem: I’m using an AWS DynamoDb Enhanced Java client V2 in Kotlin (the details of the client might not be important), where I want to make a BatchWriteItemEnhancedRequest containing a bunch of requests. The standard usage according to the docs goes:

val batchWriteItemEnhancedRequest = BatchWriteItemEnhancedRequest.builder()
    .addWriteBatch(
        WriteBatch.builder(MyClass::class.java)
            .mappedTableResource(myMappedTable)
            .addPutItem(putRequest1)
            .addPutItem(putRequest2)
            .addPutItem(putRequest3)
            .build()
    )
    .build()

Obviously this can’t handle an anonymous list of putRequest items, only some known discrete values like putRequest1 and putRequest2.

The builder for WriteBatch doesn’t seem to allow adding lists of requests: doc

The only way to do this, as I can see, is making a for-loop that goes:

var writeBatchBuilder = WriteBatch.builder(MyClass::class.java)
    .mappedTableResource(myMappedTable)

for (request in putItemRequests){
    writeBatchBuilder = writeBatchBuilder.addPutItem(request)
}

val writeBatch = writeBatchBuilder.build()

Which seems horrible to me. There’s got to be a more idiomatic way to do this right?

Solution

Given an instance of WriteBatch.Builder, there’s no need to explicit need to chain the DSL, so your writeBatchBuilder can be a val. Each builder function mutates the internal state of the WriteBatch.Builder instance, and the returned value is just for convenience – source code.

You can therefore simplify your for-loop and make writeBatchBuilder a val:

val writeBatchBuilder = WriteBatch.builder(MyClass::class.java)
    .mappedTableResource(myMappedTable)

for (request in putItemRequests){
    writeBatchBuilder.addPutItem(request)
}

val writeBatch = writeBatchBuilder.build()

Scope function

Going one step further, scope functions are a nice way of converting Java DSLs into something more Kotlin-esque.

(For a more visual guide to scope functions, and Kotlin in general, check out https://typealias.com/start/kotlin-scopes-and-scope-functions/)

apply {} will turn the created WriteBatch.Builder into the receiver, meaning you don’t need to have a variable for the builder, you can just use this.

val writeBatch = WriteBatch.builder(MyClass::class.java).apply {
  mappedTableResource(myMappedTable)

  putItemRequests.forEach { request -> 
    addPutItem(request)
  }
}.build()

(I also changed the for loop into a forEach {} – which is more Kotlin-ey.)

Custom helper function

The next improvement is to encapsulate the apply {} logic inside of a helper function, writeBatchBuilder(), so the boilerplate is hidden away, which makes re-using the function easier.

Using inline and reified T means there’s no need to define ::class.java every time – instead the class can be defined as a type-parameter, and the class can be omitted if Kotlin can infer the type.

Note that the receiver of the builder arg is WriteBatch.Builder, just like how the apply {} scope function changes the receiver to be the builder.

inline fun <reified T> buildWriteBatch(
  builder: WriteBatch.Builder<T>.() -> Unit
) : WriteBatch<T> {
  return WriteBatch.builder(T::class.java).apply(builder).build()
}

Example usage:

val writeBatch = buildWriteBatch<MyClass> {
  mappedTableResource(myMappedTable)

  putItemRequests.forEach { request -> 
    addPutItem(request)
  }
}


// no need for explicit typing on buildWriteBatch - Kotlin can infer the type
val writeBatch2: WriteBatch<MyClass> = buildWriteBatch {}

(For examples of this approach in Kotlin stdlib, see the collection builder functions)

Answered By – aSemy

Answer Checked By – Katrina (Easybugfix Volunteer)

Leave a Reply

(*) Required, Your email will not be published