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.
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.
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.
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 classTest(val value:Int){val safeValue:Intget()=if(value <0)0else value}
Une mauvaise approche que suggèrent d'autres réponses:
data classTest(privateval_value:Int){val value:Intget()=if(_value <0)0else_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).
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 classTest(privateval_value:Int){val value =_valueget():Int{returnif(field <0)0else 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.
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 classTest(privateval_value:Int){val value:Intget()=if(_value <0)0else_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:
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.
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 classTest(privateval value:Int){fun getValue():Int=if(value <0)0else 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.
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.
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 ()
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 classTestData(privatevar_value:Int){init{_value =if(_value <0)0else_value
}val value:Intget()=_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().
Réponses:
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.
Demandez à votre logique métier qui crée la
data class
valeur 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.N'utilisez pas de fichier
data class
. Utilisez un standardclass
et demandez à votre IDE de générer les méthodesequals
ethashCode
pour 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.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.
Une mauvaise approche que suggèrent d'autres réponses:
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)
etTest(-1)
ne le ferait pas l'equal
un l'autre et aurait deshashCode
s 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 aMap
ou aSet
).la source
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)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 ...List
etMutableList
sans raison.Vous pouvez essayer quelque chose comme ceci:
Dans une classe de données, vous devez marquer les paramètres du constructeur principal avec
val
ouvar
.J'attribue la valeur de
_value
àvalue
afin 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.
la source
La réponse dépend des fonctionnalités que vous utilisez réellement
data
. @EPadron a mentionné une astuce astucieuse (version améliorée):Cette volonté fonctionne comme prévu, ei il a un champ, un getter, à droite
equals
,hashcode
etcomponent1
. Le hic, c'est çatoString
etcopy
c'est bizarre:Pour résoudre le problème,
toString
vous pouvez le redéfinir à la main. Je ne connais aucun moyen de corriger la dénomination des paramètres mais de ne pas l'utiliserdata
du tout.la source
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:
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.
la source
val value = test.getValue()
et pas comme les autres gettersval value = test.value
.getValue()
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:
la source
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.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 ()
la source
J'ai trouvé que ce qui suit était la meilleure approche pour réaliser ce dont vous avez besoin sans vous casser
equals
ethashCode
:cependant,
Tout d'abord, notez que ce
_value
n'estvar
pas le casval
, 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 etTestData(0).toString() == TestData(-1).toString()
.la source
_value
est en cours de modification dans le bloc d'initialisation etequals
ethashCode
ne sont pas interrompus.