Combien de logique dans Getters

46

Mes collègues me disent qu'il devrait y avoir le moins de logique possible dans les getters et les setters.

Pourtant, je suis convaincu que beaucoup de choses peuvent être cachées dans les accesseurs et les installateurs pour protéger les utilisateurs / programmeurs des détails de la mise en œuvre.

Un exemple de ce que je fais:

public List<Stuff> getStuff()
{
   if (stuff == null || cacheInvalid())
   {
       stuff = getStuffFromDatabase();
   }
   return stuff;
}

Un exemple de la façon dont le travail me dit de faire les choses (ils citent «Clean Code» de Oncle Bob):

public List<Stuff> getStuff()
{
    return stuff;
}

public void loadStuff()
{
    stuff = getStuffFromDatabase();
}

Combien de logique est appropriée dans un setter / getter? Quelle est l'utilisation de getters et de setters vides, sauf en cas de violation de la dissimulation de données?

Maarten van Leunen
la source
6
Cela ressemble plus à tryGetStuff () pour moi ...
Bill Michell
16
Ce n'est pas un "getter". Ce terme est utilisé pour l'accesseur en lecture d'une propriété, pas pour une méthode que vous avez accidentellement placée avec un 'get' dans le nom.
Boris Yankov
6
Je ne sais pas si ce deuxième exemple est un bon exemple de ce livre de code épuré que vous avez mentionné, ou de quelqu'un qui se trompe, mais une chose que le désordre fragile n’est pas, c’est le code épuré.
Jon Hanna
@ BorisYankov Eh bien ... la deuxième méthode est. public List<Stuff> getStuff() { return stuff; }
R. Schmitz
Selon le cas d'utilisation exact, j'aime séparer ma mise en cache dans une classe distincte. Créez une StuffGetterinterface, implémentez une StuffComputerqui effectue les calculs et enveloppez-la dans un objet StuffCacherqui est responsable de l’accès au cache ou du transfert des appels vers StuffComputercelui qu’il encapsule.
Alexander

Réponses:

71

La façon dont le travail vous dit de faire les choses est boiteux.

En règle générale, ma façon de faire les choses est la suivante: si obtenir le contenu est peu coûteux du point de vue du calcul (ou si la plupart du temps il se trouve dans le cache), votre style de getStuff () convient parfaitement. Si obtenir le matériel est connu pour être coûteux en calcul, si coûteux que la publicité de son coût est nécessaire à l'interface, je ne l'appellerais pas getStuff (), je l'appellerais CalculateStuff () ou quelque chose du genre, afin d'indiquer que il y aura du travail à faire.

Dans les deux cas, la façon dont le travail vous dit de faire les choses est boiteuse, car getStuff () explose si loadStuff () n'a pas été appelé à l'avance. Ils veulent donc essentiellement que vous compliquiez votre interface en introduisant une complexité d'ordre des opérations. à cela. L'ordre des opérations est à peu près le pire type de complexité auquel je puisse penser.

Mike Nakis
la source
23
+1 pour mentionner la complexité de l'ordre des opérations. En guise de solution de contournement, peut-être que work me demandera de toujours appeler loadStuff () dans le constructeur, mais ce serait également une mauvaise chose, car cela signifie qu'il faudra toujours le charger. Dans le premier exemple, les données sont chargées paresseusement uniquement lorsque cela est nécessaire, ce qui est aussi bon que possible.
laurent
6
Je suis généralement la règle "si c'est vraiment pas cher, utilisez un getter de propriété. Si c'est cher, utilisez une fonction". Cela me sert généralement bien, et nommer correctement, comme vous l'avez indiqué pour le souligner, me semble également bon.
Denis Troller
3
si cela peut échouer - ce n'est pas un getter. Dans ce cas, que se passe-t-il si le lien de base de données est en panne?
Martin Beckett
6
+1, je suis un peu sous le choc du nombre de mauvaises réponses postées. Les Getters / Setters existent pour cacher les détails d'implémentation, sinon la variable devrait juste être rendue publique.
Izkata
2
N'oubliez pas que le fait d'exiger que la loadStuff()fonction soit appelée avant la getStuff()fonction signifie également que la classe n'abstractionne pas correctement ce qui se passe sous le capot.
Rjzii
23

La logique des accesseurs convient parfaitement.

Mais obtenir des données d'une base de données, c'est beaucoup plus que de la "logique". Il s'agit d'une série d' opérations très coûteuses où beaucoup de problèmes peuvent se produire, de manière non déterministe. J'hésiterais à le faire implicitement dans un getter.

D'un autre côté, la plupart des ORM prennent en charge le chargement paresseux des collections, ce qui est fondamentalement exactement ce que vous faites.

Michael Borgwardt
la source
18

Je pense que selon le "code propre" il devrait être divisé autant que possible en quelque chose comme:

public List<Stuff> getStuff() {
   if (hasStuff()) {
       return stuff;
   }
   loadStuff();
   return stuff;
}

private boolean hasStuff() {
    if (stuff == null) {
       return false;
    }
    if (cacheInvalid()) {
       return false;        
    }
    return true;
} 

private void loadStuff() {
    stuff = getStuffFromDatabase();
}

Bien sûr, c'est totalement absurde, étant donné que la belle forme que vous avez écrite fait le bon choix avec une fraction de code que tout le monde comprend en un coup d'œil:

public List<Stuff> getStuff() {
   if (stuff == null || cacheInvalid()) {
       stuff = getStuffFromDatabase();
   }
   return stuff;
}

Cela ne devrait pas être le casse-tête de l'appelant, mais surtout le fait de ne pas avoir mal à la tête de l'appelant de ne pas oublier d'appeler les choses dans un "ordre correct" arbitraire.

Joonas Pulakka
la source
8
-1. Le véritable casse-tête sera lorsque l'appelant sera coincé à comprendre pourquoi un simple appel de getter a entraîné un accès lent à la base de données.
Domenic
14
@ Domenic: L'accès à la base de données doit être fait de toute façon, vous ne sauvegardez les performances de personne en ne le faisant pas. Si vous en avez besoin List<Stuff>, il n'y a qu'un moyen de l'obtenir.
DeadMG
4
@lukas: Merci, je ne me souvenais pas de toutes les astuces utilisées dans le code «Nettoyer» pour créer des bouts de code triviaux, mais une ligne de plus ;-) Fixé maintenant.
Joonas Pulakka
2
Vous calomniez Robert Martin. Il ne développerait jamais une simple disjonction booléenne en une fonction de neuf lignes. Votre fonction hasStuffest l'opposé du code propre.
kevin cline
2
J'ai lu le début de cette réponse et j'allais la contourner en me disant "il y a un autre adorateur du livre", puis la partie "Bien sûr, c'est complètement absurde" a attiré mon attention. Bien dit! C -: =
Mike Nakis
8

Ils me disent qu'il devrait y avoir le moins de logique possible dans les Getters et les Setters.

Il faut qu'il y ait autant de logique que nécessaire pour répondre aux besoins de la classe. Personnellement, je préfère le moins possible, mais lors de la maintenance du code, vous devez généralement laisser l'interface d'origine avec les getters / setters existants, mais mettez-y beaucoup de logique pour corriger une nouvelle logique métier (par exemple, un "getter dans un environnement post-911 doit respecter les règles " connaître son client "et les réglementations OFAC , associées à une politique d'entreprise interdisant la comparution de clients de certains pays [tels que Cuba ou l'Iran]).

Dans votre exemple, je préfère le vôtre et n'aime pas l'exemple "oncle bob" car la version "oncle bob" oblige les utilisateurs / responsables à se rappeler d'appeler loadStuff()avant d'appeler getStuff()- il s'agit d'une recette pour un désastre si l'un de vos responsables oublie (ou pire, je ne savais jamais). La plupart des endroits où j'ai travaillé au cours de la dernière décennie utilisent encore du code datant de plus de 10 ans. La facilité de maintenance est donc un facteur essentiel à prendre en compte.

Tangurena
la source
6

Vous avez raison, vos collègues ont tort.

Oubliez les règles empiriques de chacun sur ce qu'une méthode get devrait ou ne devrait pas faire. Une classe devrait présenter une abstraction de quelque chose. Votre classe est lisible stuff. En Java, il est classique d'utiliser des méthodes 'get' pour lire les propriétés. Des milliards de lignes de cadres ont été écrits s'attendant à lire stuffpar appel getStuff. Si vous nommez votre fonction fetchStuffou autre chose que getStuff, votre classe sera incompatible avec tous ces frameworks.

Vous pouvez les diriger vers Hibernate, où 'getStuff ()' peut faire des choses très compliquées et lève une exception RuntimeException en cas d'échec.

Kevin Cline
la source
Hibernate est un ORM, donc le paquet lui-même exprime l'intention. Cette intention n'est pas aussi facile à comprendre si le paquet lui-même n'est pas un ORM.
FMJaguar
@ FMJaguar: c'est parfaitement facile à comprendre. Hibernate résume les opérations de base de données pour présenter un réseau d’objets. L'OP extrait une opération de base de données pour présenter un objet ayant une propriété nommée stuff. Les deux masquent les détails pour faciliter l'écriture du code d'appel.
Kevin Cline
Si cette classe est une classe ORM, l'intention est déjà exprimée, dans d'autres contextes: la question demeure: "Comment un autre programmeur connaît-il les effets secondaires de l'appel du getter?". Si le programme contient 1k classes et 10k getters, une politique
autorisant les
4

Cela pourrait être un peu un débat puriste par rapport à une application qui pourrait être affecté par la façon dont vous préférez contrôler les noms de fonction. Du point de vue appliqué, je préférerais de loin voir:

List<String> names = clientRoster.getNames();
List<String> emails = clientRoster.getEmails();

Par opposition à:

myObject.load();
List<String> names = clientRoster.getNames();
List<String> emails = clientRoster.getEmails();

Ou pire encore:

myObject.loadNames();
List<String> names = clientRoster.getNames();
myOjbect.loadEmails();
List<String> emails = clientRoster.getEmails();

Ce qui tend à rendre les autres codes beaucoup plus redondants et plus difficiles à lire car vous devez commencer à parcourir tous les appels similaires. En outre, l’appel de fonctions de chargeur ou de fonctions similaires annule l’objectif même de l’utilisation de la programmation orientée objet, dans la mesure où vous ne faites plus abstraction des détails d’implémentation de l’objet avec lequel vous travaillez. Si vous avez un clientRosterobjet, vous ne devriez pas avoir à vous soucier de la façon dont getNamesfonctionne, comme vous le feriez si vous deviez appeler un loadNames, vous devez juste savoir que cela getNamesvous donne un List<String>avec le nom des clients.

Il semble donc que le problème concerne davantage la sémantique et le meilleur nom pour la fonction permettant d’obtenir les données. Si la société (et d'autres) a un problème avec le préfixe getet set, pourquoi ne pas appeler la fonction à sa retrieveNamesplace? Il dit ce qui se passe mais n'implique pas que l'opération serait instantanée comme on pourrait s'y attendre d'une getméthode.

En termes de logique dans une méthode d'accès, limitez-la au minimum car elles sont généralement impliquées comme instantanées, avec uniquement une interaction nominale se produisant avec la variable. Cependant, cela ne concerne généralement que les types simples, les types de données complexes (c'est-à-dire List). Je trouve plus difficile d'encapsuler correctement dans une propriété et d'utiliser généralement d'autres méthodes pour interagir avec elles, par opposition à un mutateur strict et à un accesseur.

rjzii
la source
2

L'appel d'un getter doit présenter le même comportement que la lecture d'un champ:

  • Récupérer la valeur devrait être bon marché
  • Si vous définissez une valeur avec le setter et que vous la lisez ensuite avec le getter, la valeur doit être la même.
  • Obtenir la valeur ne devrait avoir aucun effet secondaire
  • Il ne faut pas jeter une exception
Sjoerd
la source
2
Je ne suis pas tout à fait d'accord sur ça. Je conviens qu'il ne devrait pas y avoir d'effets secondaires, mais je pense que c'est parfaitement correct de le mettre en œuvre d'une manière qui le différencie d'un domaine. En ce qui concerne le .Net BCL, InvalidOperationException est largement utilisé pour les getters. Voir également la réponse de Mike Nakis sur l'ordre des opérations.
Max
D'accord avec tous les points sauf le dernier. Il est certainement possible que l'obtention d'une valeur implique l'exécution d'un calcul ou d'une autre opération qui dépend d'autres valeurs ou ressources qui n'ont peut-être pas été définies. Dans ces cas, je m'attendrais à ce que le getter lance une sorte d'exception.
TMN
1
@TMN: Dans le meilleur des cas, la classe devrait être organisée de manière à ce que les getters n'aient pas besoin d'exécuter des opérations capables d'exécuter une exception. Minimiser les lieux pouvant générer des exceptions entraîne des surprises moins inattendues.
Hugomg
8
Je vais être en désaccord avec le deuxième point avec un exemple spécifique: foo.setAngle(361); bar = foo.getAngle(). barpourrait être 361, mais cela pourrait aussi être légitimement 1si les angles sont liés à une plage.
zzzzBov
1
-1. (1) est bon marché dans cet exemple - après le chargement paresseux. (2) Actuellement , il n'y a pas de « poseur » dans l'exemple, mais si quelqu'un ajoute un après, et il vient de jeux stuff, le getter va retourner la même valeur. (3) Le chargement paresseux, comme indiqué dans l'exemple, ne produit pas d'effets secondaires "visibles". (4) est discutable, peut-être un argument valable, car introduire le "chargement paresseux" après peut changer le contrat précédent de l'API - mais il faut regarder ce contrat pour prendre une décision.
Doc Brown
2

Un getter qui appelle d'autres propriétés et méthodes afin de calculer sa propre valeur implique également une dépendance. Par exemple, si votre propriété doit pouvoir se calculer elle-même et qu'un autre membre doit être défini, vous devez vous inquiéter des références nulles accidentelles si vous accédez à votre propriété dans le code d'initialisation où tous les membres ne sont pas nécessairement définis.

Cela ne signifie pas «n'accédez jamais à un autre membre que le champ de sauvegarde des propriétés dans le getter», cela signifie simplement que vous devez prêter attention à ce que vous sous-entendez concernant l'état requis de l'objet et si cela correspond au contexte que vous attendez. cette propriété est accessible à.

Cependant, dans les deux exemples concrets que vous avez donnés, la raison pour laquelle je choisirais l'un plutôt que l'autre est totalement différente. Votre getter est initialisé lors du premier accès, par exemple, initialisation différée . Le deuxième exemple est supposé être initialisé à un moment donné, par exemple, Initialisation explicite .

Lorsque l’initialisation se produit exactement, cela peut être important ou non.

Par exemple, il peut être très lent et doit être effectué au cours d’une étape de chargement où l’utilisateur s’attend à un délai, plutôt qu’à un ralentissement inattendu des performances lorsque l’utilisateur déclenche un accès (c.-à-d. Un clic droit, un menu contextuel apparaît, a déjà fait un clic droit de nouveau).

De plus, parfois, il y a un point évident dans l'exécution où tout ce qui peut affecter / salir la valeur de la propriété en cache se produit. Vous pouvez même vérifier qu'aucune des dépendances ne change et renvoyer des exceptions ultérieurement. Dans cette situation, il est judicieux de mettre également en cache la valeur à ce stade, même si le calcul n'est pas particulièrement coûteux, afin d'éviter de rendre l'exécution du code plus complexe et plus difficile à suivre mentalement.

Cela dit, l’initialisation différée a beaucoup de sens dans bien d’autres situations. Ainsi, comme il arrive souvent dans la programmation, il est difficile de résumer en une règle, cela revient au code concret.

James
la source
0

Faites-le comme @MikeNakis l'a dit ... Si vous avez juste le truc, c'est bien ... Si vous faites autre chose, créez une nouvelle fonction qui fait le travail et le rend public.

Si votre propriété / fonction ne fait que ce que son nom l'indique, il ne reste plus beaucoup de place à la complication. La cohésion est la clé de l'OMI

Ivan Crojach Karačić
la source
1
Faites attention à cela, vous risquez de vous exposer à trop de votre état interne. Vous ne voulez pas liquider avec beaucoup de vides loadFoo()ou preloadDummyReferences()ou createDefaultValuesForUninitializedFields()méthodes simplement parce que la mise en œuvre initiale de votre classe avait besoin d' eux.
TMN
Bien sûr ... Je disais simplement que si vous faites ce que son nom l' indique qu'il ne devrait pas y avoir beaucoup de problèmes ... mais ce que vous dites est vrai ... absolutly
Ivan Crojach Karacic
0

Personnellement, j'exposerais l'exigence de Stuff via un paramètre dans le constructeur et laisserais à la classe quelle que soit la classe qui instancie une substance d'effectuer le travail de détermination de l'origine de celle-ci. Si stuff est null, il devrait renvoyer null. Je préfère ne pas essayer de solutions aussi astucieuses que l'original de l'OP, car il s'agit d'un moyen facile de masquer des bugs au sein de votre implémentation, où il n'est pas du tout évident de voir ce qui pourrait mal tourner en cas de problème.

EricBoersma
la source
0

Il y a des questions plus importantes que la "pertinence" ici et vous devez baser votre décision sur celles-ci . La principale décision à prendre ici est de savoir si vous voulez permettre aux gens de contourner ou non la mémoire cache.

  1. Premièrement, réfléchissez s'il existe un moyen de réorganiser votre code afin que tous les appels de charge et la gestion du cache nécessaires soient effectués dans le constructeur / l'initialiseur. Si cela est possible, vous pouvez créer une classe dont l'invariant vous permet d'accéder au simple getter de la partie 2 avec la sécurité du getter complexe de la partie 1. (Un scénario gagnant-gagnant)

  2. Si vous ne pouvez pas créer une telle classe, décidez si vous avez un compromis et devez décider si vous souhaitez autoriser le consommateur à ignorer ou non le code de vérification du cache.

    1. S'il est important que le consommateur ne passe jamais le contrôle du cache et que les pénalités de performances ne vous dérangent pas, couplez le contrôle à l'intérieur du getter et empêchez le consommateur de faire le mauvais choix.

    2. Si vous pouvez ignorer la vérification du cache ou s'il est très important que vous soyez assuré de la performance O (1) dans le getter, utilisez des appels séparés.

Comme vous l'avez peut-être déjà noté, je ne suis pas un grand fan de la philosophie du "code propre", "tout scinder en petites fonctions". Si vous avez un tas de fonctions orthogonales qui peuvent être appelées dans n'importe quel ordre, leur division vous donnera plus de pouvoir expressif à peu de frais. Cependant, si vos fonctions ont des dépendances d'ordre (ou ne sont vraiment utiles que dans un ordre particulier), leur division n'augmente que le nombre de façons dont vous pouvez faire des choses mal, tout en apportant peu d'avantages.

hugomg
la source
-1, les constructeurs doivent construire et non initialiser. Le fait de mettre la logique de base de données dans un constructeur rend cette classe totalement non testable. Si vous en avez plus d'une poignée, le temps de démarrage de votre application deviendra immense. Et ce n'est que pour commencer.
Domenic
@Domenic: Il s'agit d'un problème sémantique et dépendant de la langue. Le point qu'un objet est apte à utiliser et fournit les invariants appropriés après, et seulement après, il est entièrement construit.
Hugomg
0

À mon avis, les Getters ne devraient pas contenir beaucoup de logique. Ils ne devraient pas avoir d'effets secondaires et vous ne devriez jamais obtenir une exception. À moins bien sûr, vous savez ce que vous faites. La plupart de mes accesseurs n'ont aucune logique en eux et vont simplement dans un champ. La seule exception à cette règle est une API publique qui devait être aussi simple que possible à utiliser. J'ai donc eu un getter qui échouerait si un autre getter n'avait pas été appelé. La solution? Une ligne de code comme var throwaway=MyGetter;dans le getter qui en dépendait. Je n'en suis pas fier, mais je ne vois toujours pas de façon plus propre de le faire.

Earlz
la source
0

Cela ressemble à une lecture de cache avec chargement paresseux. Comme d'autres l'ont noté, la vérification et le chargement peuvent appartenir à d'autres méthodes. Il se peut que le chargement doive être synchronisé afin d'éviter de charger vingt threads en même temps.

Il peut être approprié d’utiliser un nom getCachedStuff()pour le getter car il n’a pas de temps d’exécution cohérent.

Selon le fonctionnement de la cacheInvalid()routine, la vérification de la valeur NULL peut ne pas être nécessaire. Je ne m'attendrais pas à ce que le cache soit valide sauf s'il stuffa été rempli à partir de la base de données.

BillThor
la source
0

La logique principale que je m'attendrais à voir dans les getters qui renvoient une liste est la logique permettant de s'assurer que la liste est non modifiable. Dans l'état actuel des choses, votre exemple casserait potentiellement l'encapsulation.

quelque chose comme:

public List<Stuff> getStuff()
{
    return Collections.unmodifiableList(stuff);
}

En ce qui concerne la mise en cache dans le getter, je pense que ce serait OK, mais je pourrais être tenté de sortir de la logique du cache si la construction du cache prenait beaucoup de temps. c'est à dire cela dépend.

jk.
la source
0

Selon le cas d'utilisation exact, j'aime séparer ma mise en cache dans une classe distincte. Créez une StuffGetterinterface, implémentez une StuffComputerqui effectue les calculs et enveloppez-la dans un objet StuffCacherqui est responsable de l’accès au cache ou du transfert des appels vers StuffComputercelui qu’il encapsule.

interface StuffGetter {
     public List<Stuff> getStuff();
}

class StuffComputer implements StuffGetter {
     public List<Stuff> getStuff() {
         getStuffFromDatabase()
     }
}

class StuffCacher implements StuffGetter {
     private stuffComputer; // DI this
     private Cache<List<Stuff>> cache = new Cache<>();

     public List<Stuff> getStuff() {
         if cache.hasStuff() {
             return cache.getStuff();
         }

         List<Stuffs> stuffs = stuffComputer.getStuff();
         cache.store(stuffs);
         return stuffs;
     }
}

Cette conception vous permet d'ajouter facilement du cache, de supprimer le cache, de changer la logique de dérivation sous-jacente (par exemple, accéder à une base de données ou de renvoyer des données fictives), etc.

Alexandre
la source
-1

IMHO c'est très simple si vous utilisez un design par contrat. Décidez ce que votre getter doit fournir et codez-le en conséquence (code simple ou logique complexe pouvant être impliquée ou déléguée quelque part).

Kemoda
la source
+1: je suis d'accord avec toi! Si l'objet est uniquement destiné à contenir des données, les getters ne doivent renvoyer que le contenu actuel de l'objet. Dans ce cas, il est de la responsabilité d'un autre objet de charger les données. Si le contrat indique que l'objet est le proxy d'un enregistrement de base de données, le getter doit alors récupérer les données à la volée. Cela peut devenir encore plus compliqué si les données ont été chargées mais ne sont pas à jour: l'objet doit-il être averti des modifications apportées à la base de données? Je pense qu'il n'y a pas de réponse unique à cette question.
Giorgio