본문 바로가기

카테고리 없음

Enum vs Sealed class — which one to choose?

728x90
반응형

출처 : https://blog.kotlin-academy.com/enum-vs-sealed-class-which-one-to-choose-dc92ce7a4df5

 

Enum vs Sealed class — which one to choose?

TL;DR: Enums have supporting functions like valueOf, values or enumValues what makes them easier to iterate over or serialize. Just like…

blog.kotlin-academy.com

 

TL;DR : Enum 에는 valueOf, values 또는 enumValues와 같은 지원 기능이 있어 반복하거나 직렬화하기가 더 쉽습니다. 클래스와 마찬가지로 사용자 정의 메소드를 갖거나 데이터를 보유할 수 있지만 항상 enum value 당 하나씩 있습니다. 상수 값 집합을 나타내기에 완벽합니다. Sealed class 는 인스턴스에 특정한 데이터를 보유할 수 있습니다. 그것들은 구체적인 서브클래스 세트로 메시지나 클래스를 표현하는 데 완벽합니다.

 

Enum

가능한 옵션의 지속적인 집합을 나타내야 할 때 고전적인 선택은 Enum을 사용하는 것이었습니다. 예를 들어, 당사 웹사이트가 구체적인 지불 방법 집합을 제공하는 경우 다음 열거형 클래스를 사용하여 서비스에서 이를 나타낼 수 있습니다.

 

enum class PaymentOption {
    CASH,
    CARD,
    TRANSFER
}

Enum은 항상 항목별 값을 보유할 수 있습니다.

 
import java.math.BigDecimal

enum class PaymentOption {
    CASH,
    CARD,
    TRANSFER;

    var commission: BigDecimal = BigDecimal.ZERO
}

fun main() {
    val c1 = PaymentOption.CARD
    val c2 = PaymentOption.CARD
    print(c1 == c2) // true, because it is the same object

    c1.commission = BigDecimal.TEN
    print(c2.commission) // 10

    val t = PaymentOption.TRANSFER
    print(t.commission) // 0, because `commission` is per-item
}

 

이러한 값을 항목별로 정적으로 변경 가능하게 만드는 것은 좋지 않습니다. 이 기능은 종종 각 항목마다 일정한 값을 추가하는 데 사용됩니다. 이러한 상수 값은 기본 생성자를 사용하여 각 항목 생성 중에 첨부할 수 있습니다.

 

import java.math.BigDecimal

enum class PaymentOption(val commission: BigDecimal) {
    CASH(BigDecimal.ONE),
    CARD(BigDecimal.TEN),
    TRANSFER(BigDecimal.ZERO)
}

fun main() {
    println(PaymentOption.CARD.commission) // 10
    println(PaymentOption.TRANSFER.commission) // 0

    val paymentOption: PaymentOption = PaymentOption.values().random()
    println(paymentOption.commission) // 0, 1 or 10
}

 

Kotlin enum 에는 메서드가 있을 수도 있습니다. 구현도 항목별로 다릅니다. 그것들을 정의할 때 enum class 자체(여기서 PaymentOption과 같은)는 추상 메서드를 정의해야 하며 각 항목은 이를 재정의해야 합니다.

enum class PaymentOption {
    CASH {
        override fun startPayment(transaction: TransactionData) {
            showCashPaimentInfo(transaction)
        }
    },
    CARD {
        override fun startPayment(transaction: TransactionData) {
            moveToCardPaymentPage(transaction)
        }
    },
    TRANSFER {
        override fun startPayment(transaction: TransactionData) {
            showMoneyTransferInfo()
            setupPaymentWatcher(transaction)
        }
    };

    abstract fun startPayment(transaction: TransactionData)
}

 

이 옵션은 functional type 의 기본 생성자 매개변수를 사용하는 것이 더 편리하기 때문에 거의 사용되지 않습니다.

 

enum class PaymentOption(val startPayment: (TransactionData)->Unit) {
    CASH(::showCashPaimentInfo),
    CARD(::moveToCardPaymentPage),
    TRANSFER({
        showMoneyTransferInfo()
        setupPaymentWatcher(it)
    })
}

 

훨씬 더 편리한 방법은 extension function 을 정의하는 것입니다.

 

enum class PaymentOption {
    CASH,
    CARD,
    TRANSFER
}

fun PaymentOption.startPayment(transaction: TransactionData) {
    when (this) {
        PaymentOption.CASH -> showCashPaimentInfo(transaction)
        PaymentOption.CARD -> moveToCardPaymentPage(transaction)
        PaymentOption.TRANSFER -> {
            showMoneyTransferInfo()
            setupPaymentWatcher(transaction)
        }
    }
}
 
 
enum 의 힘은 이러한 항목이 구체적이고 일정하다는 것입니다. 따라서 values() 함수를 사용하거나 enumValueOf 함수를 사용하여 type 별로 모든 항목을 가져올 수 있습니다. valueOf(String)를 사용하여 String에서 enum 을 읽거나 enumValueOf를 사용하여 type 별로 읽을 수도 있습니다.

 

enum class PaymentOption {
    CASH,
    CARD,
    TRANSFER
}

inline fun <reified T: Enum<T>> printEnumValues() {
    for(value in enumValues<T>()) {
        println(value)
    }
}

fun main() {
    val options: Array<PaymentOption> = PaymentOption.values()
    println(options.map { it.name }) // [CASH, CARD, TRANSFER]

    val option: PaymentOption = PaymentOption.valueOf("CARD")
    println(option) // CARD

    val options2: Array<PaymentOption> = enumValues<PaymentOption>()
    println(options2.map { it.name }) // [CASH, CARD, TRANSFER]

    val option2: PaymentOption = enumValueOf<PaymentOption>("CARD")
    println(option2) // CARD

    printEnumValues<PaymentOption>()
}
 

따라서 enum 값에 대한 반복은 쉽고 직렬화/역직렬화는 간단하고 효율적이며(일반적으로 이름으로만 표시됨) 직렬화를 위해 대부분의 라이브러리(예: Gson, Jackson, Kotlin Serialization 등)에서 자동으로 지원됩니다. 그들은 또한 ordinal 를 가지며 자동으로 toString, hashCode 및 equals를 구현합니다. 결과적으로 enum 은 상수 값의 구체적인 집합을 나타내기에 완벽합니다.

 

 

 

 

Sealed class

 

구체적인 값 그룹을 나타내는 또 다른 방법은 sealed class 입니다. sealed class 는 동일한 파일에 정의된 구체적인 수의 하위 클래스가 있는 추상 클래스입니다.

Sealed 수정자가 하는 일은 파일 외부에서 이 클래스의 다른 하위 클래스를 정의하는 것이 불가능하다는 것입니다. 덕분에 단일 파일을 분석하는 것만으로도 sealed class 의 하위 클래스가 무엇인지 확신할 수 있습니다. Kotlin 컴파일러는 옵션을 제안할 수 있는 경우와 같은 일부 상황에서도 이를 알고 있으며 모든 가능성이 포함되어 있음을 이해합니다. (out 한정자를 이해하려면 이 문서를 참조하십시오. 여기에 설명된 모든 유형의 하위 유형은 Nothing 입니다.)

sealed class Response<out R>
class Success<R>(val value: R): Response<R>()
class Failure(val error: Throwable): Response<Nothing>()

fun handle(response: Response<String>) {
    val text = when (response) {
        is Success -> "Success, data are: " + response.value
        is Failure -> "Error"
    }
    print(text)
}

 

Sealed classe 는 sum type(category 이론의 부산물)을 표현하는 데 완벽합니다. 가장 일반적인 것과 같은 대안(alternatives) 집합 : 왼쪽 또는 오른쪽이 있지만, 둘다 가질수는 없습니다.

 
sealed class Either<out L, out R>
class Left<L>: Either<L, Nothing>()
class Right<R>: Either<Nothing, R>()

 

우리의 응용 프로그램에서는 대체 클래스(alternative classe) 집합을 처리할 때 sealed class 를 사용합니다. 예를 들어 API는 표시되어야 하는 광고의 종류를 알려줍니다.

 

sealed class AdView
object FacebookAd: AdView()
object GoogleAd: AdView()
class OwnAd(val text: String, val imgUrl: String): AdView()
 
FacebookAd와 GoogleAd 모두 데이터를 보유하지 않으므로 필요할 때마다 새 인스턴스를 생성하지 않도록 객체 선언을 만들어 재사용합니다. sealed class 는 객체 선언된 하위 클래스만 가질 수 있으며 이러한 방식으로 사용하면 enum 과 매우 유사합니다.
 

 

sealed class PaymentOption {
    object Cash
    object Card
    object Transfer
}

 

enum 이 이러한 경우엔 더 적절하기 때문에 이런 방식으로 sealed class 를 사용하지는 않습니다.

enum 에 비해 sealed class 의 장점은 하위 클래스가 인스턴스별 데이터를 보유할 수 있다는 것입니다. 예를 들어 선택한 지불 옵션에 대해 애플리케이션의 다른 부분에 알릴 때 선택한 지불 type 과 나중에 처리하는 데 필요한 지불 관련 데이터를 모두 전달할 수 있습니다.

import java.math.BigDecimal

sealed class Payment
data class CashPayment(val amount: BigDecimal, val pointId: Int): Payment()
data class CardPayment(val amount: BigDecimal, val orderId: Int): Payment()
data class BankTransfer(val amount: BigDecimal, val orderId: Int): Payment()

fun process(payment: Payment) {
    when (payment) {
        is CashPayment -> {
            showPaymentInfo(payment.amount, payment.pointId)
        }
        is CardPayment -> {
            moveToCardPaiment(payment.amount, payment.orderId)
        }
        is BankTransfer -> {
            val bankTransferRepo = BankTransferRepo()
            val transferDetails = bankTransferRepo.getDetails()
            displayTransferInfo(payment.amount, transferDetails)
            bankTransferRepo.setUpPaymentWathcher(payment.orderId, payment.amount, transferDetails)
        }
    }
}
 
 

sealed class 를 사용하기 좋은 경우는 모든 종류의 이벤트 또는 메시지를 나타내는 것입니다. 왜냐하면 우리는 그것이 어떤 이벤트인지, 각 이벤트가 가진 데이터가 무엇인지 둘 다 알기 때문입니다.

 

Summary

  • enum class 는 구체적인 값 집합을 나타내는 반면 sealed class 는 구체적인 클래스 집합을 나타냅니다. 이러한 클래스는 객체 선언이 될 수 있으므로 enum 대신 어느 정도 sealed class 를 사용할 수 있지만 그 반대는 불가능합니다.
  • enum class 의 장점은 즉시 직렬화 및 역직렬화할 수 있다는 것입니다. 또한 values() 및 valueOf 메서드가 있습니다. enumValues 및 enumValueOf 함수를 사용하여 type 별로 enum 값을 얻을 수도 있습니다. enum 에는 순서가 있으며 각 항목별로 일정한 데이터를 보유할 수 있습니다. 가능한 값의 상수 집합을 나타내는 데 완벽합니다.
  • sealed class 의 장점은 인스턴스별 데이터를 보유할 수 있다는 것입니다. 각 항목은 클래스 또는 객체(객체 선언을 사용하여 생성)일 수 있습니다. 이들은 대체 클래스 세트(sum type, 부산물)를 나타냅니다. 성공 또는 실패인 결과, Node 의 Leaf 또는 Tree 또는 list, object, string, boolean, int 또는 null인 JSON 값 과 같은 대안을 정의하는 데 유용합니다. 또한 발생할 수 있는 일련의 이벤트 또는 메시지를 정의하는 데도 유용합니다.
728x90
반응형