Upgrade to Pro — share decks privately, control downloads, hide ads and more …

From Java 8 to Kotlin: a series of small bang conversions

From Java 8 to Kotlin: a series of small bang conversions

We describe how we converted a line of business application from Kotlin to Java, gradually, without affecting delivery of new features. We'll talk about what we found surprising or confusing going from Java to Kotlin. And we'll show some embarrassingly bad Kotlin code we wrote as we learned the "grain" of the language.

Nat Pryce

May 04, 2017
Tweet

More Decks by Nat Pryce

Other Decks in Programming

Transcript

  1. From Java 8 to Kotlin A series of small bang

    conversions Nat Pryce [email protected] | @natpryce | github.com/npryce | speakerdeck.com/npryce
  2. @

  3. Coming up... What we did Overenthusiasm Culture Shock You can

    gradually convert Java to Kotlin without slowing delivery. We went too far so that you don't have to. Kotlin is a foreign country; they do things differently there.
  4. Code layout and organisation The Kotlin style guide is not

    very extensive It seems trivial, but inconsistent conventions became annoying • Code layout • Code organisation • Visibility modifiers Our project developed its own conventions … eventually.
  5. It took us time to accept using null data class

    SubmissionDraft( override val id: DraftId, override val submissionStatus: SubmissionStatus, override val createdTimestamp: Instant, override val submittedTimestamp: Instant? = null, override val submissionId: SubmissionId?, override val decisions: UserDecisions )
  6. Extension functions on nullable types "Elvis has left the building!"

    fun String?.orEmpty() = this ?: "" fun <T> List<T>?.orEmpty() = this ?: emptyList() fun T?.discardIf(p: (T)->Boolean) = this?.let { if (p(it)) null else this } But beware… val maybeCount: Int? = … maybeCount?.toString() // returns null or the count maybeCount.toString() // returns "null" or the count The type checker won't help: String is a subtype of String? Impossible to spot the difference in test diagnostics!
  7. Exceptions are not type checked! sealed class Result<out T> data

    class Failure(val value: ErrorCode) : Result<Nothing>() data class Success<out T>(val value: T) : Result<T>() inline fun <T, U> Result<T>.map(f: (T) -> U): Result<U> = ... inline fun <T, U> Result<T>.flatMap(f: ( T) -> Result<U>): Result<U> = ... inline fun <T> Result<T>.forEach(f: (T) -> Unit): Unit = ... inline fun <T> Result<T>.orElse(f: (ErrorCode) -> T) = ... inline fun <T> T?.asResultOr(errorCodeFn: () -> ErrorCode): Result< T> = ... ... etc.
  8. Exceptions are not type checked! Technica l error? Further action?

    Catch as close as possible to failing call. Return error code in Result<T> Translate to HTTP response in the web layer Let the exception propagate – the web layer will send a 500 response Yes No Yes No
  9. Result<T> instead of exceptions fun sendToEjp(draft: SubmissionDraft): Result<SubmissionId> = journals.getByPCode(draft.

    pCode) .flatMap { journal -> journal.ejpJournalId .asResultOr { PeerReviewSystemNotSupported() } .flatMap { ejpJournalId -> payloadConverter.convertForCreate(draft) .flatMap { requestJson -> val uri = EjpUrlRoutes. create.path(ejpJournalId) sendCreate(uri, payload) .flatMap { responseJson -> responseJson.toSubmissionId() } } } } Type safe, but...
  10. Accept early returns in functional code inline fun <T> Result<T>.onFailure(handler:

    (Failure) -> Nothing): T = when (this) { is Failure -> handler( this) is Success -> value } fun create(draft: SubmissionDraft): Result<EjpSubmissionId> { val journal = journals.getByPCode(draft. pCode) ?: return Failure(JournalNotFound(draft. pCode)) val journalId = journal. ejpJournalId ?: return Failure(PeerReviewSystemMisconfiguration()) val ejpPayload = ejpPayloadConverter.convertForCreate(draft) .onFailure { return it } val uri = EjpUrlRoutes. createPath.path(journalId) val responseJson = sendCreate(uri, payload) .onFailure { return it } return responseJson.toSubmissionId() }
  11. Infixation val config = EnvironmentVariables overriding ConfigurationProperties.fromResource("default.conf") infix fun Configuration.overriding(defaults:

    Configuration) = Override(this, defaults) (n) An unhealthy obsession with infix functions. fun Author.toJson(): JsonNode = obj( "name" of name, "affiliation" of affiliation.toJson(), "email" of emailAddress ) Unnecessary (but published, so can't be changed without breaking code) Useful
  12. Operator punning Prefer conventional operators or clearly named functions val

    submissionId = PathElement(::SubmissionId) val fileId = PathElement(::FileId) val submissionPath = "submission"/submissionId val submissionFilePath = submissionPath/"file"/fileId WTF?!?
  13. Spaces in Identifiers private fun String.`fromCamelCaseTo space separated`() = this.replace(wordBoundaryRegex,

    "$1 $2") private val wordBoundaryRegex = Regex("(\\p{Ll})(\\p{Lu})") @Test fun `an author cannot edit another author's submission`() … Too jokey & makes client code hard to read Ok – test names act as documentation in IDE views & no client code
  14. The beginning... [email protected] | @natpryce | github.com/npryce | speakerdeck.com/npryce (who

    are hiring in London & Berlin) https://www.springernature.com/gp/group/careers Thanks to