for null argument - Null list: Returns empty set - Null searchText: Returns a null set fun indicesIncludingText(list: List<String>?, searchText: String?): Set<Int>? {
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
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. */
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
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 }
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 }
private functions or extensions 2. Break the chain .let { when (it) { is Response.Success -> it.value is Response.Error -> null } } queryClient.query(queryParams)
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
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?
- 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))
layout attributes data class FooLayoutParameters( val titleText: String, val backgroundColorInt: Int, val buttonText: String, val onButtonClickListener: () -> Unit ) What’s wrong with this code?
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 )
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<R, E>(val result: R? = null, val error: E? = null)
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?
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 = ...
member open class FooClass { private val registeredElements: MutableSet<Element> = mutableSetOf() protected fun List<Element>.register() { registeredElements += this } } What’s wrong with this code?
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
method is enough open class FooClass { protected fun register(list: Collection<Element>) { registeredElements += list } } ... val listToRegister = dataList.map...filter... register(listToRegister) - A receiver-less function implies that this can be the receiver
while ObserverScope is alive interface Observable<T> { /** * Calls `action` when this instance is updated and `scope` is alive... */ fun observe(scope: ObserverScope, action: (T) -> Unit) }
ObserverScope as the receiver fun <T> ObserverScope.observe(observable: Observable<T>, 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 */ }
Example: A class inheriting both ObserverScope and Observable class BadClass: ObserverScope(), Observable<Int> { ... } fun function(badInstance1: BadClass, badInstance2: BadClass) { // As `Observable.observe` badInstance1.observe(badInstance2) { ... } // As `ObserverScope.observe` (badInstance1 as ObserverScope).observe(badInstance2) { ... }
and the parameter 2. Give different names for such extensions fun <T> ObserverScope.observe(observable: Observable<T>) { ... } fun <T> Observable<T>.observedDuring(scope: ObserverScope) { ... }
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 }
What’s wrong with this code? fun List<Int>.filterParity(takesEven: Boolean = false): List<Int> { val expectedRemainder = if (takesEven) 0 else 1 return filter { it % 2 == expectedRemainder } } - true: Returns even elements - false: Returns odd elements
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?
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()
a function for type conversion 3. Define a sealed class of the parameter fun setBackgroundColor(colorInt: Int) { ... } fun setBackgroundTheme(themeType: ThemeType) { ... }
a function for type conversion 3. Define a sealed class of the parameter fun setBackgroundColor(colorInt: Int) { ... } enum class ThemeType { ... fun toColorInt(): Int { ... } }
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() }
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?
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(...)
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
"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() }
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()
a common property val commentText = when(locationComment) { is GeoLocationComment.AtLatLon -> locationComment.commentText is GeoLocationComment.AtPlace -> locationComment.commentText }
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() }
parent sealed class GeoLocationComment { class AtLatLon( commentText: String, val latitude: Long, val longitude: Long ) : GeoLocationComment() class AtPlace( commentText: String, val placeId: Long ) : GeoLocationComment() }
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) }
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) }
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) ...
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) ...
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 )
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...