안드로이드 개발자 노트
[이펙티브 코틀린] Item39. 태그 클래스보다는 클래스 계층을 사용하라 본문
태그를 포함한 클래스를 태그 클래스(tagged class)라고 부릅니다.
상수(constant) '모드'를 태그라고 부르며, 태그 클래스는 서로 다른 책임을 한 클래스에 태그로 구분해서 넣는다는 문제를 내포합니다.
예를 들어 테스트에 사용되는 클래스로서 어떤 값이 기준에 만족하는지 확인하기 위해 사용되는 클래스를 살펴보겠습니다.
class ValueMatcher<T> private constructor(
private val value: T? = null,
private val matcher: Matcher
) {
fun match(value: T?) = when(matcher) {
Matcher.EQUAL -> value == this.value
Matcher.NOT_EQUAL -> value != this.value
Matcher.LIST_EMPTY -> value is List<*> && value.isEmpty()
Matcher.LIST_NOT_EMPTY -> value is List<*> && value.isNotEmpty()
}
enum class Matcher {
EQUAL,
NOT_EQUAL,
LIST_EMPTY,
LIST_NOT_EMPTY
}
companion object {
fun <T> equal(value: T) =
ValueMatcher<T>(value = value, matcher = Matcher.EQUAL)
fun <T> notEqual(value: T) =
ValueMatcher<T>(value = value, matcher = Matcher.NOT_EQUAL)
fun <T> emptyList() =
ValueMatcher<T>(matcher = Matcher.LIST_EMPTY)
fun <T> notEmptyList() =
ValueMatcher<T>(matcher = Matcher.LIST_NOT_EMPTY)
}
}
이러한 접근 방법에는 굉장히 많은 단점이 있습니다.
- 한 클래스에 여러 모드를 처리하기 위한 상용구(boilerplate)가 추가된다.
- 여러 목적으로 사용해야 하므로 프로퍼티가 일반적이지 않게 사용될 수 있으며, 더 많은 프로퍼티가 필요하다. 예를들어 위의 예제에서 value는 모드가 LIST_EMPTY 또는 LIST_NOT_EMPTY일 때 아예 사용되지도 않는다.
- 요소가 여러 목적을 가지고, 요소를 여러 방법으로 설정할 수 있는 경우에는 상태의 일관성과 정확성을 지키기 어렵다.
- 팩토리 메서드를 사용해야 하는 경우가 많다. 그렇지 않으면 객체가 제대로 생성되었는지 확인하는 것 자체가 어렵다.
코틀린은 그래서 일반적으로 태그 클래스보다 sealed 클래스를 많이 사용합니다.
한 클래스에 여러 모드를 만드는 방법 대신, 각각의 모드를 여러 클래스로 만들고 타입 시스템과 다형성을 활용하는 것입니다.
sealed class ValueMatcher<T> {
abstract fun match(value: T): Boolean
class Equal<T>(val value: T): ValueMatcher<T>() {
override fun match(value: T): Boolean =
value == this.value
}
class NotEqual<T>(val value: T): ValueMatcher<T>() {
override fun match(value: T): Boolean =
value != this.value
}
class EmptyList<T>(): ValueMatcher<T>() {
override fun match(value: T): Boolean =
value is List<*> && value.isEmpty()
}
class NotEmptyList<T>(): ValueMatcher<T>() {
override fun match(value: T): Boolean =
value is List<*> && value.isNotEmpty()
}
}
sealed 한정자
sealed 한정자는 외부 파일에서 서브클래스를 만드는 행위 자체를 모두 제한합니다.
외부에서 추가적인 서브클래스를 만들 수 없으므로, 타입이 추가되지 않을 거라는게 보장됩니다.
따라서 when 구문에서 else 브랜치를 따로 만들 필요가 없습니다.
fun <T> ValueMatcher<T>.reversed(): ValueMatcher<T> =
when (this) {
is ValueMatcher.EmptyList -> ValueMatcher.NotEmptyList<T>()
is ValueMatcher.NotEmptyList -> ValueMatcher.EmptyList<T>()
is ValueMatcher.Equal -> ValueMatcher.NotEqual(value)
is ValueMatcher.NotEqual -> ValueMatcher.Equal(value)
}
태그 클래스와 상태 패턴의 차이
태그 클래스와 상태 패턴(state pattern)을 혼동하면 안 됩니다.
상태 패턴은 객체의 내부 상태가 변화할 때, 객체의 동작이 변하는 소프트웨어 디자인 패턴입니다.
상태 패턴은 프런트엔드 컨트롤러(controller), 프레젠터(presenter), 뷰(view) 모델을 설계할 때 많이 사용됩니다.
상태 패턴을 사용한다면, 서로 다른 상태를 나타내는 클래스 계층 구조를 만들게 됩니다.
그리고 현재 상태를 나타내기 위한 읽고 쓸 수 있는 프로퍼티도 만들게 됩니다.
sealed class WorkoutState
class PrepareState(val exercise: Exercise): WorkoutState()
class ExerciseState(val exercise: Exercise): WorkoutState()
object DoneState : WorkoutState()
fun List<Exercise>.toState(): List<WorkoutState> =
flatMap { exercise ->
listOf(PrepareState(exercise), ExerciseState(exercise))
} + DoneState
class WorkoutPresenter( /*...*/ ) {
private val state: WorkoutState = states.first()
//...
}
정리
- 태그 클래스보다 타입 계층을 사용하는 것이 좋다.
- 일반적으로 이러한 타입 계층을 만들 때는 sealed 클래스를 사용한다.
- 타입 계층과 상태 패턴은 실질적으로 함꼐 사용하는 협력 관계라고 할 수 있다.
'Kotlin > 이펙티브 코틀린' 카테고리의 다른 글
[이펙티브 코틀린] Item40. equals의 규약을 지켜라 (1) | 2024.01.13 |
---|---|
[이펙티브 코틀린] Item38. 연산 또는 액션을 전달할 때는 인터페이스 대신 함수 타입을 사용하라 (0) | 2023.12.31 |
[이펙티브 코틀린] Item37. 데이터 집합 표현에 data 한정자를 사용하라 (0) | 2023.12.31 |