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

可読性から見たKotlinの言語機能 -「使いたい」の、その先へ。 / Kotlin language features and readability

可読性から見たKotlinの言語機能 -「使いたい」の、その先へ。 / Kotlin language features and readability

Kotlin language features and readability
可読性から見たKotlinの言語機能 -「使いたい」の、その先へ。

Munetoshi Ishikawa, LINE Corporation

※ この資料は以下のイベントの発表内容です
https://fortee.jp/kotlin-fest-2022/proposal/e23789e7-fc9d-4496-8646-ca5aa12399f9

LINE Developers

December 10, 2022
Tweet

More Decks by LINE Developers

Other Decks in Technology

Transcript

  1. Munetoshi Ishikawa, LINE Corporation
    Kotlin language features and readability
    Մಡੑ͔ΒݟͨKotlinͷݴޠػೳ

    -ʮ࢖͍͍ͨʯͷɺͦͷઌ΁ɻ

    View full-size slide

  2. Kotlin's language features
    Examples: null safety, data classes, extensions...

    Helps writing code

    - Removes boiler plates

    - Keeps code type-safe

    - Makes code concise and robust

    View full-size slide

  3. Drawbacks of language features
    "Feature overusing" may cause unreadable and non-robust code

    - Need to make the objective clear

    - Shouldn't mistake the means and the ends

    This talk demonstrates anti-patterns and the solutions

    View full-size slide

  4. Contents of this talk
    - Null safety

    - Scope function

    - Data class

    - Extension

    - Default argument

    - Sealed class

    View full-size slide

  5. Contents of this talk
    - Null safety

    - Scope function

    - Data class

    - Extension

    - Default argument

    - Sealed class

    View full-size slide

  6. Null safety
    Nullable/Non-null type is statically checked

    - cf. https://kotlinlang.org/docs/null-safety.html
    var nonNullInt: Int = 1
    nonNullInt = null // compile error
    Introduce 1 anti-pattern example
    var nullableInt: Int? = 1
    nullableInt + 1 // compile error

    View full-size slide

  7. Null safety: Anti-pattern example

    View full-size slide

  8. Null safety: Anti-pattern example
    Function to return indices matching a condition
    fun indicesIncludingText(list: List?, searchText: String?): Set? {
    What’s wrong with this code?
    if (searchText == null) {
    return null
    }
    return list.orEmpty().asSequence()
    .withIndex()
    .filter { (_, value) -> value.contains(searchText) }
    .map { (index, _) -> index }
    .toSet()
    }

    View full-size slide

  9. Null safety: Problem
    Inappropriate nullable parameters



    Unpredictable behavior for null argument

    - Null list: Returns empty set

    - Null searchText: Returns a null set
    fun indicesIncludingText(list: List?, searchText: String?): Set? {

    View full-size slide

  10. Null safety: Solution
    Options:

    1. Use "standard/default/initial value" instead of null

    2. Write documentation for null argument

    3. Create a null object explicitly

    View full-size slide

  11. Null safety: Solution
    Options:

    1. Use "standard/default/initial value" instead of null

    2. Write documentation for null argument

    3. Create a null object explicitly

    View full-size slide

  12. Null safety: Solution, option 1
    Use "standard/default/initial value" instead of null

    - empty(): Collection

    - "": String

    - 0: adder, size, count

    - 1: multiplier, exponent

    - MIN_VALUE: timestamp, max value selection, lower bound

    - INVALID, INITIAL: model, state

    View full-size slide

  13. Null safety: Solution, option 1 (cont.)
    Select a standard/default/initial value carefully

    Some values might not be appropriate

    - 0 for invalid UInt/ULong value (ID, age, ...)

    - -1 or size for an invalid index

    - "" for invalid UUID string


    Keep null if there is a special meaning

    View full-size slide

  14. Null safety: Solution
    Options:

    1. Use "standard/default/initial value" instead of null

    2. Write documentation for null argument

    3. Create a null object explicitly

    View full-size slide

  15. Null safety: Solution, option 2
    Write documentation for null argument
    class TextUiElement {
    fun setText(text: String?) { ... }
    }
    /**
    * Shows [text] in this UI element.
    *
    * This UI element is hidden with a null argument
    * while an empty string `""` is shown explicitly.
    */

    View full-size slide

  16. Null safety: Solution
    Options:

    1. Use "standard/default/initial value" instead of null

    2. Write documentation for null argument

    3. Create a null object explicitly

    View full-size slide

  17. Null safety: Solution, option 3
    Create a null object explicitly
    sealed class TextUiElementState {
    class Visible(text: String) : TextUiElementState()
    object Hidden : TextUiElementState()
    }
    Note: Avoid over-engineering
    class TextUiElement {
    fun setState(state: TextUiElementState) { ... }
    }

    View full-size slide

  18. Null safety: Summary

    View full-size slide

  19. Null safety: Summary
    Don't overuse null value

    - Replace with "default value"

    - Write documentation or create a null object

    View full-size slide

  20. Contents of this talk
    - Null safety

    - Scope function

    - Data class

    - Extension

    - Default argument

    - Sealed class

    View full-size slide

  21. Scope function
    Executes a given function within the context of an object

    - cf. https://kotlinlang.org/docs/scope-functions.html

    - 5 functions: let, run, with, apply, and also
    Typical purposes:

    - Nullable value handling

    - Function call grouping/chaining
    Introduce 1 anti-pattern example

    View full-size slide

  22. Scope function: Anti-pattern example

    View full-size slide

  23. Scope function: Anti-pattern example
    Cache layer for query/response
    class ResponseValueCache(...) {
    private val cachedValues: MutableMap = mutableMapOf()
    }
    What’s wrong with this code?
    fun getCacheOrQuery(queryParams: Q): V? = cachedValues[query]
    ?: queryClient.query(queryParams)
    .let {
    when (it) {
    is Response.Success -> it.value
    is Response.Error -> null
    }
    }?.also { cachedValues[query] = it }

    View full-size slide

  24. Scope function: Problem
    Scope functions just for chaining
    .let {
    when (it) {
    is Response.Success -> it.value
    is Response.Error -> null
    }
    }?.also { cachedValues[query] = it }

    View full-size slide

  25. Scope function: Problem
    Scope functions just for chaining

    - Not easy to find the receiver
    .let {
    when (it) {
    is Response.Success -> it.value
    is Response.Error -> null
    }
    }?.also { cachedValues[query] = it }

    View full-size slide

  26. Scope function: Problem
    Scope functions just for chaining

    - Not easy to find the receiver

    - The important side-effect is hidden
    .let {
    when (it) {
    is Response.Success -> it.value
    is Response.Error -> null
    }
    }?.also { cachedValues[query] = it }

    View full-size slide

  27. Scope function: Problem
    Scope functions just for chaining

    - Not easy to find the receiver

    - The important side-effect is hidden
    .let {
    when (it) {
    is Response.Success -> it.value
    is Response.Error -> null
    }
    }?.also { cachedValues[query] = it }

    View full-size slide

  28. Scope function: Solution
    Options:

    1. Simplify the chain

    2. Break the chain

    View full-size slide

  29. Scope function: Solution
    Options:

    1. Simplify the chain: Extract as private functions or extensions

    2. Break the chain
    .let {
    when (it) {
    is Response.Success -> it.value
    is Response.Error -> null
    }
    }
    queryClient.query(queryParams)

    View full-size slide

  30. Scope function: Solution
    Options:

    1. Simplify the chain: Extract as private functions or extensions

    2. Break the chain
    queryClient.query(queryParams).valueOrNull
    private val Response.valueOrNull: R?
    get() = when(this) { ... }

    View full-size slide

  31. Scope function: Solution
    Options:

    1. Simplify the chain: Extract as private functions or extensions

    2. Break the chain: Define local values
    private val Response.valueOrNull: R?
    get() = when(this) { ... }
    queryClient.query(queryParams).valueOrNull

    View full-size slide

  32. Scope function: Solution
    Options:

    1. Simplify the chain: Extract as private functions or extensions

    2. Break the chain: Define local values
    val responseValue = queryClient.query(queryParams).valueOrNull
    if (responseValue != null) {
    cachedValues[query] = responseValue
    }
    return responseValue
    private val Response.valueOrNull: R?
    get() = when(this) { ... }

    View full-size slide

  33. Scope function: Solution
    Options:

    1. Simplify the chain: Extract as private functions or extensions

    2. Break the chain: Define local values
    val responseValue = queryClient.query(queryParams).valueOrNull
    if (responseValue != null) {
    cachedValues[query] = responseValue
    }
    return responseValue
    private val Response.valueOrNull: R?
    get() = when(this) { ... }

    View full-size slide

  34. Scope function: Other anti-patterns

    View full-size slide

  35. Scope function: Other anti-patterns
    Nested scope functions




    ?.let for not important value
    receiver.apply {
    property = MutableValue().apply { ... }
    anotherProperty = MutableValue().apply { ... }
    }
    option?.let { repository.store(data, it) }
    if (option != null) {
    repository.store(data, option)
    }

    View full-size slide

  36. Scope function: Summary

    View full-size slide

  37. Scope function: Summary
    Don't use scope functions just to make a chain

    - Make the objective of the chain clear

    - Simplify or break the chain if possible

    View full-size slide

  38. Contents of this talk
    - Null safety

    - Scope function

    - Data class

    - Extension

    - Default argument

    - Sealed class

    View full-size slide

  39. Data class
    A class to represent a plain product data type

    - cf. https://kotlinlang.org/docs/data-classes.html

    - Some functions (equals, toString, ...) are automatically derived

    Similar to case class, record, and tuple at other languages
    Introduce 2 anti-pattern examples

    View full-size slide

  40. Data class 1/2: Anti-pattern example

    View full-size slide

  41. Data class 1/2: Anti-pattern example
    A data class with

    - A readonly property of a mutable object

    - A non read-only property of an immutable object
    data class FooModel(
    val mutableElement: MutableElement,
    var immutableElement: ImmutableElement
    )
    What’s wrong with this code?

    View full-size slide

  42. Data class 1/2: Problem
    Multiple ways to update the properties

    - Create a new instance

    - Modify a property directly




    Data class can be shared and have a long lifecycle

    - The affected scope of modification is ambiguous
    fooModel.immutableElement = ImmutableElement(0)
    fooModel.copy(immutableElement = ImmutableElement(0))

    View full-size slide

  43. Data class 1/2: Solution
    Make data classes immutable

    - Use read-only variables

    - Use immutable property type instances

    data class FooModel(val immutableElement: ImmutableElement)

    View full-size slide

  44. Data class 2/2: Anti-pattern example

    View full-size slide

  45. Data class 2/2: Anti-pattern example
    A data class of UI layout attributes
    data class FooLayoutParameters(
    val titleText: String,
    val backgroundColorInt: Int,
    val buttonText: String,
    val onButtonClickListener: () -> Unit
    )
    What’s wrong with this code?

    View full-size slide

  46. Data class 2/2: Problem
    Ambiguous equivalency

    data class FooLayoutParameters(
    val titleText: String,
    val backgroundColorInt: Int,
    val buttonText: String,
    val onButtonClickListener: () -> Unit
    )

    View full-size slide

  47. Data class 2/2: Problem
    Ambiguous equivalency

    // false
    FooLayoutParameters("", 0, "") { privateFunction() } ==
    FooLayoutParameters("", 0, "") { privateFunction() }
    // true
    FooLayoutParameters("", 0, "", ::privateFunction) ==
    FooLayoutParameters("", 0, "", ::privateFunction)
    // Java code
    // false
    new FooLayoutAttributes("", 0, "", JavaClass::staticMethod).equals(
    new FooLayoutAttributes("", 0, "", JavaClass::staticMethod)
    )

    View full-size slide

  48. Data class 2/2: Solution
    Confirm every property is data class friendly

    If not...

    - Use non-data class

    - Split properties into two classes
    class FooLayoutAttributes(
    val titleText: String,
    val backgroundColorInt: Int,
    val buttonText: String,
    val onButtonClickListener: () -> Unit
    )

    View full-size slide

  49. Data class: Other anti-patterns

    View full-size slide

  50. Data class: Other anti-patterns
    Non-product data model




    Special case: Should be replaced with a sealed class
    // An invalid instance is easily created by `copy`.
    data class CoinState(val coinCount: Int, val coinText: String) {
    constructor(coinCount: Int): this(coinCount, "Coins: $coinCount")
    }
    data class Response(val result: R? = null, val error: E? = null)

    View full-size slide

  51. Data class: Summary

    View full-size slide

  52. Data class: Summary
    Confirm whether a data class behaves as a plain product data type

    - Immutability

    - Equivalency

    View full-size slide

  53. Contents of this talk
    - Null safety

    - Scope function

    - Data class

    - Extension

    - Default argument

    - Sealed class

    View full-size slide

  54. Extension
    Defines a function of an existing receiver type

    - Adds a method without inheriting or decorator

    - cf. https://kotlinlang.org/docs/extensions.html
    Introduce 3 anti-pattern examples

    View full-size slide

  55. Extension 1/3: Anti-pattern example

    View full-size slide

  56. Extension 1/3: Anti-pattern example
    Parses the receiver and creates a UserModel instance

    - At the top-level of a Kotlin file
    fun String.toUserModel(): UserModel? {
    val userId = ... ?: return null
    val userName = ... ?: return null
    ...
    return UserModel(userId, userName, ...)
    }
    What’s wrong with this code?

    View full-size slide

  57. Extension 1/3: Problem
    Feature specific public top-level extension

    - Causes too many IDE suggestions

    View full-size slide

  58. Extension 1/3: Solution
    Avoid "public" or "extension" for feature specific code

    - Use internal or private

    - Define as a normal function in an object


    Confirm if it's natural to define the method in the receiver class
    class String: ... {
    fun toFeatureSpecificModel(): FeatureSpecificModel = ...

    View full-size slide

  59. Extension 2/3: Anti-pattern example

    View full-size slide

  60. Extension 2/3: Anti-pattern example
    An extension which modifies an instance member

    open class FooClass {
    private val registeredElements: MutableSet = mutableSetOf()
    protected fun List.register() {
    registeredElements += this
    }
    }
    What’s wrong with this code?

    View full-size slide

  61. Extension 2/3: Problem
    The implicit receiver hides modification

    register modifies the parent property
    class BarClass(...): FooClass() {
    fun someFunction() {
    ...
    someDataList.register()
    }
    }

    View full-size slide

  62. Extension 2/3: Root cause
    How do we guess a function behavior

    1. Has an important return value: No modification

    2. Has a receiver: Modify the receiver

    3. Has a parameter: Modify the parameter


    someDataList.register() looks modifying the receiver

    View full-size slide

  63. Extension 2/3: Solution
    Remove implicit receiver modification

    - An instance method is enough
    open class FooClass {
    protected fun register(list: Collection) {
    registeredElements += list
    }
    }
    ...
    val listToRegister = dataList.map...filter...
    register(listToRegister)
    - A receiver-less function implies that this can be the receiver

    View full-size slide

  64. Extension 3/3: Anti-pattern example

    View full-size slide

  65. Extension 3/3: Anti-pattern example
    Background:

    - Observable instance is observed while ObserverScope is alive

    interface Observable {
    /**
    * Calls `action` when this instance is updated and `scope` is alive...
    */
    fun observe(scope: ObserverScope, action: (T) -> Unit)
    }

    View full-size slide

  66. Extension 3/3: Anti-pattern example (cont.)
    An extension allows to pass ObserverScope as the receiver
    fun ObserverScope.observe(observable: Observable, action: (T) -> Unit) =
    observable.observe(this, action)
    What’s wrong with this code?
    class FooObserver(private val observerScope: ObserverScope) {
    init {
    ...
    observerScope.observe(observable) { /* do something */ }
    observerScope.observe(anotherObservable) { /* do another thing */ }

    View full-size slide

  67. Extension 3/3: Problem
    Non obvious prioritization of function calls

    - Example: A class inheriting both ObserverScope and Observable
    class BadClass: ObserverScope(), Observable { ... }
    fun function(badInstance1: BadClass, badInstance2: BadClass) {
    // As `Observable.observe`
    badInstance1.observe(badInstance2) { ... }
    // As `ObserverScope.observe`
    (badInstance1 as ObserverScope).observe(badInstance2) { ... }

    View full-size slide

  68. Extension 3/3: Root cause
    An extension is similar to an overloading method


    "Overloaded functions flipping the parameter order" is confusing
    fun observe(scope: ObserverScope, observable: Observable) { ... }
    fun observe(observable: Observable, scope: ObserverScope) { ... }

    View full-size slide

  69. Extension 3/3: Solution
    Options:

    1. Remove extensions flipping the receiver and the parameter

    2. Give different names for such extensions
    fun ObserverScope.observe(observable: Observable) { ... }
    fun Observable.observedDuring(scope: ObserverScope) { ... }

    View full-size slide

  70. Extension: Other anti-patterns

    View full-size slide

  71. Extension: Other anti-patterns
    With inheritance 

    (cf., https://kotlinlang.org/docs/extensions.html#extensions-are-resolved-statically)




    With down-casting for non-sealed class
    fun Base.getName() = "Base"
    fun Derived.getName() = "Derived"
    val base: Base = Derived()
    base.getName() // "Base"
    val RpcFrameworkException.errorCode: ErrorCode
    get() = when (this) {
    is FooException -> fooErrorCode
    is BarException -> barErrorCode
    else -> ErrorCode.UNKNOWN
    }

    View full-size slide

  72. Extension: Summary

    View full-size slide

  73. Extension: Summary
    Extension behavior should look as natural as a normal function

    - For feature specific logic

    - For the receiver

    - For sub-typing

    View full-size slide

  74. Contents of this talk
    - Null safety

    - Scope function

    - Data class

    - Extension

    - Default argument

    - Sealed class

    View full-size slide

  75. Default argument
    Allows skipping arguments without boiler plates for overloading

    - cf. https://kotlinlang.org/docs/functions.html#default-arguments
    Introduce 2 anti-pattern examples

    View full-size slide

  76. Default argument 1/2: Anti-pattern example

    View full-size slide

  77. Default argument 1/2: Anti-pattern example
    An extension filtering even/odd numbers
    What’s wrong with this code?
    fun List.filterParity(takesEven: Boolean = false): List {
    val expectedRemainder = if (takesEven) 0 else 1
    return filter { it % 2 == expectedRemainder }
    }
    - true: Returns even elements

    - false: Returns odd elements

    View full-size slide

  78. Default argument 1/2: Problem
    The default value is not predictable on the caller side



    Some booleans might be predictable

    - View.isEnabled: true?

    - CheckBox.isChecked: false?

    - isEven: ???
    listOf(1, 2, 3, 4).filterParity() // even or odd?

    View full-size slide

  79. Default argument 1/2: Solution
    Use well-known or predictable default values

    - 0, 1, MIN_VALUE, empty(), null, null object

    - "Initial value" can be a hint


    Simply, remove default argument

    View full-size slide

  80. Default argument 2/2: Anti-pattern example

    View full-size slide

  81. Default argument 2/2: Anti-pattern example
    Changing the logic depending on which parameter is given
    fun setBackground(colorInt: Int? = null, themeType: ThemeType? = null) {
    when {
    colorInt != null -> ...
    themeType != null -> ...
    }
    }
    What’s wrong with this code?
    view.setBackground(colorInt = Color.WHITE)
    view.setBackground(themeType = ThemeType.PRIMARY)

    View full-size slide

  82. Default argument 2/2: Problem
    The number of arguments might not be one

    - The behavior is not predictable
    // This may apply to both arguments.
    // Or, one of the arguments takes the priority.
    view.setColor(colorInt = Color.WHITE, themeType = ThemeType.PRIMARY)
    // This may do nothing, or may throw an exception.
    view.setColor()

    View full-size slide

  83. Default argument 2/2: Solution
    Options:

    1. Split functions

    2. Implement a function for type conversion

    3. Define a sealed class of the parameter

    View full-size slide

  84. Default argument 2/2: Solution
    Options:

    1. Split functions

    2. Implement a function for type conversion

    3. Define a sealed class of the parameter
    fun setBackgroundColor(colorInt: Int) { ... }
    fun setBackgroundTheme(themeType: ThemeType) { ... }

    View full-size slide

  85. Default argument 2/2: Solution
    Options:

    1. Split functions

    2. Implement a function for type conversion

    3. Define a sealed class of the parameter
    fun setBackgroundColor(colorInt: Int) { ... }
    enum class ThemeType {
    ...
    fun toColorInt(): Int { ... }
    }

    View full-size slide

  86. Default argument 2/2: Solution
    Options:

    1. Split functions

    2. Implement a function for type conversion

    3. Define a sealed class of the parameter
    fun setBackground(model: BackgroundModel) { ... }
    sealed class BackgroundModel {
    class Color(val colorInt: Int) : BackgroundModel()
    class Theme(val themeType: ThemeType) : BackgroundModel()
    }

    View full-size slide

  87. Default argument: Other anti-patterns

    View full-size slide

  88. Default argument: Other anti-patterns
    With vararg






    Pass the child class constructor parameter
    fun getGeneralizedMean(vararg values: Double, exponent: Double = 1.0): Double =
    ...
    // Executed as getGeneralizedMean(2.0, 4.0, 1.0, exponent = 1.0)
    // instead of getGeneralizedMean(2.0, 4.0, exponent = 1.0)
    getGeneralizedMean(*doubleArrayOf(2.0, 4.0), 1.0)
    abstract class Layout(themeType: ThemeType = ThemeType.LIGHT)
    class FooLayout(themeType: ThemeType): Layout(themeType)

    View full-size slide

  89. Default argument: Summary

    View full-size slide

  90. Default argument: Summary
    Use default argument just for "default"

    - Default value must be predictable

    - Don't use for any other purpose

    - Consider to remove the default argument

    View full-size slide

  91. Contents of this talk
    - Null safety

    - Scope function

    - Data class

    - Extension

    - Default argument

    - Sealed class

    View full-size slide

  92. Sealed class
    A class cannot be overridden externally

    - cf. https://kotlinlang.org/docs/functions.html#default-arguments

    - Used to represent a sum data type, basically
    Introduce 2 anti-pattern examples

    View full-size slide

  93. Sealed class 1/2: Anti-pattern example

    View full-size slide

  94. Sealed class 1/2: Anti-pattern example
    A sealed class Color with child data classes: Rgb, Cmy, and Hsl
    sealed class Color {
    data class Rgb(val red: UByte, val green: UByte, val blue: UByte) : Color()
    data class Cmy(val cyan: UByte, val magenta: UByte, val yellow: UByte) :
    Color()
    data class Hsl(val hue: AngleInt, val saturation: PercentInt, ...) :
    Color()
    }
    What’s wrong with this code?

    View full-size slide

  95. Sealed class 1/2: Problem
    Ambiguity on equivalency
    val RED_1: Color = Color.Rgb(red = 255u, green = 0u, blue = 0u)
    val RED_2: Color = Color.Cmy(cyan = 0u, magenta = 255u, yellow = 255u)
    RED_1 == RED_2 // Can be false!

    View full-size slide

  96. Sealed class 1/2: Solution 1
    Choose a "main" unit

    - Create constructors/factories to convert to the "main" unit
    data class Color(val red: UByte, val green: UByte, val blue: UByte) {
    companion object {
    fun fromCmy(cyan: UByte, magenta: UByte, yellow: UByte) : Color =
    Color((UByte.MAX_VALUE - cyan).toUByte(), ...)
    fun fromHsl(hue: AngleInt, ...) : Color = Color(...)

    View full-size slide

  97. Sealed class 1/2: Note of solution 1
    Resolution and value domain may differ

    - Rounding error

    - HSL color has "hue" even for black or white
    val redHue = AngleInt.of(0)
    val blackColor = Color.fromHsl(
    hue = redHue,
    saturation = PercentInt.MIN,
    lightness = PercentInt.MIN
    )
    blackColor.hue != redHue // May be true

    View full-size slide

  98. Sealed class 1/2: Solution 2
    Implement equals function for every child class combination


    Can be too complex

    - Number of combinations: (n2) / 2

    - hashCode must be implemented too

    View full-size slide

  99. Sealed class 1/2: Solution 3
    Define as "representation" instead of "value"

    - Consider to use non-data class

    sealed class ColorRepresentation {
    class Rgb(val red: UByte, ...) : ColorRepresentation()
    class Cmy(val cyan: UByte, ...) : ColorRepresentation()
    class Hsl(val hue: AngleInt, ...) : ColorRepresentation()
    }

    View full-size slide

  100. Sealed class 2/2: Anti-pattern example

    View full-size slide

  101. Sealed class 2/2: Anti-pattern example
    Data model of "comment" for a specific location

    sealed class GeoLocationComment {
    }
    What's wrong with this code?
    class AtLatLon(
    val commentText: String,
    val latitude: Long,
    val longitude: Long
    ) : GeoLocationComment()
    class AtPlace(
    val commentText: String,
    val placeId: Long
    ) : GeoLocationComment()

    View full-size slide

  102. Sealed class 2/2: Problem
    Unnecessary downcast

    - Required to take a common property
    val commentText = when(locationComment) {
    is GeoLocationComment.AtLatLon -> locationComment.commentText
    is GeoLocationComment.AtPlace -> locationComment.commentText
    }

    View full-size slide

  103. Sealed class 2/2: Solution?
    Extract the common property to the parent

    sealed class GeoLocationComment {
    class AtLatLon(
    val commentText: String,
    val latitude: Long,
    val longitude: Long
    ) : GeoLocationComment()
    class AtPlace(
    val commentText: String,
    val placeId: Long
    ) : GeoLocationComment()
    }

    View full-size slide

  104. Sealed class 2/2: Solution?
    Extract the common property to the parent

    sealed class GeoLocationComment {
    class AtLatLon(
    commentText: String,
    val latitude: Long,
    val longitude: Long
    ) : GeoLocationComment()
    class AtPlace(
    commentText: String,
    val placeId: Long
    ) : GeoLocationComment()
    }

    View full-size slide

  105. Sealed class 2/2: Solution?
    Extract the common property to the parent

    sealed class GeoLocationComment(val commentText: String) {
    class AtLatLon(
    commentText: String,
    val latitude: Long,
    val longitude: Long
    ) : GeoLocationComment(commentText)
    class AtPlace(
    commentText: String,
    val placeId: Long
    ) : GeoLocationComment(commentText)
    }

    View full-size slide

  106. Sealed class 2/2: Solution?
    Extract the common property to the parent

    sealed class GeoLocationComment(val commentText: String) {
    class AtLatLon(
    commentText: String,
    val latitude: Long,
    val longitude: Long
    ) : GeoLocationComment(commentText)
    class AtPlace(
    commentText: String,
    val placeId: Long
    ) : GeoLocationComment(commentText)
    }

    View full-size slide

  107. Sealed class 2/2: Other problems
    1. Need to update all the children to add a new property

    2. open val is required to make a data class

    View full-size slide

  108. Sealed class 2/2: Other problems
    1. Need to update all the children to add a new property

    2. open val is required to make a data class

    sealed class GeoLocationComment(
    val commentText: String,
    val commentAuthorId: Long
    ) {
    class AtLatLon(
    commentText: String,
    commentAuthorId: Long,
    ...
    ) : GeoLocationComment(commentText, commentAuthorId)
    ...

    View full-size slide

  109. Sealed class 2/2: Other problems
    1. Need to update all the children to add a new property

    2. open val is required to make a data class

    sealed class GeoLocationComment(
    open val commentText: String,
    open val commentAuthorId: Long
    ) {
    data lass AtLatLon(
    override val commentText: String,
    override val commentAuthorId: Long,
    ...
    ) : GeoLocationComment(commentText, commentAuthorId)
    ...

    View full-size slide

  110. Sealed class 2/2: Solution
    Minimize the sealed class responsibility

    - Split the sealed class of location type from GeoLocationComment
    sealed class GeoLocation {
    class LatLon(val latitude: Long, val longitude: Long) : GeoLocation()
    class Place(val placeId: Long) : GeoLocation()
    }
    class GeoLocationComment(
    val commentText: String,
    val location: GeoLocation
    )

    View full-size slide

  111. Sealed class: Summary

    View full-size slide

  112. Sealed class: Summary
    Keep sealed class responsibility minimized

    - Make unrelated properties accessible without downcast

    - Try to split if there is a "common" property

    View full-size slide

  113. Contents of this talk
    - Null safety

    - Scope function

    - Data class

    - Extension

    - Default argument

    - Sealed class
    Sealed class

    View full-size slide

  114. Aside
    1. How to share the knowledge

    2. Non-Kotlin specific topic

    3. Something is missing

    View full-size slide

  115. Aside
    1. How to share the knowledge








    2. Non-Kotlin specific topic

    3. Something is missing
    Watch a presentation at DroidKaigi 2021


    ௕͘ੜ͖Δίʔυϕʔεͷʮ඼࣭ʯ໰୊ʹ޲͖߹͏

    https://droidkaigi.jp/2021/timetable/276957/?day=2

    View full-size slide

  116. Aside
    1. How to share the knowledge

    2. Non-Kotlin specific topic








    3. Something is missing
    Presentation "code readability"ɻ

    https://speakerdeck.com/munetoshi/code-readability
    Book "ಡΈ΍͍͢ίʔυͷΨΠυϥΠϯ

    -࣋ଓՄೳͳιϑτ΢ΣΞ։ൃͷͨΊʹ"

    https://gihyo.jp/book/2022/978-4-297-13036-7

    View full-size slide

  117. Aside
    1. How to share the knowledge

    2. Non-Kotlin specific topic

    3. Something is missing


    View full-size slide

  118. Aside
    1. How to share the knowledge

    2. Non-Kotlin specific topic

    3. Something is missing


    View full-size slide

  119. Aside
    1. How to share the knowledge

    2. Non-Kotlin specific topic

    3. Something is missing


    Coroutines?

    Listen to the next presentation!


    View full-size slide

  120. Summary
    Kotlin's language feature is convenient, but needs attentions

    - Make the objective clear

    - Don't mistake the means and the ends

    Introduced anti-patterns and the solutions

    - For null safety, scope functions, data classes, extensions...

    View full-size slide