Attribution: cela découle d'une question connexe de P.SE
Mes antécédents sont en C / C ++, mais j'ai beaucoup travaillé en Java et je suis en train de coder C #. En raison de mon expérience en C, la vérification des pointeurs passés et retournés est de seconde main, mais je reconnais que cela biaise mon point de vue.
J'ai récemment vu la mention du modèle d'objet nul où l'idée est qu'un objet est toujours retourné. Le cas normal renvoie l'objet attendu et rempli et le cas d'erreur renvoie un objet vide au lieu d'un pointeur nul. La prémisse étant que la fonction appelante aura toujours une sorte d'objet à accéder et évitera donc les violations de mémoire à accès nul.
- Alors, quels sont les avantages / inconvénients d'une vérification nulle par rapport à l'utilisation du modèle d'objet nul?
Je peux voir un code d'appel plus propre avec le NOP, mais je peux aussi voir où cela créerait des échecs cachés qui autrement ne seraient pas déclenchés. Je préférerais que mon application échoue (alias une exception) pendant que je la développe plutôt qu'une fuite silencieuse d'erreur dans la nature.
- Le modèle d'objet nul ne peut-il pas avoir des problèmes similaires à celui de ne pas effectuer de vérification nulle?
Beaucoup d'objets avec lesquels j'ai travaillé contiennent des objets ou des conteneurs qui leur sont propres. Il semble que je devrais avoir un cas spécial pour garantir que tous les conteneurs de l'objet principal avaient leurs propres objets vides. Il semble que cela puisse devenir moche avec plusieurs couches d'imbrication.
la source
Réponses:
Vous n'utiliseriez pas un modèle d'objet nul dans les endroits où null (ou objet nul) est renvoyé en raison d'une défaillance catastrophique. Dans ces endroits, je continuerais de retourner nul. Dans certains cas, s'il n'y a pas de récupération, vous pouvez aussi bien planter car au moins le vidage sur incident indiquera exactement où le problème s'est produit. Dans de tels cas, lorsque vous ajoutez votre propre gestion des erreurs, vous allez toujours tuer le processus (encore une fois, je l'ai dit pour les cas où il n'y a pas de récupération), mais votre gestion des erreurs masquera des informations très importantes qu'un vidage sur incident aurait fourni.
Null Object Pattern est plus pour les endroits où il y a un comportement par défaut qui pourrait être pris dans le cas où un objet n'est pas trouvé. Par exemple, considérez ce qui suit:
Si vous utilisez NOP, vous écririez:
Notez que le comportement de ce code est "si Bob existe, je veux mettre à jour son adresse". De toute évidence, si votre application nécessite la présence de Bob, vous ne voulez pas réussir en silence. Mais il y a des cas où ce type de comportement serait approprié. Et dans ces cas, NOP ne produit-il pas un code beaucoup plus propre et concis?
Dans les endroits où vous ne pouvez vraiment pas vivre sans Bob, GetUser () lèverait une exception d'application (c'est-à-dire pas une violation d'accès ou quelque chose comme ça) qui serait gérée à un niveau supérieur et signalerait un échec général de l'opération. Dans ce cas, il n'y a pas besoin de NOP mais il n'est pas non plus nécessaire de vérifier explicitement NULL. IMO, ces vérifications pour NULL, ne font qu'agrandir le code et enlèvent la lisibilité. Vérifier NULL est toujours le bon choix de conception pour certaines interfaces, mais pas autant que certaines personnes le pensent.
la source
Avantages
Les inconvénients
Ce dernier "pro" est le principal facteur de différenciation (d'après mon expérience) quant au moment où chacun doit être appliqué. "L'échec devrait-il être bruyant?". Dans certains cas, vous voulez qu'un échec soit dur et immédiat; si un scénario qui ne devrait jamais se produire arrive. Si une ressource vitale n'a pas été trouvée ... etc. Dans certains cas, vous êtes d'accord avec un défaut sain car ce n'est pas vraiment une erreur : obtenir une valeur à partir d'un dictionnaire, mais la clé est manquante par exemple.
Comme toute autre décision de conception, il y a des avantages et des inconvénients selon vos besoins.
la source
Je ne suis pas une bonne source d'informations objectives, mais subjectivement:
Dans un environnement comme Java ou C #, cela ne vous donnera pas le principal avantage d'explicitness - la sécurité de la solution étant complète, car vous pouvez toujours recevoir null à la place de tout objet dans le système et Java n'a aucun moyen (sauf les annotations personnalisées pour Beans) , etc.) pour vous en empêcher, à l'exception des if explicites. Mais dans le système exempt d'implémentations nulles, l'approche de la solution du problème de cette manière (avec des objets représentant des cas exceptionnels) offre tous les avantages mentionnés. Essayez donc de lire autre chose que les langages traditionnels et vous vous retrouverez à changer vos méthodes de codage.
En général, les objets Null / Error / types de retour sont considérés comme une stratégie de gestion des erreurs très solide et assez mathématiquement solide, tandis que la stratégie de gestion des exceptions de Java ou C va comme une étape au-dessus de la stratégie "die ASAP" - donc laissez fondamentalement toute la charge de instabilité et imprévisibilité du système sur le développeur ou le mainteneur.
Il existe également des monades qui peuvent être considérées comme supérieures à cette stratégie (également supérieures dans la complexité de la compréhension au début), mais celles-ci concernent davantage les cas où vous devez modéliser des aspects externes de type, tandis que Null Object modélise des aspects internes. Donc NonExistingUser est probablement mieux modélisé comme monade peut-être_existant.
Et enfin, je ne pense pas qu'il existe un lien entre le modèle Null Object et la gestion silencieuse des situations d'erreur. Cela devrait être l'inverse - cela devrait vous obliger à modéliser chaque cas d'erreur de manière explicite et à le traiter en conséquence. Maintenant, bien sûr, vous pourriez décider d'être bâclé (pour quelque raison que ce soit) et d'ignorer la gestion explicite du cas d'erreur, mais de le gérer de manière générique - eh bien, c'était votre choix explicite.
la source
Je pense qu'il est intéressant de noter que la viabilité d'un objet nul semble être un peu différente selon que votre langue est ou non typée statiquement. Ruby est un langage qui implémente un objet pour Null (ou
Nil
dans la terminologie du langage).Au lieu de renvoyer un pointeur vers la mémoire non initialisée, la langue renvoie l'objet
Nil
. Ceci est facilité par le fait que la langue est typée dynamiquement. Il n'est pas garanti qu'une fonction renvoie toujours unint
. Il peut revenirNil
si c'est ce qu'il doit faire. Cela devient plus compliqué et difficile de le faire dans un langage de type statique, car vous vous attendez naturellement à ce que null soit applicable à tout objet pouvant être appelé par référence. Vous devez commencer à implémenter des versions nulles de tout objet qui pourrait être nul (ou suivre la route Scala / Haskell et avoir une sorte de wrapper "Peut-être").MrLister a évoqué le fait qu'en C ++, il n'est pas garanti d'avoir une référence / un pointeur d'initialisation lors de sa première création. En ayant un objet null dédié au lieu d'un pointeur null brut, vous pouvez obtenir de belles fonctionnalités supplémentaires. Ruby
Nil
comprend une fonctionto_s
(toString) qui se traduit par du texte vierge. C'est très bien si cela ne vous dérange pas d'obtenir un retour nul sur une chaîne et que vous souhaitez ignorer le signalement sans planter. Les classes ouvertes vous permettent également de remplacer manuellement la sortie deNil
si besoin est.Certes, tout cela peut être pris avec un grain de sel. Je crois que dans un langage dynamique, l'objet nul peut être très pratique lorsqu'il est utilisé correctement. Malheureusement, pour en tirer le meilleur parti, vous devrez peut-être modifier ses fonctionnalités de base, ce qui pourrait rendre votre programme difficile à comprendre et à maintenir pour les personnes habituées à travailler avec ses formulaires plus standard.
la source
Pour un point de vue supplémentaire, jetez un œil à Objective-C, où au lieu d'avoir un objet Null, le type de valeur nil fonctionne comme un objet. Tout message envoyé à un objet nil n'a aucun effet et renvoie une valeur de 0 / 0,0 / NO (faux) / NULL / nil, quel que soit le type de valeur de retour de la méthode. Ceci est utilisé comme un modèle très omniprésent dans la programmation MacOS X et iOS, et généralement les méthodes seront conçues pour qu'un objet nul retournant 0 soit un résultat raisonnable.
Par exemple, si vous souhaitez vérifier si une chaîne contient un ou plusieurs caractères, vous pouvez écrire
et obtenir un résultat raisonnable si aString == nil, car une chaîne inexistante ne contient aucun caractère. Les gens ont tendance à prendre un certain temps pour s'y habituer, mais il me faudrait probablement un certain temps pour m'habituer à vérifier les valeurs nulles partout dans d'autres langues.
(Il existe également un objet singleton de classe NSNull qui se comporte assez différemment et n'est pas très utilisé en pratique).
la source
nil
get#define
'd à NULL et se comporte comme un objet nul. Il s'agit d'une fonctionnalité d'exécution alors qu'en C # / Java, les pointeurs nuls émettent des erreurs.N'utilisez pas non plus si vous pouvez l'éviter. Utilisez une fonction d'usine renvoyant un type d'option, un tuple ou un type de somme dédié. En d'autres termes, représentent une valeur potentiellement par défaut avec un type différent d'une valeur garantie par défaut.
D'abord, listons les desiderata puis réfléchissons à la façon dont nous pouvons l'exprimer dans quelques langages (C ++, OCaml, Python)
grep
pour rechercher des erreurs potentielles.Je pense que la tension entre le modèle d'objet nul et les pointeurs nuls vient de (5). Si nous pouvons détecter les erreurs assez tôt, cependant, (5) devient théorique.
Considérons cette langue par langue:
C ++
À mon avis, une classe C ++ devrait généralement être constructible par défaut car elle facilite les interactions avec les bibliothèques et facilite l'utilisation de la classe dans les conteneurs. Cela simplifie également l'héritage car vous n'avez pas besoin de penser au constructeur de superclasse à appeler.
Cependant, cela signifie que vous ne pouvez pas savoir avec certitude si une valeur de type
MyClass
est dans "l'état par défaut" ou non. En plus de mettre unbool nonempty
champ ou un champ similaire pour faire apparaître la valeur par défaut lors de l'exécution, le mieux que vous puissiez faire est de produire de nouvelles instances deMyClass
manière à obliger l'utilisateur à le vérifier .Je recommanderais d'utiliser une fonction d'usine qui renvoie un
std::optional<MyClass>
,std::pair<bool, MyClass>
ou une référence de valeur r à unstd::unique_ptr<MyClass>
si possible.Si vous voulez que votre fonction d'usine retourne une sorte d'état "d'espace réservé" qui est différent d'un construit par défaut
MyClass
, utilisez unstd::pair
et assurez-vous de documenter que votre fonction fait cela.Si la fonction d'usine a un nom unique, il est facile de
grep
rechercher le nom et de rechercher la négligence. Cependant, il est difficile de trouvergrep
des cas où le programmeur aurait dû utiliser la fonction d'usine mais ne l'a pas fait .OCaml
Si vous utilisez un langage comme OCaml, vous pouvez simplement utiliser un
option
type (dans la bibliothèque standard OCaml) ou uneither
type (pas dans la bibliothèque standard, mais facile à rouler le vôtre). Ou undefaultable
type (j'invente le termedefaultable
).Un
defaultable
comme indiqué ci-dessus est meilleur qu'une paire car l'utilisateur doit correspondre à un modèle pour extraire le'a
et ne peut pas simplement ignorer le premier élément de la paire.L'équivalent du
defaultable
type indiqué ci-dessus peut être utilisé en C ++ en utilisant unstd::variant
avec deux instances du même type, mais des parties de l'std::variant
API sont inutilisables si les deux types sont identiques. C'est aussi une utilisation étrangestd::variant
car ses constructeurs de types sont sans nom.Python
De toute façon, vous n'obtenez aucune vérification à la compilation pour Python. Mais, étant typé dynamiquement, il n'y a généralement pas de circonstances où vous avez besoin d'une instance d'espace réservé d'un certain type pour apaiser le compilateur.
Je recommanderais simplement de lever une exception lorsque vous seriez obligé de créer une instance par défaut.
Si ce n'est pas acceptable, je recommanderais de créer un
DefaultedMyClass
qui hériteMyClass
et de le renvoyer de votre fonction d'usine. Cela vous offre de la flexibilité en termes de fonctionnalité de "stubbing out" dans les instances par défaut si vous en avez besoin.la source