web-dev-qa-db-fra.com

Un nouveau champ booléen est-il meilleur qu'une référence nulle lorsqu'une valeur peut être significativement absente?

Par exemple, supposons que j'ai une classe, Member, qui a un lastChangePasswordTime:

class Member{
  .
  .
  .
  constructor(){
    this.lastChangePasswordTime=null,
  }
}

dont lastChangePasswordTime peut être significativement absent, car certains membres peuvent ne jamais changer leurs mots de passe.

Mais selon Si les valeurs nulles sont mauvaises, que faut-il utiliser lorsqu'une valeur peut être significativement absente? et https://softwareengineering.stackexchange.com/a/12836/248528, je ne devrais pas utiliser null pour représenter une valeur significativement absente. J'essaie donc d'ajouter un drapeau booléen:

class Member{
  .
  .
  .
  constructor(){
    this.isPasswordChanged=false,
    this.lastChangePasswordTime=null,
  }
}

Mais je pense que c'est assez obsolète parce que:

  1. Lorsque isPasswordChanged est false, lastChangePasswordTime doit être null, et la vérification de lastChangePasswordTime == null est presque identique à la vérification de isPasswordChanged est false, donc je préfère vérifier lastChangePasswordTime == null directement

  2. Lorsque je change la logique ici, je peux oublier de mettre à jour les deux champs .

Remarque: lorsqu'un utilisateur change de mot de passe, j'enregistre l'heure comme ceci:

this.lastChangePasswordTime=Date.now();

Le champ booléen supplémentaire est-il meilleur qu'une référence nulle ici?

39
ocomfd

Je ne vois pas pourquoi, si vous avez une valeur significativement absente, null ne devrait pas être utilisé si vous êtes délibéré et prudent à ce sujet.

Si votre objectif est d'entourer la valeur nullable pour éviter de la référencer accidentellement, je suggère de créer la valeur isPasswordChanged en tant que fonction ou propriété qui renvoie le résultat d'une vérification null, par exemple:

class Member {
    DateTime lastChangePasswordTime = null;
    bool isPasswordChanged() { return lastChangePasswordTime != null; }

}

À mon avis, le faire de cette façon:

  • Donne une meilleure lisibilité du code qu'une vérification nulle, ce qui pourrait perdre le contexte.
  • Supprime le besoin d'avoir à vous soucier du maintien de la valeur isPasswordChanged que vous mentionnez.

La façon dont vous conservez les données (vraisemblablement dans une base de données) serait responsable de garantir que les valeurs nulles sont préservées.

71
TZHX

nulls ne sont pas mauvais. Les utiliser sans réfléchir est. C'est un cas où null est exactement la bonne réponse - il n'y a pas de date.

Notez que votre solution crée plus de problèmes. Quelle est la signification de la date définie sur quelque chose, mais le isPasswordChanged est faux? Vous venez de créer un cas d'informations conflictuelles que vous devez capturer et traiter spécialement, tandis qu'une valeur null a une signification clairement définie et non ambiguë et ne peut pas être en conflit avec d'autres informations.

Alors non, votre solution n'est pas meilleure. Permettre une valeur null est la bonne approche ici. Les gens qui prétendent que null est toujours mauvais, peu importe le contexte, ne comprennent pas pourquoi null existe.

63
Tom

Selon votre langage de programmation, il peut y avoir de bonnes alternatives, comme un type de données optional. En C++ (17), ce serait std :: facultatif . Il peut être absent ou avoir une valeur arbitraire du type de données sous-jacent.

29
dasmy

Utilisation correcte de null

Il existe différentes manières d'utiliser null. La manière la plus courante et sémantiquement correcte est de l'utiliser lorsque vous pouvez ou non avoir une valeur unique . Dans ce cas, une valeur est égale à null ou est quelque chose de significatif comme un enregistrement de base de données ou quelque chose.

Dans ces situations, vous l'utilisez ensuite principalement comme ceci (en pseudo-code):

if (value is null) {
  doSomethingAboutIt();
  return;
}

doSomethingUseful(value);

Problème

Et il a un très gros problème. Le problème est qu'au moment où vous appelez doSomethingUseful, la valeur n'a peut-être pas été vérifiée pour null! Si ce n'est pas le cas, le programme se bloquera probablement. Et l'utilisateur peut même ne pas voir de bons messages d'erreur, se retrouver avec quelque chose comme "horrible erreur: valeur recherchée mais nulle!" (après la mise à jour: bien qu'il puisse y avoir une erreur encore moins informative comme Segmentation fault. Core dumped., ou pire encore, aucune erreur et manipulation incorrecte sur null dans certains cas)

Oublier d'écrire des chèques pour null et de gérer null situations est un bogue extrêmement courant. C'est pourquoi Tony Hoare qui a inventé null a déclaré lors d'une conférence sur les logiciels appelée QCon Londres en 2009 qu'il avait fait l'erreur d'un milliard de dollars en 1965: https://www.infoq.com/presentations/ Null-References-The-Billion-Dollar-Mistake-Tony-Hoare

Éviter le problème

Certaines technologies et langages rendent impossible la vérification de null de différentes manières, ce qui réduit le nombre de bogues.

Par exemple, Haskell a la monade Maybe au lieu de null. Supposons que DatabaseRecord est un type défini par l'utilisateur. Dans Haskell, une valeur de type Maybe DatabaseRecord Peut être égale à Just <somevalue> Ou elle peut être égale à Nothing. Vous pouvez ensuite l'utiliser de différentes manières, mais peu importe comment vous l'utilisez, vous ne pouvez pas appliquer une opération sur Nothing sans le savoir.

Par exemple, cette fonction appelée zeroAsDefault renvoie x pour Just x Et 0 Pour Nothing:

zeroAsDefault :: Maybe Int -> Int
zeroAsDefault mx = case mx of
    Nothing -> 0
    Just x -> x

Christian Hackl dit que C++ 17 et Scala ont leurs propres moyens. Donc, vous voudrez peut-être essayer de savoir si votre langue a quelque chose comme ça et l'utiliser.

Les nuls sont encore largement utilisés

Si vous n'avez rien de mieux, utiliser null est très bien. Continuez à faire attention. Les déclarations de type dans les fonctions vous aideront de toute façon quelque peu.

Cela peut aussi sembler peu progressif, mais vous devriez vérifier si vos collègues veulent utiliser null ou autre chose. Ils peuvent être conservateurs et ne pas vouloir utiliser de nouvelles structures de données pour certaines raisons. Par exemple, prendre en charge les anciennes versions d'une langue. Ces éléments doivent être déclarés dans les normes de codage du projet et discutés correctement avec l'équipe.

Sur votre proposition

Vous proposez d'utiliser un champ booléen distinct. Mais vous devez quand même le vérifier et vous pouvez toujours oublier de le vérifier. Il n'y a donc rien gagné ici. Si vous pouvez même oublier autre chose, comme mettre à jour les deux valeurs à chaque fois, alors c'est encore pire. Si le problème d'oubli de vérifier pour null n'est pas résolu alors il n'y a pas de point. Éviter null est difficile et vous ne devez pas le faire d'une manière qui ne fait qu'empirer les choses.

Comment ne pas utiliser null

Enfin, il existe des façons courantes d'utiliser incorrectement null. L'une de ces méthodes consiste à l'utiliser à la place de structures de données vides telles que des tableaux et des chaînes. Un tableau vide est un tableau approprié comme les autres! Il est presque toujours important et utile que les structures de données, qui peuvent contenir plusieurs valeurs, puissent être vides, c'est-à-dire avoir une longueur nulle.

Du point de vue de l'algèbre, une chaîne vide pour les chaînes ressemble beaucoup à 0 pour les nombres, c'est-à-dire l'identité:

a+0=a
concat(str, '')=str

Une chaîne vide permet aux chaînes en général de devenir monoïdes: https://en.wikipedia.org/wiki/Monoid Si vous ne les obtenez pas, ce n'est pas si important pour vous.

Voyons maintenant pourquoi c'est important pour la programmation avec cet exemple:

for (element in array) {
  doSomething(element);
}

Si nous passons un tableau vide ici, le code fonctionnera correctement. Cela ne fera rien. Cependant, si nous transmettons un null ici, nous obtiendrons probablement un plantage avec une erreur comme "impossible de parcourir null, désolé". Nous pourrions l'envelopper dans if mais c'est moins propre et encore une fois, vous vous pourriez oublier de le vérifier

Comment gérer null

Ce que doSomethingAboutIt() doit faire et surtout s'il doit lever une exception est un autre problème compliqué. En bref, cela dépend si null était une valeur d'entrée acceptable pour une tâche donnée et ce qui est attendu en réponse. Les exceptions concernent les événements qui n'étaient pas attendus. Je n'irai pas plus loin dans ce sujet. Cette réponse est déjà très longue.

11
Gherman

En plus de toutes les très bonnes réponses précédemment données, j'ajouterais que chaque fois que vous êtes tenté de laisser un champ être nul, réfléchissez bien s'il peut s'agir d'un type de liste. Un type nullable est de manière équivalente une liste de 0 ou 1 éléments, et souvent cela peut être généralisé à une liste de N éléments. Plus précisément dans ce cas, vous voudrez peut-être considérer que lastChangePasswordTime est une liste de passwordChangeTimes.

4
Joel

Demandez-vous ceci: quel comportement nécessite le champ lastChangePasswordTime?

Si vous avez besoin de ce champ pour une méthode IsPasswordExpired () pour déterminer si un membre doit être invité à modifier son mot de passe de temps en temps, je définirais le champ à l'heure à laquelle le membre a été initialement créé. L'implémentation IsPasswordExpired () est la même pour les membres nouveaux et existants.

class Member{
   private DateTime lastChangePasswordTime;

   public Member(DateTime lastChangePasswordTime) {
      // set value, maybe check for null
   }

   public bool IsPasswordExpired() {
      DateTime limit = DateTime.Now.AddMonths(-3);
      return lastChangePasswordTime < limit;
   }
}

Si vous avez une exigence distincte que les membres nouvellement créés ont pour mettre à jour leur mot de passe, j'ajouterais un champ booléen séparé appelé passwordShouldBeChanged et le définirais sur true lors de la création. Je changerais alors la fonctionnalité de la méthode IsPasswordExpired () pour inclure une vérification pour ce champ (et renommer la méthode en ShouldChangePassword).

class Member{
   private DateTime lastChangePasswordTime;
   private bool passwordShouldBeChanged;

   public Member(DateTime lastChangePasswordTime, bool passwordShouldBeChanged) {
      // set values, maybe check for nulls
   }

   public bool ShouldChangePassword() {
      return PasswordExpired(lastChangePasswordTime) || passwordShouldBeChanged;
   }

   private static bool PasswordExpired(DateTime lastChangePasswordTime) {
      DateTime limit = DateTime.Now.AddMonths(-3);
      return lastChangePasswordTime < limit;
   }
}

Rendez vos intentions explicites dans le code.

2
Rik D

Premièrement, le fait d'être nul est un dogme et comme d'habitude avec le dogme, il fonctionne mieux comme guide et non comme test de réussite/échec.

Deuxièmement, vous pouvez redéfinir votre situation de manière à ce qu'il soit logique que la valeur ne puisse jamais être nulle. InititialPasswordChanged est un booléen initialement défini sur false, PasswordSetTime est la date et l'heure auxquelles le mot de passe actuel a été défini.

Notez que bien que cela ait un léger coût, vous pouvez maintenant TOUJOURS calculer le temps écoulé depuis la dernière définition d'un mot de passe.

2
jmoreno

Les deux sont "sûrs/sensés/corrects" si l'appelant vérifie avant utilisation. Le problème est de savoir ce qui se passe si l'appelant ne vérifie pas. Quel est le meilleur, une certaine saveur d'erreur nulle ou l'utilisation d'une valeur non valide?

Il n'y a pas de réponse correcte unique. Cela dépend de ce qui vous inquiète.

Si les plantages sont vraiment mauvais mais que la réponse n'est pas critique ou a une valeur par défaut acceptée, il est peut-être préférable d'utiliser un booléen comme indicateur. Si l'utilisation d'une mauvaise réponse est un problème pire que le plantage, il est préférable d'utiliser null.

Pour la majorité des cas "typiques", l'échec rapide et le fait de forcer les appelants à vérifier est le moyen le plus rapide de coder sainement, et donc je pense que null devrait être le choix par défaut. Je ne ferais pas trop confiance aux évangélistes "X est la racine de tous les méchants"; ils n'ont normalement pas anticipé tous les cas d'utilisation.

1
ANone