Je viens d'avoir une discussion sur un choix de conception après une revue de code. Je me demande quelles sont vos opinions.
Il y a cette classe Preferences
, qui est un compartiment pour les paires clé-valeur. Les valeurs nulles sont légales (c'est important). Nous nous attendons à ce que certaines valeurs ne soient pas encore enregistrées, et nous voulons gérer ces cas automatiquement en les initialisant avec une valeur par défaut prédéfinie lorsque cela est demandé.
La solution discutée a utilisé le modèle suivant (REMARQUE: ce n'est pas le code réel, évidemment - il est simplifié à des fins d'illustration):
public class Preferences {
// null values are legal
private Map<String, String> valuesFromDatabase;
private static Map<String, String> defaultValues;
class KeyNotFoundException extends Exception {
}
public String getByKey(String key) {
try {
return getValueByKey(key);
} catch (KeyNotFoundException e) {
String defaultValue = defaultValues.get(key);
valuesFromDatabase.put(key, defaultvalue);
return defaultValue;
}
}
private String getValueByKey(String key) throws KeyNotFoundException {
if (valuesFromDatabase.containsKey(key)) {
return valuesFromDatabase.get(key);
} else {
throw new KeyNotFoundException();
}
}
}
Il a été critiqué comme un anti-modèle - abusant des exceptions pour contrôler le flux . KeyNotFoundException
- réalisé pour ce seul cas d'utilisation - ne sortira jamais du cadre de cette classe.
Il s'agit essentiellement de deux méthodes pour jouer avec lui simplement pour communiquer quelque chose.
La clé qui n'est pas présente dans la base de données n'est pas quelque chose d'alarmant ou d'exceptionnel - nous nous attendons à ce que cela se produise chaque fois qu'un nouveau paramètre de préférence est ajouté, d'où le mécanisme qui l'initialise gracieusement avec une valeur par défaut si nécessaire.
Le contre-argument était que getValueByKey
- la méthode privée - telle que définie en ce moment n'a aucun moyen naturel d'informer la méthode publique sur les deux la valeur , et si la clé était là. (Si ce n'est pas le cas, il doit être ajouté pour que la valeur puisse être mise à jour).
Renvoyer null
serait ambigu, puisque null
est une valeur parfaitement légale, donc on ne sait pas si cela signifiait que la clé n'était pas là, ou s'il y avait un null
.
getValueByKey
devrait renvoyer une sorte de Tuple<Boolean, String>
, le booléen étant défini sur true si la clé est déjà là, afin que nous puissions faire la distinction entre (true, null)
et (false, null)
. (Un paramètre out
pourrait être utilisé en C #, mais c'est Java).
Est-ce une alternative plus agréable? Oui, il faudrait définir une classe à usage unique à l'effet de Tuple<Boolean, String>
, Mais ensuite nous nous débarrassons de KeyNotFoundException
, donc ce genre d'équilibres. Nous évitons également les frais généraux liés à la gestion d'une exception, bien que cela ne soit pas significatif en termes pratiques - il n'y a pas de considérations de performances à proprement parler, c'est une application cliente et ce n'est pas comme si les préférences des utilisateurs seraient récupérées des millions de fois par seconde.
Une variante de cette approche pourrait utiliser le Optional<String>
De Guava (la goyave est déjà utilisée tout au long du projet) au lieu de certains Tuple<Boolean, String>
Personnalisés, et nous pourrons alors faire la différence entre Optional.<String>absent()
et "proper" "null
. Il semble toujours hackeux, cependant, pour des raisons qui sont faciles à voir - l'introduction de deux niveaux de "nullité" semble abuser du concept qui était à l'origine de la création de Optional
s en premier lieu.
Une autre option serait de vérifier explicitement si la clé existe (ajouter une méthode boolean containsKey(String key)
et appeler getValueByKey
uniquement si nous avons déjà affirmé qu'elle existe).
Enfin, on pourrait également incorporer la méthode privée, mais le getByKey
réel est un peu plus complexe que mon exemple de code, donc l'inliner le rendrait assez méchant.
Je divise peut-être les cheveux ici, mais je suis curieux de savoir sur quoi vous parieriez pour être le plus proche des meilleures pratiques dans ce cas. Je n'ai pas trouvé de réponse dans les guides de style d'Oracle ou de Google.
L'utilisation d'exceptions comme dans l'exemple de code est-elle un anti-modèle, ou est-ce acceptable étant donné que les alternatives ne sont pas très propres non plus? Si c'est le cas, dans quelles circonstances cela irait-il bien? Et vice versa?
Oui, votre collègue a raison: c'est du mauvais code. Si une erreur peut être gérée localement, elle doit être gérée immédiatement. Une exception ne doit pas être levée, puis gérée immédiatement.
C'est beaucoup plus propre que votre version (la méthode getValueByKey()
est supprimée):
public String getByKey(String key) {
if (valuesFromDatabase.containsKey(key)) {
return valuesFromDatabase.get(key);
} else {
String defaultValue = defaultValues.get(key);
valuesFromDatabase.put(key, defaultvalue);
return defaultValue;
}
}
Une exception ne doit être levée que si vous ne savez pas comment résoudre l'erreur localement.
Je n'appellerais pas cette utilisation des exceptions un anti-modèle, mais pas la meilleure solution au problème de la communication d'un résultat complexe.
La meilleure solution (en supposant que vous êtes toujours sur Java 7) serait d'utiliser l'option facultative de Guava; je ne suis pas d'accord que son utilisation dans ce cas serait hackish. Il me semble, sur la base de - explication détaillée de Guava de Facultatif , que ceci est un exemple parfait de quand l'utiliser. Vous différenciez entre "aucune valeur trouvée" et "valeur de null trouvé".
Puisqu'il n'y a pas de considérations de performances et que c'est un détail d'implémentation, peu importe la solution que vous choisissez. Mais je dois convenir que c'est un mauvais style; la clé étant absente est quelque chose que vous savez se produira , et vous ne le gérez même pas plus d'un appel à la pile, ce qui est le cas où les exceptions sont les plus utiles.
L'approche Tuple est un peu hacky car le deuxième champ n'a pas de sens lorsque le booléen est faux. Vérifier si la clé existe au préalable est idiot car la carte recherche la clé deux fois. (Eh bien, vous le faites déjà, donc dans ce cas trois fois.) La solution Optional
est parfaitement adaptée au problème. Cela peut sembler un peu ironique de stocker un null
dans un Optional
, mais si c'est ce que l'utilisateur veut faire, vous ne pouvez pas dire non.
Comme l'a noté Mike dans les commentaires, cela pose un problème; ni Goyave ni Java 8's Optional
permet de stocker null
s. Ainsi, vous auriez besoin de rouler le vôtre qui - bien que simple - implique une bonne quantité de passe-partout, donc peut-être exagéré pour quelque chose qui ne sera utilisé qu'une seule fois en interne. Vous pouvez également changer le type de la carte en Map<String, Optional<String>>
, mais la gestion Optional<Optional<String>>
s devient maladroit.
Un compromis raisonnable pourrait consister à maintenir l'exception, mais à reconnaître son rôle de "retour alternatif". Créez une nouvelle exception vérifiée avec la trace de pile désactivée et jetez-en une instance pré-créée que vous pouvez conserver dans une variable statique. C'est bon marché - peut-être aussi bon marché qu'une branche locale - et vous ne pouvez pas oublier de le gérer, donc le manque de trace de pile n'est pas un problème.
Il y a cette classe Preferences, qui est un compartiment pour les paires clé-valeur. Les valeurs nulles sont légales (c'est important). Nous nous attendons à ce que certaines valeurs ne soient pas encore enregistrées, et nous voulons gérer ces cas automatiquement en les initialisant avec une valeur par défaut prédéfinie lorsque cela est demandé.
Le problème est exactement celui-ci. Mais vous avez déjà publié la solution vous-même:
Une variante de cette approche pourrait utiliser l'option facultative de la goyave (la goyave est déjà utilisée tout au long du projet) au lieu d'un tuple personnalisé, et nous pourrons alors faire la différence entre Optional.absent () et " propre "null . Il semble toujours hackeux, cependant, pour des raisons qui sont faciles à voir - l'introduction de deux niveaux de "nullité" semble abuser du concept qui était à l'origine de la création d'Optionals en premier lieu.
Cependant, n'utilisez pas null
ou Optional
. Vous pouvez et devez utiliser uniquement Optional
. Pour votre sentiment de "hackish", utilisez-le simplement imbriqué, de sorte que vous vous retrouvez avec Optional<Optional<String>>
Qui rend explicite qu'il pourrait y avoir une clé dans la base de données (première couche d'options) et qu'elle pourrait contenir une valeur prédéfinie (deuxième couche d'option).
Cette approche est meilleure que l'utilisation d'exceptions et elle est assez facile à comprendre tant que Optional
n'est pas nouveau pour vous.
Veuillez également noter que Optional
a quelques fonctions de confort, de sorte que vous n'avez pas à faire tout le travail vous-même. Ceux-ci inclus:
static static <T> Optional<T> fromNullable(T nullableReference)
pour convertir votre entrée de base de données en types Optional
abstract T or(T defaultValue)
pour obtenir la valeur de clé de la couche d'options interne ou (si elle n'est pas présente) obtenir votre valeur de clé par défautJe sais que je suis en retard à la fête, mais de toute façon votre cas d'utilisation ressemble à comment Java's Properties
permet également de définir un ensemble de propriétés par défaut, qui sera vérifié s'il n'y a pas de clé correspondante chargé par l'instance.
En regardant comment l'implémentation est faite pour Properties.getProperty(String)
(de Java 7):
Object oval = super.get(key);
String sval = (oval instanceof String) ? (String)oval : null;
return ((sval == null) && (defaults != null)) ? defaults.getProperty(key) : sval;
Il n'est vraiment pas nécessaire "d'abuser des exceptions pour contrôler le flux", comme vous l'avez cité.
Une approche similaire, mais un peu plus concise, de la réponse de @ BЈовић peut également être:
public String getByKey(String key) {
if (!valuesFromDatabase.containsKey(key)) {
valuesFromDatabase.put(key, defaultValues.get(key));
}
return valuesFromDatabase.get(key);
}
Bien que je pense que la réponse de @ BЈовић est correcte dans le cas où getValueByKey
n'est nécessaire nulle part ailleurs, je ne pense pas que votre solution soit mauvaise dans le cas où votre programme contient les deux cas d'utilisation:
récupération par clé avec création automatique au cas où la clé n'existerait pas au préalable, et
récupération sans cet automatisme, sans rien changer dans la base de données, le référentiel ou le mappage de clés (pensez à getValueByKey
étant public, pas privé)
Si tel est votre cas et tant que les performances sont acceptables, je pense que la solution que vous proposez est tout à fait correcte. Il a l'avantage d'éviter la duplication du code de récupération, il ne repose pas sur des frameworks tiers, et c'est assez simple (du moins à mes yeux).
En effet, dans une telle situation, en dépend du contexte si une clé manquante est une situation "exceptionnelle" ou non. Pour un contexte où il se trouve, quelque chose comme getValueByKey
est nécessaire. Pour un contexte où la création de clé automatique est attendue, fournir une méthode qui réutilise la fonction déjà disponible, avale l'exception et fournit un comportement de défaillance différent, est parfaitement logique. Cela peut être interprété comme une extension ou un "décorateur" de getValueByKey
, pas tant qu'une fonction où "l'exception est abusée pour le flux de contrôle".
Bien sûr, il existe une troisième alternative: créer une troisième méthode privée renvoyant le Tuple<Boolean, String>
, comme vous l'avez suggéré, et réutilisez cette méthode dans getValueByKey
et getByKey
. Pour un cas plus compliqué qui pourrait en effet être la meilleure alternative, mais pour un cas aussi simple que celui montré ici, cela a à mon humble avis une odeur de suringénierie, et je doute que le code soit vraiment plus facile à maintenir de cette façon. Je suis ici avec la réponse la plus élevée ici par Karl Bielefeldt :
"vous ne devriez pas vous sentir mal à propos des exceptions quand cela simplifie votre code".
Optional
est la bonne solution. Cependant, si vous préférez, il existe une alternative qui a moins une sensation de "deux null", pensez à une sentinelle.
Définissez une chaîne statique privée keyNotFoundSentinel
, avec une valeur new String("")
. *
Maintenant, la méthode privée peut return keyNotFoundSentinel
Plutôt que throw new KeyNotFoundException()
. La méthode publique peut vérifier cette valeur
String rval = getValueByKey(key);
if (rval == keyNotFoundSentinel) { // reference equality, not string.equals
... // generate default value
} else {
return rval;
}
En réalité, null
est un cas particulier de sentinelle. Il s'agit d'une valeur sentinelle définie par le langage, avec quelques comportements spéciaux (c'est-à-dire un comportement bien défini si vous appelez une méthode sur null
). Il se trouve que c'est tellement utile d'avoir une sentinelle comme celle-là que presque toujours une langue l'utilise.
* Nous utilisons intentionnellement new String("")
plutôt que simplement ""
Pour empêcher Java "d'interner" la chaîne, ce qui lui donnerait la même référence que tout autre vide) Parce que cette étape supplémentaire a été effectuée, nous sommes garantis que la chaîne référencée par keyNotFoundSentinel
est une instance unique, dont nous devons nous assurer qu'elle ne puisse jamais apparaître dans la carte elle-même.
Apprenez du cadre qui a appris de tous les points faibles de Java:
.NET fournit deux solutions beaucoup plus élégantes à ce problème, illustrées par:
Ce dernier est très facile à écrire en Java, le premier nécessite juste une classe d'aide "référence forte".