Using coroutines in real life

Microbenchmarks are very funny and they give us an idea of the power of Kotlin coroutines, but they don't represent a real-case scenario.

Let's introduce our real-case scenario:

enum class Gender {
MALE, FEMALE;

companion object {
fun valueOfIgnoreCase(name: String): Gender = valueOf(name.toUpperCase())
}
}

typealias UserId = Int

data class User(val id: UserId, val firstName: String, val lastName: String, val gender: Gender)

data class Fact(val id: Int, val value: String, val user: User? = null)

interface UserService {
fun getFact(id: UserId): Fact
}

Our UserService interface has just one method—getFact will return a Chuck Norris-style fact about our user, identified by the user ID.

The implementation should check first on a local database for a user; if the user doesn't exist in the database, it should get it from the RandomUser API service, (https://randomuser.me/documentation), and then store for future use. Once the service has a user, it should check again in the database for a fact related to that user; if the fact doesn't exist in the database, it should get it from The Internet Chuck Norris Database API service, (http://www.icndb.com/api/), and store it in the database. Once the service has a fact, it could be returned. The service must try to reduce the number of external calls (database, API services) without using a cache.

Now, let's introduce other interfaces, HTTP clients—UserClient and FactClient:

interface UserClient {
fun getUser(id: UserId): User
}

interface FactClient {
fun getFact(user: User): Fact
}

Our clients will be implemented using http4k (https://www.http4k.org/) for HTTP communication, and Kotson (https://github.com/SalomonBrys/Kotson) for JSON processing. Both libraries are being designed for Kotlin, but any other library should work fine:

import com.github.salomonbrys.kotson.*
import com.google.gson.GsonBuilder
import org.http4k.client.ApacheClient

abstract class
WebClient {
protected val apacheClient = ApacheClient()

protected val gson = GsonBuilder()
.registerTypeAdapter<User> {
deserialize { des ->
val json = des.json
User(json["info"]["seed"].int,
json["results"][0]["name"]["first"].string.capitalize(),
json["results"][0]["name"]["last"].string.capitalize(),
Gender.valueOfIgnoreCase(json["results"][0]["gender"].string))

}
}
.registerTypeAdapter<Fact> {
deserialize { des ->
val json = des.json
Fact(json["value"]["id"].int,
json["value"]["joke"].string)
}
}.create()!!
}

Both clients will extend a common parent class that contains http4k ApacheClient and a Gson value configured with Kotson DSL:

import org.http4k.core.Method
import org.http4k.core.Request

class
Http4KUserClient : WebClient(), UserClient {
override fun getUser(id: UserId): User {
return gson.fromJson(apacheClient(Request(Method.GET, "https://randomuser.me/api")
.query("seed", id.toString()))
.bodyString())
}
}

Http4KUserClient is very simple, both libraries are easy to use, and we move a lot of code to the parent class:

class Http4KFactClient : WebClient(), FactClient {
override fun getFact(user: User): Fact {
return gson.fromJson<Fact>(apacheClient(Request(Method.GET, "http://api.icndb.com/jokes/random")
.query("firstName", user.firstName)
.query("lastName", user.lastName))
.bodyString())
.copy(user = user)
}
}

Http4KFactClient sets the user value inside the Fact instance, using the copy method.

These classes are very nicely implemented, but to test the actual performance of our algorithm, we will mock these interfaces:

class MockUserClient : UserClient {
override fun getUser(id: UserId): User {
println("MockUserClient.getUser")
Thread.sleep(500)
return User(id, "Foo", "Bar", Gender.FEMALE)
}
}

class MockFactClient : FactClient {
override fun getFact(user: User): Fact {
println("MockFactClient.getFact")
Thread.sleep(500)
return Fact(Random().nextInt(), "FACT ${user.firstName}, ${user.lastName}", user)
}
}

Take a look at the following database repositories, UserRepository and FactRepository:

interface UserRepository {
fun getUserById(id: UserId): User?
fun insertUser(user: User)
}

interface FactRepository {
fun getFactByUserId(id: UserId): Fact?
fun insertFact(fact: Fact)
}

For our repositories, we'll use JdbcTemplate of Spring 5. Spring 5 comes with support for Kotlin, including extension functions for easy and idiomatic Kotlin use (you can use JdbcTemplate in any application, it doesn't need to be a Spring one):

import org.springframework.dao.EmptyResultDataAccessException
import org.springframework.jdbc.core.JdbcTemplate

abstract class
JdbcRepository(protected val template: JdbcTemplate) {
protected fun <T> toNullable(block: () -> T): T? {
return try {
block()
} catch (_: EmptyResultDataAccessException) {
null
}
}
}

As with the clients, both repositories will have a parent class—in this case, with a function to transform, EmptyResultDataAccessException; (spring's way to indicate a non-existing record) into a nullable—idiomatic Kotlin.

Both implementations are straightforward, as follows:

import org.springframework.jdbc.core.queryForObject

class
JdbcUserRepository(template: JdbcTemplate) : JdbcRepository(template), UserRepository {
override fun getUserById(id: UserId): User? {
return toNullable {
template.queryForObject("select * from USERS where id = ?", id) { resultSet, _ ->
with(resultSet) {
User(getInt("ID"),
getString("FIRST_NAME"),
getString("LAST_NAME"),
Gender.valueOfIgnoreCase(getString("GENDER")))
}
}
}
}

override fun insertUser(user: User) {
template.update("INSERT INTO USERS VALUES (?,?,?,?)",
user.id,
user.firstName,
user.lastName,
user.gender.name)
}
}

class JdbcFactRepository(template: JdbcTemplate) : JdbcRepository(template), FactRepository {
override fun getFactByUserId(id: Int): Fact? {
return toNullable {
template.queryForObject("select * from USERS as U inner join FACTS as F on U.ID = F.USER where U.ID = ?", id) { resultSet, _ ->
with(resultSet) {
Fact(getInt(5),
getString(6),
User(getInt(1),
getString(2),
getString(3),
Gender.valueOfIgnoreCase(getString(4))))
}
}
}
}

override fun insertFact(fact: Fact) {
template.update("INSERT INTO FACTS VALUES (?,?,?)", fact.id, fact.value, fact.user?.id)
}
}

For our database, we are using the H2 in-memory database, but any database will work (you can make this application work with some different persistence mechanisms, such as NoSQL database or any cache):

fun initJdbcTemplate(): JdbcTemplate {
return JdbcTemplate(JdbcDataSource()
.apply {
setUrl("jdbc:h2:mem:facts_app;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false")
})
.apply {
execute("CREATE TABLE USERS (ID INT AUTO_INCREMENT PRIMARY KEY, FIRST_NAME VARCHAR(64) NOT NULL, LAST_NAME VARCHAR(64) NOT NULL, GENDER VARCHAR(8) NOT NULL);")
execute("CREATE TABLE FACTS (ID INT AUTO_INCREMENT PRIMARY KEY, VALUE_ TEXT NOT NULL, USER INT NOT NULL, FOREIGN KEY (USER) REFERENCES USERS(ID) ON DELETE RESTRICT)")
}
}

The function initJdbcTemplate creates JdbcTemplate with an H2 DataSource, and, once it is ready, it creates the tables inside the apply extension function. The apply extension function is useful to configure properties and call initialization code, returning the same value:

public inline fun <T> T.apply(block: T.() -> Unit): T {
block()
return this
}

As with the clients, for testing, we will use mocks:

class MockUserRepository : UserRepository {
private val users = hashMapOf<UserId, User>()

override fun getUserById(id: UserId): User? {
println("MockUserRepository.getUserById")
Thread.sleep(200)
return users[id]
}

override fun insertUser(user: User) {
println("MockUserRepository.insertUser")
Thread.sleep(200)
users[user.id] = user
}
}

class MockFactRepository : FactRepository {

private val facts = hashMapOf<UserId, Fact>()

override fun getFactByUserId(id: UserId): Fact? {
println("MockFactRepository.getFactByUserId")
Thread.sleep(200)
return facts[id]
}

override fun insertFact(fact: Fact) {
println("MockFactRepository.insertFact")
Thread.sleep(200)
facts[fact.user?.id ?: 0] = fact
}

}

With these mocks, our worst case scenario is around 1,600 milliseconds:

  • UserRepository.getUserById = 200ms ~
  • UserClient.getUser = 500ms ~
  • UserRepository = 200ms ~
  • FactClient.getFact = 500ms ~
  • FactRepository.insertRepository = 200ms ~

Now, we'll implement UserService with different styles of asynchronicity, including a synchronous implementation, our baseline.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.97.170