Enum vs classe selada – qual escolher?

Tempo de leitura: 5 minutes

TL; DR: Enums têm funções de suporte como valueOf, values ​​ou enumValues, o que os torna mais fáceis de iterar ou serializar. Assim como as classes, eles podem ter métodos personalizados ou armazenar dados, mas sempre um por valor de enum. Eles são perfeitos para representar um conjunto de valores constantes. As classes seladas podem conter dados específicos de uma instância. Eles são perfeitos para representar mensagens ou classes com um conjunto concreto de subclasses.

 

Enum

Quando tínhamos que representar um conjunto constante de opções possíveis, uma escolha clássica era usar Enum. Por exemplo, se nosso site oferece um conjunto concreto de métodos de pagamento, podemos representá-los em nosso serviço usando a seguinte classe enum:

enum class PaymentOption {
    CASH,
    CARD,
    TRANSFER
}

Enum pode conter valores que são sempre específicos do item:

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) // verdade, porque é o mesmo objeto

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

    val t = PaymentOption.TRANSFER
    print(t.commission) // 0, porque a `comissão` é por item
}

É um mau gosto tornar esses valores mutáveis, pois são estáticos por item. Embora essa funcionalidade seja freqüentemente usada para anexar alguns valores constantes para cada item. Esses valores constantes podem ser anexados durante a criação de cada item, usando o construtor principal:

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
}

Os enums de Kotlin podem até ter métodos. Suas implementações também são específicas para cada item. Quando os definimos, a própria classe enum (como PaymentOption aqui) precisa definir um método abstrato e cada item deve substituí-lo:

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)
}

Embora essa opção seja raramente usada porque é mais conveniente usar um parâmetro do construtor primário do tipo funcional:

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

A opção ainda mais conveniente é definir uma função de extensão:

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)
        }
    }
}

O poder do enum é que esses itens são específicos e constantes. Portanto, podemos obter todos os itens usando a função values ​​() ou por tipo usando a função enumValueOf. Também podemos ler enum de String usando valueOf (String) ou por tipo usando enumValueOf.

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>()
}

Portanto, iterar sobre valores de enum é fácil e sua serialização / desserialização é simples e eficiente (já que eles geralmente são representados apenas pelo nome) e são automaticamente suportados pela maioria das bibliotecas para serialização (como Gson, Jackson, Kotlin Serialization, etc.). Eles também possuem toString, hashCode e equals ordinal e implementados automaticamente. Como resultado, enums são perfeitos para representar um conjunto concreto de valores constantes.

 

Classe Sealed

Outra forma de representar um grupo concreto de valores é uma classe selada. As classes seladas são classes abstratas com um número concreto de subclasses, todas definidas no mesmo arquivo.

O que o modificador selado faz é impossível definir outra subclasse dessa classe fora do arquivo. Graças a isso temos certeza do que são subclasses de uma classe selada apenas analisando um único arquivo. O compilador Kotlin sabe disso também e em alguns contextos, como quando ele pode sugerir opções e entender que todas as possibilidades foram cobertas.

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 -> "Sucesso, os dados são: " + response.value
        is Failure -> "Erro"
    }
    print(text)
}

Classes seladas são perfeitas para representar tipos de soma (coprodutos na teoria de categorias) – um conjunto de alternativas, como a mais genérica: Qualquer uma, que tem Esquerda ou Direita, mas nunca ambas.

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

Em nossos aplicativos, usamos classes seladas quando estamos lidando com um conjunto de classes alternativas. Por exemplo, a API nos diz que tipo de anúncio deve ser exibido:

sealed class AdView
object FacebookAd: AdView()
object GoogleAd: AdView()
class OwnAd(val text: String, val imgUrl: String): AdView()

Observe que tanto o FacebookAd quanto o GoogleAd não armazenam dados e, portanto, para não criar uma nova instância toda vez que precisarmos, fazemos declarações de objeto e os reutilizamos. As classes seladas podem ter apenas subclasses de declaração de objeto e, quando usadas dessa forma, são muito semelhantes a um enum:

sealed class PaymentOption {
    object Cash
    object Card
    object Transfer
}

Embora não usemos classes lacradas dessa maneira, pois enums são mais apropriados para esses casos.

A vantagem das classes lacradas sobre o enum é que as subclasses podem conter dados específicos da instância. Por exemplo, quando informamos outra parte de nosso aplicativo sobre a opção de pagamento escolhida, podemos passar tanto o tipo de pagamento escolhido quanto os dados específicos do pagamento que são necessários para processamento posterior.

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)
        }
    }
}

Um ótimo caso para classes lacradas é representar todos os tipos de eventos ou mensagens porque temos informações sobre qual evento é esse e cada evento pode conter dados.

 

Resumo

  • As classes Enum representam um conjunto concreto de valores, enquanto as classes seladas representam um conjunto concreto de classes. Como essas classes podem ser declarações de objetos, podemos usar classes lacradas até um certo grau em vez de enums, mas não o contrário.
  • A vantagem das classes enum é que elas podem ser serializadas e desserializadas imediatamente. Eles têm os métodos values ​​() e valueOf. Também podemos obter valores enum por tipo usando as funções enumValues ​​e enumValueOf. Enums têm ordinal e podemos conter dados constantes para cada item. Eles são perfeitos para representar um conjunto constante de valores possíveis.
  • A vantagem das classes lacradas é que elas podem conter dados específicos da instância. Cada item pode ser uma classe ou um objeto (criado usando a declaração do objeto). Eles representam um conjunto de classes alternativas (tipo de soma, coproduto). Eles são úteis para definir alternativas, Result que é Success ou Failure, Tree que é Leaf ou Node, ou valor JSON que é list, object, string, boolean, int ou null. Eles também são ótimos para definir um conjunto de eventos ou mensagens que podem ocorrer.

 

Obrigado por gastar seu tempo lendo este artigo. Compartilhe se você gostou!