Substituir getter para classe de dados Kotlin

94

Dada a seguinte classe de Kotlin:

data class Test(val value: Int)

Como eu substituiria o Int getter para que retornasse 0 se o valor fosse negativo?

Se isso não for possível, quais são algumas técnicas para obter um resultado adequado?

spierce7
fonte
13
Considere alterar a estrutura do seu código para que os valores negativos sejam convertidos em 0 quando a classe for instanciada, e não em um getter. Se você substituir o getter conforme descrito na resposta abaixo, todos os outros métodos gerados, como equals (), toString () e acesso ao componente ainda usarão o valor negativo original, o que provavelmente levará a um comportamento surpreendente.
yole

Respostas:

138

Depois de passar quase um ano inteiro escrevendo Kotlin diariamente, descobri que tentar substituir as classes de dados dessa maneira é uma prática ruim. Existem 3 abordagens válidas para isso e, depois de apresentá-las, explicarei por que a abordagem que outras respostas sugeriram é ruim.

  1. Tenha sua lógica de negócios que cria o data classalter o valor para ser 0 ou maior antes de chamar o construtor com o valor incorreto. Esta é provavelmente a melhor abordagem para a maioria dos casos.

  2. Não use um data class. Use um regular classe faça com que seu IDE gere os métodos equalse hashCodepara você (ou não, se não precisar deles). Sim, você terá que gerá-lo novamente se alguma das propriedades for alterada no objeto, mas você terá controle total do objeto.

    class Test(value: Int) {
      val value: Int = value
        get() = if (field < 0) 0 else field
    
      override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is Test) return false
        return true
      }
    
      override fun hashCode(): Int {
        return javaClass.hashCode()
      }
    }
  3. Crie uma propriedade de segurança adicional no objeto que faz o que você deseja, em vez de ter um valor privado que é efetivamente substituído.

    data class Test(val value: Int) {
      val safeValue: Int
        get() = if (value < 0) 0 else value
    }

Uma abordagem ruim que outras respostas estão sugerindo:

data class Test(private val _value: Int) {
  val value: Int
    get() = if (_value < 0) 0 else _value
}

O problema com essa abordagem é que as classes de dados não são realmente destinadas a alterar dados como este. Eles servem apenas para armazenar dados. Substituir o getter para uma classe de dados como essa significaria isso Test(0)e Test(-1)não equalum ao outro e teria hashCodes diferentes , mas quando você chamasse .value, eles teriam o mesmo resultado. Isso é inconsistente e, embora possa funcionar para você, outras pessoas em sua equipe que veem que se trata de uma classe de dados podem acidentalmente usá-la indevidamente, sem perceber como você a alterou / fez com que não funcionasse como esperado (ou seja, essa abordagem não funcionaria t funcionar corretamente em a Mapou a Set).

spierce7
fonte
e as classes de dados usadas para serialização / desserialização, achatando uma estrutura aninhada? Por exemplo, acabei de escrever data class class(@JsonProperty("iss_position") private val position: Map<String, Double>) { val latitude = position["latitude"]; val longitude = position["longitude"] }, e considero muito bom para o meu caso, tbh. O que você pensa sobre isso? (havia outros campos ofc e, portanto, acredito que não fazia sentido para mim recriar essa estrutura json aninhada em meu código)
Antek
@Antek Visto que você não está alterando os dados, não vejo nada de errado com essa abordagem. Mencionarei também que você está fazendo isso porque o modelo do lado do servidor que você está recebendo não é conveniente para usar no cliente. Para combater essas situações, minha equipe cria um modelo do lado do cliente para o qual traduzimos o modelo do lado do servidor após a desserialização. Envolvemos tudo isso em uma API do lado do cliente. Depois de começar a obter exemplos mais complicados do que os que você mostrou, essa abordagem é muito útil, pois protege o cliente de decisões / apis de modelos de servidor incorretos.
spierce7
Não concordo com o que você afirma ser a "melhor abordagem". O problema que vejo é que é muito comum querer definir um valor em uma classe de dados e nunca alterá-lo. Por exemplo, analisar uma string em um int. Os getters / setters personalizados em uma classe de dados não são apenas úteis, mas também necessários; caso contrário, você fica com POJOs de bean Java que não fazem nada e seu comportamento + validação está contido em alguma outra classe.
Abhijit Sarkar
O que eu disse é "Esta é provavelmente a melhor abordagem para a maioria dos casos". Na maioria dos casos, a menos que surjam certas circunstâncias, os desenvolvedores devem ter uma separação clara entre seu modelo e algoritmo / lógica de negócios, onde o modelo resultante de seu algoritmo representa claramente os vários estados dos resultados possíveis. Kotlin é fantástico para isso, com classes seladas e classes de dados. Para o seu exemplo de parsing a string into an int, você está permitindo claramente a lógica de negócios de análise e tratamento de erros não numéricos em sua classe de modelo ...
spierce7
... A prática de confundir a linha entre o modelo e a lógica de negócios sempre leva a um código menos sustentável, e eu diria que é um antipadrão. Provavelmente 99% das classes de dados que crio são configuradores imutáveis ​​/ ausentes. Acho que você realmente gostaria de ler sobre os benefícios de sua equipe manter seus modelos imutáveis. Com modelos imutáveis, posso garantir que meus modelos não sejam modificados acidentalmente em algum outro lugar aleatório no código, o que reduz os efeitos colaterais e, novamente, leva a um código sustentável. ou seja, Kotlin não se separou Liste MutableListsem motivo.
spierce7
31

Você pode tentar algo assim:

data class Test(private val _value: Int) {
  val value = _value
    get(): Int {
      return if (field < 0) 0 else field
    }
}

assert(1 == Test(1).value)
assert(0 == Test(0).value)
assert(0 == Test(-1).value)

assert(1 == Test(1)._value) // Fail because _value is private
assert(0 == Test(0)._value) // Fail because _value is private
assert(0 == Test(-1)._value) // Fail because _value is private
  • Em uma classe de dados, você deve marcar os parâmetros do construtor primário com valou var.

  • Estou atribuindo o valor de _valuea valuepara usar o nome desejado para a propriedade.

  • Eu defini um acessador personalizado para a propriedade com a lógica que você descreveu.

EPadronU
fonte
1
Recebi um erro no IDE, ele diz "O inicializador não é permitido aqui, pois esta propriedade não tem campo de apoio"
Cheng
6

A resposta depende de quais recursos você realmente usa que datafornece. @EPadron mencionou um truque bacana (versão aprimorada):

data class Test(private val _value: Int) {
    val value: Int
        get() = if (_value < 0) 0 else _value
}

Isso funcionará conforme o esperado, ei tem um campo, um getter, certo equals, hashcodee component1. O problema é esse toStringe copysão estranhos:

println(Test(1))          // prints: Test(_value=1)
Test(1).copy(_value = 5)  // <- weird naming

Para corrigir o problema, toStringvocê pode redefini-lo manualmente. Não conheço nenhuma maneira de corrigir a nomenclatura do parâmetro, mas não de usá data-la.

voddan
fonte
2

Eu sei que esta é uma questão antiga, mas parece que ninguém mencionou a possibilidade de tornar o valor privado e escrever um getter personalizado como este:

data class Test(private val value: Int) {
    fun getValue(): Int = if (value < 0) 0 else value
}

Isso deve ser perfeitamente válido, pois o Kotlin não gerará um getter padrão para o campo privado.

Mas, caso contrário, eu definitivamente concordo com spierce7 que as classes de dados são para armazenar dados e você deve evitar codificar a lógica de "negócios" aí.

bio007
fonte
Eu concordo com sua solução, mas que no código você teria que chamá-lo assim val value = test.getValue() e não como outros getters val value = test.value
gori
Sim. Está correto. É um pouco diferente se você chamá-lo de Java, pois está sempre.getValue()
bio007
1

Eu vi sua resposta, concordo que as classes de dados são destinadas a conter apenas dados, mas às vezes precisamos fazer algumas coisas com elas.

Aqui está o que estou fazendo com minha classe de dados, alterei algumas propriedades de val para var e as substituí no construtor.

igual a:

data class Recording(
    val id: Int = 0,
    val createdAt: Date = Date(),
    val path: String,
    val deleted: Boolean = false,
    var fileName: String = "",
    val duration: Int = 0,
    var format: String = " "
) {
    init {
        if (fileName.isEmpty())
            fileName = path.substring(path.lastIndexOf('\\'))

        if (format.isEmpty())
            format = path.substring(path.lastIndexOf('.'))

    }


    fun asEntity(): rc {
        return rc(id, createdAt, path, deleted, fileName, duration, format)
    }
}
Simou
fonte
Tornar os campos mutáveis ​​apenas para que você possa modificá-los durante a inicialização é uma prática ruim. Seria melhor tornar o construtor privado e, em seguida, criar uma função que atue como um construtor (ou seja fun Recording(...): Recording { ... }). Além disso, talvez uma classe de dados não seja o que você deseja, já que com classes que não são de dados, você pode separar suas propriedades dos parâmetros do seu construtor. É melhor ser explícito com suas intenções de mutabilidade em sua definição de classe. Se esses campos também forem mutáveis ​​de qualquer maneira, uma classe de dados está bem, mas quase todas as minhas classes de dados são imutáveis.
spierce7
@ spierce7 é realmente tão ruim merecer um voto negativo? De qualquer forma, essa solução me serve bem, não requer tanta codificação e mantém o hash e igual intactos.
Simou
0

Esta parece ser uma (entre outras) desvantagens irritantes do Kotlin.

Parece que a única solução razoável, que mantém completamente a compatibilidade com versões anteriores da classe, é convertê-la em uma classe regular (não uma classe de "dados") e implementar manualmente (com a ajuda do IDE) os métodos: hashCode ( ), equals (), toString (), copy () e componentN ()

class Data3(i: Int)
{
    var i: Int = i

    override fun equals(other: Any?): Boolean
    {
        if (this === other) return true
        if (other?.javaClass != javaClass) return false

        other as Data3

        if (i != other.i) return false

        return true
    }

    override fun hashCode(): Int
    {
        return i
    }

    override fun toString(): String
    {
        return "Data3(i=$i)"
    }

    fun component1():Int = i

    fun copy(i: Int = this.i): Data3
    {
        return Data3(i)
    }

}
Asher Stern
fonte
Não tenho certeza se chamaria isso de desvantagem. É apenas uma limitação do recurso de classe de dados, que não é um recurso que o Java oferece.
spierce7
0

Achei o seguinte a melhor abordagem para conseguir o que você precisa sem quebrar equalse hashCode:

data class TestData(private var _value: Int) {
    init {
        _value = if (_value < 0) 0 else _value
    }

    val value: Int
        get() = _value
}

// Test value
assert(1 == TestData(1).value)
assert(0 == TestData(-1).value)
assert(0 == TestData(0).value)

// Test copy()
assert(0 == TestData(-1).copy().value)
assert(0 == TestData(1).copy(-1).value)
assert(1 == TestData(-1).copy(1).value)

// Test toString()
assert("TestData(_value=1)" == TestData(1).toString())
assert("TestData(_value=0)" == TestData(-1).toString())
assert("TestData(_value=0)" == TestData(0).toString())
assert(TestData(0).toString() == TestData(-1).toString())

// Test equals
assert(TestData(0) == TestData(-1))
assert(TestData(0) == TestData(-1).copy())
assert(TestData(0) == TestData(1).copy(-1))
assert(TestData(1) == TestData(-1).copy(1))

// Test hashCode()
assert(TestData(0).hashCode() == TestData(-1).hashCode())
assert(TestData(1).hashCode() != TestData(-1).hashCode())

Contudo,

Primeiro, observe que _valueé var, nãoval , mas, por outro lado, uma vez que é privado e classes de dados não pode ser herdada de, é bastante fácil de se certificar de que não é modificada dentro da classe.

Em segundo lugar, toString()produz um resultado ligeiramente diferente do que produziria se _valuefosse nomeado value, mas é consistente e TestData(0).toString() == TestData(-1).toString().

Schatten
fonte
@ spierce7 Não, não é. _valueestá sendo modificado no bloco de inicialização equalse hashCode não está quebrado.
schatten