Un getter doit-il lancer une exception si son objet a un état non valide?


55

Je rencontre souvent ce problème, en particulier en Java, même s’il s’agit d’un problème général de programmation orientée objet. Autrement dit, le fait de lever une exception révèle un problème de conception.

Supposons que j'ai une classe qui a un String namechamp et un String surnamechamp.

Ensuite, il utilise ces champs pour composer le nom complet d'une personne afin de l'afficher sur un type de document, par exemple une facture.

public void String name;
public void String surname;

public String getCompleteName() {return name + " " + surname;}

public void displayCompleteNameOnInvoice() {
    String completeName = getCompleteName();
    //do something with it....
}

Maintenant, je veux renforcer le comportement de ma classe en générant une erreur si le displayCompleteNameOnInvoicenom est appelé avant que le nom ait été attribué. Cela semble une bonne idée, n'est-ce pas?

Je peux ajouter un code d'exception à la getCompleteNameméthode. Mais de cette façon, je viole un contrat «implicite» avec l'utilisateur de la classe. En général, les getters ne sont pas supposés lancer des exceptions si leurs valeurs ne sont pas définies. Ok, ce n’est pas un getter standard car il ne renvoie pas un seul champ, mais du point de vue de l’utilisateur, la distinction peut être trop subtile pour y penser.

Ou je peux jeter l'exception de l'intérieur displayCompleteNameOnInvoice. Mais pour le faire, je dois tester directement nameou surnamechamps et ce faisant, je violeriez l'abstraction représentée par getCompleteName. C'est la responsabilité de cette méthode de vérifier et de créer le nom complet. Il pourrait même décider, en se basant sur d’autres données, que cela suffit dans certains cas surname.

Ainsi, la seule possibilité semble changer la sémantique de la méthode getCompleteNameen composeCompleteName, ce qui suggère un comportement plus «actif» et, avec lui, la capacité de lancer une exception.

Est-ce la meilleure solution de conception? Je recherche toujours le meilleur équilibre entre simplicité et exactitude. Existe-t-il une référence de conception pour ce problème?


8
"En général, les getters ne sont pas supposés lancer des exceptions si leurs valeurs ne sont pas définies." - Qu'est-ce qu'ils sont censés faire alors?
Rotem

2
@scriptin Ensuite, en suivant cette logique, displayCompleteNameOnInvoicepeut simplement lever une exception si getCompleteNameretourne null, n'est-ce pas?
Rotem

2
@Rotem ça peut. La question est de savoir si cela devrait.
scriptin

22
À ce sujet, les programmeurs de Falsehoods Believe About Names expliquent très bien pourquoi «prénom» et «nom de famille» sont des concepts très étroits.
Lars Viklund

9
Si votre objet peut avoir un état invalide, vous avez déjà foiré.
user253751

Réponses:


151

Ne laissez pas votre classe être construite sans assigner un nom.


9
C'est une solution intéressante mais assez radicale. Bien souvent, vos classes doivent être plus fluides, ne permettant des états qui deviennent un problème que si et lorsque certaines méthodes sont appelées, sinon vous vous retrouverez avec une prolifération de petites classes nécessitant trop d'explications.
AgostinoX

71
Ceci n'est pas un commentaire S'il applique le conseil dans la réponse, il n'aura plus le problème décrit. C'est une réponse.
DeadMG

14
@ AgostinoX: Vous ne devriez rendre vos classes fluides que si cela est absolument nécessaire. Sinon, ils devraient être aussi invariants que possible.
DeadMG

25
@gnat: vous faites un excellent travail ici en interrogeant la qualité de chaque question et chaque réponse sur PSE, mais parfois vous êtes à mon humble avis un peu trop fort.
Doc Brown

9
S'ils peuvent être valablement facultatifs, il n'y a aucune raison pour qu'il jette en premier lieu.
DeadMG

75

La réponse fournie par DeadMG la décrit assez bien, mais permettez-moi de la formuler un peu différemment: éviter d'avoir des objets avec un état non valide. Un objet doit être "entier" dans le contexte de la tâche qu'il remplit. Ou cela ne devrait pas exister.

Donc, si vous voulez de la fluidité, utilisez quelque chose comme le motif Builder . Ou tout ce qui est une réification distincte d'un objet en construction par opposition à la "chose réelle", où le dernier est garanti comme ayant un état valide et qui expose en réalité les opérations définies sur cet état (par exemple getCompleteName).


7
So if you want fluidity, use something like the builder pattern- fluidité ou immuabilité (sauf si c'est ce que vous voulez dire). Si l'on veut créer des objets immuables, mais qu'il faille différer la définition de certaines valeurs, il convient de les définir un par un sur un objet générateur et de ne créer un objet immuable que lorsque toutes les valeurs requises ont été rassemblées. État ...
Konrad Morawski

1
@ KonradMorawski: Je suis confus. Etes-vous en train de clarifier ou de contredire ma déclaration? ;)
back2dos 03

3
J'ai essayé de le compléter ...
Konrad Morawski

40

Ne pas avoir d'objet avec un état invalide.

Le but d'un constructeur ou d'un constructeur est de définir l'état de l'objet sur quelque chose de cohérent et utilisable. Sans cette garantie, des problèmes surgissent avec un état incorrectement initialisé dans les objets. Ce problème devient complexe si vous avez affaire à une concurrence et que quelque chose peut accéder à l'objet avant que vous ayez complètement fini de le configurer.

Donc, la question pour cette partie est "pourquoi autorisez-vous le nom ou le nom de famille à être nuls?" Si cela n’est pas valable dans la classe pour que cela fonctionne correctement, ne le permettent pas. Ayez un constructeur ou un générateur qui valide correctement la création de l'objet et si ce n'est pas correct, soulevez le problème à ce stade. Vous pouvez également souhaiter utiliser l’une des @NotNullannotations existantes pour indiquer que "cela ne peut pas être nul" et l’appliquer dans le codage et l’analyse.

Avec cette garantie en place, il devient beaucoup plus facile de raisonner sur le code, sur ce qu’il fait, et de ne pas avoir à lancer des exceptions dans des endroits étranges ou à effectuer des vérifications excessives sur les fonctions d’obtention.

Getters qui font plus.

Il y a pas mal de choses à ce sujet. Vous avez combien de logique dans les Getters et Qu'est - ce qui devrait être autorisé dans les Getters et les setters? à partir d’ici, les getters et les setters effectuent une logique supplémentaire sur Stack Overflow. C'est un problème qui revient sans cesse dans la conception de la classe.

Le noyau de ceci vient de:

en général, les accesseurs ne sont pas supposés lancer des exceptions si leurs valeurs ne sont pas définies

Et tu as raison. Ils ne devraient pas. Ils devraient avoir l'air stupide du reste du monde et faire ce qui est attendu. Mettre trop de logique là-dedans conduit à des problèmes de violation de la loi du moindre étonnement. Et avouons-le, vous ne voulez vraiment pas envelopper les accesseurs avec un try catchparce que nous savons à quel point nous aimons le faire en général.

Il y a aussi des situations où vous devez utiliser une getFoométhode telle que le framework JavaBeans et où vous avez quelque chose d' appels d' EL qui espère obtenir un bean (de sorte que les <% bar.foo %>appels getFoo()dans la classe - en mettant de côté le 'si le bean est en train de faire la composition ou devrait être laissés à la vue? 'car on peut facilement trouver des situations où l'une ou l'autre peut clairement être la bonne réponse)

Sachez également qu’il est possible de spécifier un getter donné dans une interface ou d’avoir fait partie de l’API publique précédemment exposée pour la classe en cours de refactorisation (une version précédente venait juste de recevoir 'completeName' et un refactoring cassé.) en deux champs).

À la fin de la journée...

Faites la chose la plus facile à raisonner. Vous passerez plus de temps à gérer ce code qu'à concevoir (bien que moins vous passez de temps à concevoir, plus vous passerez de temps à le maintenir). Le choix idéal est de le concevoir de manière à ce que l'entretien ne prenne pas autant de temps, mais ne réfléchissez pas pendant des jours, à moins qu'il ne s'agisse vraiment d'un choix de conception qui aura des conséquences plus tard. .

private T foo; T getFoo() { return foo; }Si vous essayez de vous en tenir à la pureté sémantique du getter , vous aurez des problèmes à un moment donné. Parfois, le code ne correspond tout simplement pas à ce modèle et les contorsions que l'on subit pour essayer de le conserver ainsi n'ont tout simplement pas de sens ... et finalement rendent la conception plus difficile.

Acceptez parfois que les idéaux de la conception ne se réalisent pas comme vous le souhaitez dans le code. Les directives sont des directives - pas des chaînes.


2
Évidemment, mon exemple n'était qu'une excuse pour améliorer le style de codage en raisonnant dessus, pas un problème de blocage. Mais je suis assez perplexe devant l’importance que vous et les autres semblez accorder aux constructeurs. En théorie, ils tiennent leurs promesses. En pratique, il devient difficile de conserver l’héritage, inutilisable lorsque vous extrayez une interface d’une classe, non adoptée par javabeans et d’autres frameworks comme printemps, si je ne me trompe pas. Dans le cadre d'une idée de «classe», vivent de nombreuses catégories d'objets. Un objet de valeur peut bien avoir un constructeur de validation, mais ce n'est qu'un type.
AgostinoX

2
Pouvoir raisonner sur le code est essentiel pour pouvoir écrire du code à l'aide d'une API ou pouvoir le conserver. Si une classe a un constructeur qui lui permet de se trouver dans un état invalide, il est beaucoup plus difficile de réfléchir à "bien, qu'est-ce que cela fait?" parce que maintenant vous devez considérer plus de situations que le concepteur de la classe considérée ou destinée. Cette charge conceptuelle supplémentaire entraîne la pénalisation de plus de bugs et un temps plus long pour écrire du code. D'autre part, si vous pouvez regarder le code et dire "le nom et le prénom ne seront jamais nuls", il devient beaucoup plus facile d'écrire du code à l'aide de ces codes.

2
Incomplet ne signifie pas nécessairement invalide. Une facture sans reçu peut être en cours et l’API publique qu’elle présente reflète le fait qu’il s’agit d’un état valide. Si surnameou nameétant null est un état valide pour l'objet, manipulez-le en conséquence. Mais si ce n'est pas un état valide, les méthodes de la classe émettent des exceptions, il faut éviter de se trouver dans cet état à partir du moment où il a été initialisé. Vous pouvez faire passer des objets incomplets, mais transmettre des objets non valides est problématique. Si un nom de famille nul est exceptionnel, essayez de l’empêcher plus tôt que plus tard.

2
Un article de blog connexe à lire: Conception de l'initialisation des objets et notez les quatre approches pour initialiser une variable d'instance. L'un d'eux consiste à lui permettre d'être dans un état non valide, mais notez qu'il lève une exception. Si vous essayez d'éviter cela, cette approche n'est pas valide et vous devrez utiliser les autres approches, qui consistent à garantir un objet valide à tout moment. Extrait du blog: "Les objets pouvant avoir des états non valides sont plus difficiles à utiliser et à comprendre que ceux qui sont toujours valables." et c'est la clé.

5
"Faites la chose la plus facile à raisonner." Cela
Ben

5

Je ne suis pas un gars de Java, mais cela semble respecter les deux contraintes que vous avez présentées.

getCompleteNamene lève pas d'exception si les noms ne sont pas initialisés, et le displayCompleteNameOnInvoicefait.

public String getCompleteName()
{
    if (name == null || surname == null)
    {
        return null;
    }
    return name + " " + surname;
}

public void displayCompleteNameOnInvoice() 
{
    String completeName = getCompleteName();
    if (completeName == null)
    {
        //throw an exception.
    }
    //do something with it....
}

Pour expliquer pourquoi il s'agit d'une solution correcte: Ainsi, l'appelant de getCompleteName()n'aura pas à se soucier de savoir si la propriété est réellement stockée ou si elle est calculée à la volée.
Bart van Ingen Schenau

18
Pour expliquer pourquoi cette solution est complètement folle, vous devez vérifier la nullité de chaque appel getCompleteName, ce qui est hideux.
DeadMG

Non, @DeadMG, vous devez uniquement le vérifier dans les cas où une exception devrait être levée si getCompleteName renvoie null. C'est comme ça que ça devrait être. Cette solution est correcte.
Dawood dit réintégrer Monica

1
@DeadMG Oui, mais nous ne savons pas ce que AUTRE utilise il y a getCompleteName()dans le reste de la classe, ni même dans d'autres parties du programme. Dans certains cas, le retour nullpeut être exactement ce qui est requis. Mais bien qu'il existe UNE méthode dont la spécification est "déclencher une exception dans ce cas", c'est EXACTEMENT ce que nous devrions coder.
Dawood dit réintégrer Monica

4
Il peut y avoir des situations où c'est la meilleure solution, mais je ne peux pas en imaginer. Dans la plupart des cas, cela semble trop compliqué: une méthode jette des données incomplètes, une autre renvoie null. J'ai bien peur que les appelants ne l'utilisent pas correctement.
Sleske

4

Il semble que personne ne réponde à votre question.
Contrairement à ce que les gens aiment croire, "éviter les objets invalides" n'est pas souvent une solution pratique.

La réponse est:

Oui ça devrait.

Exemple simple: FileStream.Lengthen C # /. NET , qui jette NotSupportedExceptionsi le fichier n’est pas cherchable.


Les commentaires ne sont pas pour une discussion prolongée; cette conversation a été déplacée pour discuter .
arbre_érable

1

Vous avez tout le nom de vérification vérifier à l'envers.

getCompleteName()ne doit pas savoir quelles fonctions l’utiliseront. Ainsi, ne pas avoir d' nameappeler lors de l'appel displayCompleteNameOnInvoice()n'est pas la getCompleteName()responsabilité de s.

Mais, nameest une condition préalable claire pour displayCompleteNameOnInvoice(). Ainsi, il devrait prendre la responsabilité de vérifier l'état (ou il devrait déléguer la responsabilité de vérifier à une autre méthode, si vous préférez).

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.