유효성검사의 에러메세지, 액티비티 혹은 프래그먼트에 진입하는 entry type등을 정해줄때 주로 enum class
를 사용한다.
관련있는 상수들끼리 모아주기때문에 데이터 관리에 효과적이나, 서로의 타입이 다르면 안된다는 한계가 있다.
그런 단점을 보완하기 위해 sealed class를 사용해 왔다.
대표적으로 리사이클러뷰의 뷰타입을 나눠 줄때 유용하게 사용했는대
sealed class SearchViewType {
data class SortType(val sort: String): SearchViewItem()
data class ItemType(val type: String): SearchViewItem()
data class Contents(val content: SearchModel) : SearchViewItem()
}
sealed class UiEvent {
object OnClick : UiEvent()
data class OnInput(val text: String) : UiEvent()
}
이렇게 다양한 타입의 데이터를 하나로 묶어 줄 수 있어 리사이클러뷰 타입, 이벤트 처리 등 관리하기 쉽고 안정적으로 만들 수 있다.
하지만 제약사항으로 하위 클래스의 정의는 같은 패키지나 파일 내부에 기술되어야 한다.
하위 타입을 좀더 세분화 할 필요가 있을땐 Nested sealed class를 사용해서 구분해주기도한다
sealed class Action {
object Stand : Action()
object Move : Action()
data class Sit(val position: String) : Action()
}
움직임을 나타낸 실드 클래스를 만들었다.
여기선 Stand, Move, Sit 세가지 서브 클래스를 가지고 있는데
움직임을 더 다양하게 나타내고 싶다면 Move를 실드클래스로 선언해 세분화해서 나타낼 수 있다.
sealed class Action {
object Stand : Action()
sealed class Move : Action() {
object Walk : Move()
object Run : Move()
data class Jump(val height: Int) : Move()
}
data class Sit(val position: String) : Action()
}
fun performAction(action: Action) {
when (action) {
is Action.Stand -> println("일어난다.")
is Action.Sit -> println("${action.position}번 위치에 앉는다")
is Action.Move.Walk -> println("걷는다")
is Action.Move.Run -> println("뛴다")
is Action.Move.Jump -> println("${action.height}만큼 점프 했다.")
}
}
fun main() {
performAction(Action.Move.Run)
}
이처럼 네스티드 실드 클래스를 사용하면, 복잡한 계층 구조를 명확하게 표현할 수 있다.
하지만 내부 클래스의 인스턴스화가 제한되고, 계층 구조 내에서 공통의 부모 클래스를 공유하는 것이 어려운 문제가 있는데 이를 해결 할 수 있는게 sealed interface다
sealed interface는 sealed class의 계층적 한계를 극복하기 위해서 추가되었는데
클래스 계층 구조를 더 유연하게 표현할 수 있으며, 인터페이스기 때문에 인스턴스화나, 다중상속에서 자유롭다
실드 인터페이스를 사용하면 코드의 유연성을 높이고, 복잡한 계층 구조를 쉽게 관리할 수 있는데
실드 클래스와 비교해 보겠다.
sealed class Action {
object Stand : Action()
sealed class Move : Action() {
object Walk : Move()
object Run : Move()
data class Jump(val height: Int) : Move()
}
data class UseAbility(val ability: String) : Action()
data class Shield(val strength: Int) : Action()
data class Strike(val power: Int) : Action()
}
fun performAction(action: Action) {
when (action) {
is Action.Stand -> println("일어난다.")
is Action.Move -> when (action) {
is Action.Move.Walk -> println("걷는다")
is Action.Move.Run -> println("뛴다")
is Action.Move.Jump -> println("{action.height}만큼 점프 했다.")
}
is Action.UseAbility -> println("능력발동: ${action.ability}")
is Action.Shield -> println("방어한다 \n 방어력: ${action.strength}")
is Action.Strike -> println("공격한다 \n 공격력: ${action.power}")
}
}
fun main() {
performAction(Action.Move.Run)
performAction(Action.UseAbility("Fireball!"))
performAction(Action.Shield(100))
performAction(Action.Strike(50))
}
위에서 만든 액션에서 능력사용, 방어, 공격을 추가해봤다.
기존의 클래스가 점점 커지면서 복잡해지는걸 볼 수 있는데, 유지 보수가 힘들고 더 많은 기능을 추가하기가 점점 힘들어져 확장성이 떨어진다
그리고 한 객체가 여러 상태나 행동을 동시에 나타내는 것을 표현하기에도 어려운대
예를 들어, 캐릭터가 달리면서 공격하는등 복합적인 행동을 모델링하기 힘들다.
sealed interface Action
sealed interface Move : Action {
object Walk : Move
object Run : Move
data class Jump(val height: Int) : Move
}
sealed interface Ability : Action {
data class UseAbility(val ability: String) : Ability
}
sealed interface Defense : Action {
data class Shield(val strength: Int) : Defense
}
sealed interface Attack : Action {
data class Strike(val power: Int) : Attack
}
class Character : Move, Ability, Defense, Attack
fun performAction(action: Action) {
when (action) {
is Move.Walk -> println("걷는다")
is Move.Run -> println("뛴다")
is Move.Jump -> println("J{action.height}만큼 점프 했다.")
is Ability.UseAbility -> println("능력발동: ${action.ability}")
is Defense.Shield -> println("방어한다 \n 방어력: ${action.strength}")
is Attack.Strike -> println("공격한다 \n 공격력: ${action.power}")
}
}
fun main() {
val actions = listOf<Action>(
Move.Run,
Ability.UseAbility("Invisibility"),
Defense.Shield(100),
Attack.Strike(50)
)
actions.forEach { action ->
performAction(action)
}
}
다중 상속을 받으면서 계속 상속할 수 있어 굉장히 유연해진다.
다중 상속으로 인한 확장성뿐만 아니라 다양한 행동을 동시에 나타낼 수 있는데
sealed interface Action
...
// 복합 행동을 위한 인터페이스
sealed interface CombinedAction : Action
// 달리면서 공격하는 행동을 표현하는 클래스
data class RunAndStrike(val postion: Int, val power: Int) : Move, Attack, CombinedAction
fun performAction(action: Action) {
when (action) {
is Move.Walk -> println("걷는다")
...
is RunAndStrike -> println("달리면서 공격한다. \n 이동속도 ${action.speed}, 공격력 ${action.power}")
}
}
fun main() {
val actions = listOf<Action>(
Move.Run,
Ability.UseAbility("Invisibility"),
Defense.Shield(100),
Attack.Strike(50),
RunAndStrike(10, 60) // 달리면서 공격하는 행동 추가
)
actions.forEach { action ->
performAction(action)
}
}
Move, Attack, 그리고 CombinedAction 인터페이스를 구현하여, 달리면서 동시에 공격하는 복합 행동을 나타낼수 있다.
sealed interface를 보면 확장성과 유연성에서 sealed 클래스에 비해 훨씬 뛰어난걸 볼 수 있다.
그렇다면 이걸 언제 사용해야 할까?
실드 클래스를 전부 대체 할 수 있는건가?
실드 클래스는 실드 인터페이스로 대체 할 수 없는 특징이 있는데
생성자를 가질 수 있다.
그렇기 때문에 생성자를 통해 초기화 해줄 필요가 있다면 실드 클래스를 사용하는게 좋은 방법이 될 수 있다.
생성자가 필요없다면 sealed interface를 쓰자.
확장성과 유연성이 뛰어나서 생성자가 필요없다면 sealed interface를 쓰면 된다.
하지만 생성자를 통해 초기화 해줄 필요가 있다면 sealed Class를 쓰자,
sealed interface는 생성자를 가질 수 없으니 초기화가 필요하다면 sealed Class를 쓰면 된다.