Remplacer le getter pour la classe de données Kotlin

94

Compte tenu de la classe Kotlin suivante:

data class Test(val value: Int)

Comment remplacer le Intgetter pour qu'il renvoie 0 si la valeur est négative?

Si ce n'est pas possible, quelles sont les techniques pour obtenir un résultat approprié?

spierce7
la source
13
Veuillez envisager de modifier la structure de votre code afin que les valeurs négatives soient converties en 0 lorsque la classe est instanciée, et non dans un getter. Si vous remplacez le getter comme décrit dans la réponse ci-dessous, toutes les autres méthodes générées telles que equals (), toString () et l'accès aux composants utiliseront toujours la valeur négative d'origine, ce qui entraînera probablement un comportement surprenant.
yole le

Réponses:

138

Après avoir passé presque un an à écrire quotidiennement Kotlin, j'ai trouvé que tenter de remplacer des classes de données comme celle-ci est une mauvaise pratique. Il y a 3 approches valables à cela, et après les avoir présentées, je vais vous expliquer pourquoi l'approche suggérée par d'autres réponses est mauvaise.

  1. Demandez à votre logique métier qui crée la data classvaleur de modifier la valeur 0 ou supérieure avant d'appeler le constructeur avec la valeur incorrecte. C'est probablement la meilleure approche dans la plupart des cas.

  2. N'utilisez pas de fichier data class. Utilisez un standard classet demandez à votre IDE de générer les méthodes equalset hashCodepour vous (ou non, si vous n'en avez pas besoin). Oui, vous devrez le régénérer si l'une des propriétés est modifiée sur l'objet, mais vous vous retrouvez avec le contrôle total de l'objet.

    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. Créez une propriété sûre supplémentaire sur l'objet qui fait ce que vous voulez au lieu d'avoir une valeur privée qui est effectivement remplacée.

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

Une mauvaise approche que suggèrent d'autres réponses:

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

Le problème avec cette approche est que les classes de données ne sont pas vraiment destinées à modifier des données comme celle-ci. Ils sont vraiment juste pour contenir des données. Remplacer le getter pour une classe de données comme celle-ci signifierait cela Test(0)et Test(-1)ne le ferait pas l' equalun l'autre et aurait des hashCodes différents , mais lorsque vous appelez .value, ils auraient le même résultat. Ceci est incohérent, et bien que cela puisse fonctionner pour vous, d'autres personnes de votre équipe qui voient qu'il s'agit d'une classe de données peuvent accidentellement en abuser sans se rendre compte de la façon dont vous l'avez modifiée / empêchée de fonctionner comme prévu (c'est-à-dire que cette approche ne fonctionnerait pas. t fonctionnent correctement en a Mapou a Set).

spierce7
la source
qu'en est-il des classes de données utilisées pour la sérialisation / désérialisation, l'aplatissement d'une structure imbriquée? Par exemple, je viens d'écrire data class class(@JsonProperty("iss_position") private val position: Map<String, Double>) { val latitude = position["latitude"]; val longitude = position["longitude"] }, et je considère que c'est assez bon pour mon cas, tbh. Que pensez-vous de ceci? (il y avait souvent d'autres champs et par conséquent, je pense que cela n'avait aucun sens pour moi de recréer cette structure json imbriquée dans mon code)
Antek
@Antek Étant donné que vous ne modifiez pas les données, je ne vois rien de mal à cette approche. Je mentionnerai également que la raison pour laquelle vous faites cela est que le modèle côté serveur que vous êtes envoyé n'est pas pratique à utiliser sur le client. Pour contrer de telles situations, mon équipe crée un modèle côté client dans lequel nous traduisons le modèle côté serveur après la désérialisation. Nous emballons tout cela dans une API côté client. Une fois que vous commencez à obtenir des exemples plus compliqués que ce que vous avez montré, cette approche est très utile car elle protège le client des mauvaises décisions / API relatives au modèle de serveur.
spierce7
Je ne suis pas d'accord avec ce que vous prétendez être la «meilleure approche». Le problème que je vois est qu'il est très courant de vouloir définir une valeur dans une classe de données et de ne jamais la modifier. Par exemple, analyser une chaîne en un int. Les getters / setters personnalisés sur une classe de données sont non seulement utiles, mais également nécessaires; sinon, il vous reste des POJO de bean Java qui ne font rien et leur comportement + validation est contenu dans une autre classe.
Abhijit Sarkar
Ce que j'ai dit, c'est "c'est probablement la meilleure approche pour la plupart des cas". Dans la plupart des cas, à moins que certaines circonstances ne surviennent, les développeurs doivent avoir une séparation claire entre leur modèle et leur algorithme / logique métier, le modèle résultant de leur algorithme représentant clairement les différents états des résultats possibles. Kotlin est fantastique pour cela, avec des classes scellées et des classes de données. Pour votre exemple de parsing a string into an int, vous autorisez clairement la logique métier d'analyse et de gestion des erreurs des chaînes non numériques dans votre classe de modèle ...
spierce7
... La pratique consistant à brouiller la ligne entre le modèle et la logique métier conduit toujours à un code moins maintenable, et je dirais que c'est un anti-modèle. Probablement 99% des classes de données que je crée sont des setters immuables / manquants. Je pense que vous aimeriez vraiment prendre le temps de lire sur les avantages de votre équipe en gardant ses modèles immuables. Avec les modèles immuables, je peux garantir que mes modèles ne sont pas modifiés accidentellement à un autre endroit aléatoire du code, ce qui réduit les effets secondaires et, encore une fois, conduit à un code maintenable. ie Kotlin ne s'est pas séparé Listet MutableListsans raison.
spierce7
31

Vous pouvez essayer quelque chose comme ceci:

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
  • Dans une classe de données, vous devez marquer les paramètres du constructeur principal avec valou var.

  • J'attribue la valeur de _valueà valueafin d'utiliser le nom souhaité pour la propriété.

  • J'ai défini un accesseur personnalisé pour la propriété avec la logique que vous avez décrite.

EPadronU
la source
1
J'ai eu une erreur sur l'IDE, elle dit "L'initialiseur n'est pas autorisé ici car cette propriété n'a pas de champ de sauvegarde"
Cheng
6

La réponse dépend des fonctionnalités que vous utilisez réellement data. @EPadron a mentionné une astuce astucieuse (version améliorée):

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

Cette volonté fonctionne comme prévu, ei il a un champ, un getter, à droite equals, hashcodeet component1. Le hic, c'est ça toStringet copyc'est bizarre:

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

Pour résoudre le problème, toStringvous pouvez le redéfinir à la main. Je ne connais aucun moyen de corriger la dénomination des paramètres mais de ne pas l'utiliser datadu tout.

voddan
la source
2

Je sais que c'est une vieille question mais il semble que personne n'ait mentionné la possibilité de rendre la valeur privée et d'écrire un getter personnalisé comme celui-ci:

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

Cela devrait être parfaitement valide car Kotlin ne générera pas de getter par défaut pour le champ privé.

Mais sinon, je suis tout à fait d'accord avec spierce7 pour dire que les classes de données sont destinées à contenir des données et que vous devriez éviter de coder en dur la logique «métier» là-bas.

bio007
la source
Je suis d'accord avec votre solution, mais que dans le code, vous devriez l'appeler comme ça val value = test.getValue() et pas comme les autres getters val value = test.value
gori
Oui. C'est correct. C'est un peu différent si vous l'appelez de Java car il y en a toujours.getValue()
bio007
1

J'ai vu votre réponse, je suis d'accord que les classes de données sont destinées à contenir des données uniquement, mais parfois nous devons en faire quelque chose.

Voici ce que je fais avec ma classe de données, j'ai changé certaines propriétés de val en var, et les ai overid dans le constructeur.

ainsi:

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
la source
Rendre les champs mutables juste pour pouvoir les modifier lors de l'initialisation est une mauvaise pratique. Il serait préférable de rendre le constructeur privé, puis de créer une fonction qui agit comme un constructeur (c'est-à-dire fun Recording(...): Recording { ... }). Une classe de données n'est peut-être pas non plus ce que vous voulez, car avec des classes sans données, vous pouvez séparer vos propriétés de vos paramètres de constructeur. Il vaut mieux être explicite avec vos intentions de mutabilité dans votre définition de classe. Si ces champs sont également modifiables de toute façon, alors une classe de données convient, mais presque toutes mes classes de données sont immuables.
spierce7
@ spierce7 est-ce vraiment si grave de mériter un vote négatif? Quoi qu'il en soit, cette solution me convient bien, elle ne nécessite pas autant de codage et elle garde le hachage et les égaux intacts.
Simou
0

Cela semble être l'un (parmi d'autres) inconvénients gênants de Kotlin.

Il semble que la seule solution raisonnable, qui garde complètement la rétrocompatibilité de la classe, est de la convertir en une classe régulière (pas une classe "data"), et d'implémenter à la main (à l'aide de l'EDI) les méthodes: hashCode ( ), equals (), toString (), copy () et 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
la source
Pas sûr que j'appellerais cela un inconvénient. Il s'agit simplement d'une limitation de la fonctionnalité de classe de données, qui n'est pas une fonctionnalité offerte par Java.
spierce7
0

J'ai trouvé que ce qui suit était la meilleure approche pour réaliser ce dont vous avez besoin sans vous casser equalset 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())

cependant,

Tout d'abord, notez que ce _valuen'est varpas le cas val, mais d'un autre côté, puisque c'est privé et que les classes de données ne peuvent pas être héritées, il est assez facile de s'assurer qu'il n'est pas modifié dans la classe.

Deuxièmement, toString()produit un résultat légèrement différent de ce qu'il serait si _valueétait nommé value, mais il est cohérent et TestData(0).toString() == TestData(-1).toString().

schatten
la source
@ spierce7 Non, ce n'est pas le cas. _valueest en cours de modification dans le bloc d'initialisation et equalset hashCode ne sont pas interrompus.
schatten