Twitter LinkedIn Github

JetBrains

Take this code

fun Route.session() {
    get("/sessions/{date}") {
        val sessionData = SessionData()
        val date = if (call.parameters["date"] != null) {
            try {
                DateTime.parse(call.parameters["date"], DateTimeFormat.forPattern("dd-MM-yy"))
            } catch (e: Exception) {
                call.respond(HttpStatusCode.BadRequest)
            }
        } else {
            DateTime.now()
        }
        call.respond(sessionData.sessionsByDay(date))
    }

    get("/sessions/current/{date}") {
        val sessionData = SessionData()
        val date = if (call.parameters["date"] != null) {
            DateTime.parse(call.parameters["date"], DateTimeFormat.forPattern("dd-MM-yy-hh-mm"))
        } else {
            DateTime.now()
        }
        call.respond(sessionData.currentSessions(date))
    }
}

Lot of duplicate code there. Let’s extract the date parsing into a new function

fun parseDate(value: String?, format: String): Date? {
    return if (value != null) {
        return try {
            DateTime.parse(value, DateTimeFormat.forPattern(format))
        } catch (e: Exception) {
            null
        }
    } else {
        DateTime.now()
    }
}

Hmm, don’t like nulls. Let’s use a sealed class to represent an Algebraic Data Type

sealed class DateTimeParse {
    class Success(val dateTime: DateTime) : DateTimeParse()
    class Failure(val message: String) : DateTimeParse()
}

fun parseDate(value: String?, format: String): DateTimeParse {
    return if (value != null) {
        return try {
            DateTimeParse.Success(DateTime.parse(value, DateTimeFormat.forPattern(format)))
        } catch (e: Exception) {
            DateTimeParse.Failure("Failed to parse date")
        }
    } else {
        DateTimeParse.Success(DateTime.now())
    }
}

Now let’s use this code

fun Route.session() {
    get("/sessions/{date}") {
        val sessionData = SessionData()
        val dateParsedResult = parseDate(call.parameters["date"], "dd-MM-yy")
        when (dateParsedResult) {
            is DateTimeParse.Success -> call.respond(sessionData.sessionsByDay(dateParsedResult.dateTime))
            is DateTimeParse.Failure -> call.respondText(text = dateParsedResult.message, status = HttpStatusCode.BadRequest)
        }
    }

    get("/sessions/current/{date}") {
        val sessionData = SessionData()
        val dateParsedResult = parseDate(call.parameters["date"], "dd-MM-yy-hh-mm")
        when (dateParsedResult) {
            is DateTimeParse.Success -> call.respond(sessionData.currentSessions(dateParsedResult.dateTime))
            is DateTimeParse.Failure -> call.respondText(text = dateParsedResult.message, status = HttpStatusCode.BadRequest)
        }
    }
}

We’ve introduced new duplication. Let’s refactor

sealed class DateTimeParse {
    class Success(val dateTime: DateTime) : DateTimeParse()
    class Failure(val message: String) : DateTimeParse()
}

fun parseDate(value: String?, format: String): DateTimeParse {
    return if (value != null) {
        return try {
            DateTimeParse.Success(DateTime.parse(value, DateTimeFormat.forPattern(format)))
        } catch (e: Exception) {
            DateTimeParse.Failure("Failed to parse date")
        }
    } else {
        DateTimeParse.Success(DateTime.now())
    }
}

fun Route.session() {
    get("/sessions/{date}") {
        processSessionRequest(::sessionsByDay, "dd-MM-yy")
    }

    get("/sessions/current/{date}") {
        processSessionRequest(::currentSessions, "dd-MM-yy-hh-mm")
    }
}

private suspend fun PipelineContext<Unit>.processSessionRequest(f: (DateTime) -> List<Session>, dateFormat: String) {
    val dateParsedResult = parseDate(call.parameters["date"], dateFormat)
    when (dateParsedResult) {
        is DateTimeParse.Success -> call.respond(f(dateParsedResult.dateTime))
        is DateTimeParse.Failure -> call.respondText(text = dateParsedResult.message, status = HttpStatusCode.BadRequest)
    }
}

This is the default refactoring that IntelliJ IDEA offers when we select

val sessionData = SessionData()
val dateParsedResult = parseDate(call.parameters["date"], "dd-MM-yy-hh-mm")
when (dateParsedResult) {
    is DateTimeParse.Success -> call.respond(sessionData.currentSessions(dateParsedResult.dateTime))
    is DateTimeParse.Failure -> call.respondText(text = dateParsedResult.message, status = HttpStatusCode.BadRequest)
}

and tell it to extract function.


The only “manual” refactoring is that sessionsByDay and currentSessions are now top-level functions. They no longer belong to a class SessionData. Given this class had no state, only behaviour, why class?


But something just isn’t right. It’s introduced a class that really has no place in my code, i.e. PipelineContext.


Let’s leverage Kotlin’s extension functions and make processSessionRequest an extension function to call

suspend fun ApplicationCall.respondSessionRequest(f: (DateTime) -> List<Session>, dateFormat: String) {
    val dateParsedResult = parseDate(parameters["date"], dateFormat)
    when (dateParsedResult) {
        is DateTimeParse.Success -> respond(f(dateParsedResult.dateTime))
        is DateTimeParse.Failure -> respondText(text =  dateParsedResult.message, status = HttpStatusCode.BadRequest)
    }

}

The result

sealed class DateTimeParse {
    class Success(val dateTime: DateTime) : DateTimeParse()
    class Failure(val message: String) : DateTimeParse()
}

fun parseDate(value: String?, format: String): DateTimeParse {
    return if (value != null) {
        return try {
            DateTimeParse.Success(DateTime.parse(value, DateTimeFormat.forPattern(format)))
        } catch (e: Exception) {
            DateTimeParse.Failure("Failed to parse date")
        }
    } else {
        DateTimeParse.Success(DateTime.now())
    }
}

fun Route.session() {
    get("/sessions/{date}") {
        call.respondSessionRequest(::sessionsByDay, "dd-MM-yy")
    }

    get("/sessions/current/{date}") {
        call.respondSessionRequest(::currentSessions, "dd-MM-yy-hh-mm")
    }
}


suspend fun ApplicationCall.respondSessionRequest(f: (DateTime) -> List<Session>, dateFormat: String) {
    val dateParsedResult = parseDate(parameters["date"], dateFormat)
    when (dateParsedResult) {
        is DateTimeParse.Success -> respond(f(dateParsedResult.dateTime))
        is DateTimeParse.Failure -> respondText(text =  dateParsedResult.message, status = HttpStatusCode.BadRequest)
    }

}

(Note: suspend is necessary since we’re inside coroutines)


In summary, we’ve introduced a few new types, added some more code, but in turn we’ve removed all duplication and as a consequence, any changes or updates will only be done once.


What are your thoughts? Worth it or not?