Pourquoi les méthodes getter et setter sont mauvaises

Je n'avais pas l'intention de lancer une série "is evil", mais plusieurs lecteurs m'ont demandé d'expliquer pourquoi j'avais mentionné que vous devriez éviter les méthodes get / set dans la colonne du mois dernier, "Why extend Is Evil."

Bien que les méthodes getter / setter soient courantes en Java, elles ne sont pas particulièrement orientées objet (OO). En fait, ils peuvent endommager la maintenabilité de votre code. De plus, la présence de nombreuses méthodes getter et setter est un signal d'alarme indiquant que le programme n'est pas nécessairement bien conçu du point de vue OO.

Cet article explique pourquoi vous ne devriez pas utiliser les getters et les setters (et quand vous pouvez les utiliser) et suggère une méthodologie de conception qui vous aidera à sortir de la mentalité getter / setter.

Sur la nature du design

Avant de me lancer dans une autre chronique liée au design (avec un titre provocateur, rien de moins), je souhaite clarifier quelques choses.

J'ai été sidéré par certains commentaires de lecteurs qui ont résulté de la colonne du mois dernier, "Pourquoi étend le mal" (voir Talkback sur la dernière page de l'article). Certaines personnes pensaient que je soutenais que l'orientation des objets était mauvaise simplement parce qu'elle extendsavait des problèmes, comme si les deux concepts étaient équivalents. Ce n'est certainement pas ce que je pensais avoir dit, alors laissez-moi clarifier quelques méta-questions.

Cette chronique et l'article du mois dernier traitent du design. Le design, par nature, est une série de compromis. Chaque choix a un bon et un mauvais côté, et vous faites votre choix dans le cadre de critères globaux définis par nécessité. Le bien et le mal ne sont cependant pas absolus. Une bonne décision dans un contexte peut être mauvaise dans un autre.

Si vous ne comprenez pas les deux côtés d'un problème, vous ne pouvez pas faire un choix intelligent; en fait, si vous ne comprenez pas toutes les ramifications de vos actions, vous ne concevez pas du tout. Vous trébuchez dans le noir. Ce n'est pas un hasard si chaque chapitre du livre des modèles de conception de Gang of Four comprend une section «Conséquences» qui décrit quand et pourquoi l'utilisation d'un modèle est inappropriée.

Déclarer que certaines fonctionnalités du langage ou certains idiomes de programmation courants (comme les accesseurs) ont des problèmes n'est pas la même chose que de ne jamais les utiliser en aucune circonstance. Et juste parce que une fonction ou expression est couramment utilisé ne signifie pas que vous devez l' utiliser non plus . Les programmeurs non informés écrivent de nombreux programmes et le simple fait d'être employés par Sun Microsystems ou Microsoft n'améliore pas comme par magie les capacités de programmation ou de conception de quelqu'un. Les packages Java contiennent beaucoup de bon code. Mais il y a aussi des parties de ce code, je suis sûr que les auteurs sont gênés d'admettre qu'ils ont écrit.

De la même manière, le marketing ou les incitations politiques poussent souvent les idiomes du design. Parfois, les programmeurs prennent de mauvaises décisions, mais les entreprises veulent promouvoir ce que la technologie peut faire, alors elles minimisent le fait que la manière dont vous le faites est loin d'être idéale. Ils tirent le meilleur parti d'une mauvaise situation. Par conséquent, vous agissez de manière irresponsable lorsque vous adoptez une pratique de programmation simplement parce que «c'est ainsi que vous êtes censé faire les choses». De nombreux projets EJB (Enterprise JavaBeans) qui ont échoué prouvent ce principe. La technologie basée sur EJB est une excellente technologie lorsqu'elle est utilisée de manière appropriée, mais peut littéralement faire tomber une entreprise si elle est utilisée de manière inappropriée.

Mon point est que vous ne devriez pas programmer aveuglément. Vous devez comprendre les ravages qu'une fonctionnalité ou un idiome peut causer. Ce faisant, vous êtes bien mieux placé pour décider si vous devez utiliser cette fonctionnalité ou cette expression idiomatique. Vos choix doivent être à la fois éclairés et pragmatiques. Le but de ces articles est de vous aider à aborder votre programmation avec les yeux ouverts.

Abstraction de données

Un précepte fondamental des systèmes OO est qu'un objet ne doit exposer aucun de ses détails d'implémentation. De cette façon, vous pouvez modifier l'implémentation sans changer le code qui utilise l'objet. Il s'ensuit alors que dans les systèmes OO, vous devez éviter les fonctions getter et setter car elles permettent principalement d'accéder aux détails d'implémentation.

Pour comprendre pourquoi, considérez qu'il peut y avoir 1 000 appels à une getX()méthode dans votre programme et que chaque appel suppose que la valeur de retour est d'un type particulier. Vous pouvez stocker getX()la valeur de retour de la valeur de retour dans une variable locale, par exemple, et ce type de variable doit correspondre au type de valeur de retour. Si vous avez besoin de changer la façon dont l'objet est implémenté de telle sorte que le type de X change, vous avez de gros problèmes.

Si X était un int, mais doit maintenant être un long, vous obtiendrez 1000 erreurs de compilation. Si vous ne résolvez pas correctement le problème en convertissant la valeur de retour en int, le code se compilera proprement, mais cela ne fonctionnera pas. (La valeur de retour peut être tronquée.) Vous devez modifier le code entourant chacun de ces 1 000 appels pour compenser le changement. Je ne veux certainement pas faire autant de travail.

Un principe de base des systèmes OO est l'abstraction des données . Vous devez masquer complètement la manière dont un objet implémente un gestionnaire de messages du reste du programme. C'est l'une des raisons pour lesquelles toutes vos variables d'instance (les champs non constants d'une classe) devraient l'être private.

Si vous créez une variable d'instance public, vous ne pouvez pas modifier le champ à mesure que la classe évolue au fil du temps, car vous casseriez le code externe qui utilise le champ. Vous ne voulez pas rechercher 1 000 utilisations d'une classe simplement parce que vous modifiez cette classe.

Ce principe de masquage d'implémentation conduit à un bon test acide de la qualité d'un système OO: pouvez-vous apporter des modifications massives à une définition de classe - même jeter le tout et le remplacer par une implémentation complètement différente - sans affecter le code qui l'utilise les objets de la classe? Ce type de modularisation est la prémisse centrale de l'orientation des objets et facilite grandement la maintenance. Sans masquage de l'implémentation, il est inutile d'utiliser d'autres fonctionnalités OO.

Les méthodes Getter et Setter (également appelées accesseurs) sont dangereuses pour la même raison que les publicchamps sont dangereux: elles fournissent un accès externe aux détails d'implémentation. Que faire si vous devez changer le type du champ accédé? Vous devez également modifier le type de retour de l'accesseur. Vous utilisez cette valeur de retour à de nombreux endroits, vous devez donc également modifier tout ce code. Je souhaite limiter les effets d'une modification à une seule définition de classe. Je ne veux pas qu'ils se répercutent dans tout le programme.

Étant donné que les accesseurs violent le principe d'encapsulation, vous pouvez raisonnablement affirmer qu'un système qui utilise fortement ou de manière inappropriée des accesseurs n'est tout simplement pas orienté objet. Si vous passez par un processus de conception, par opposition à un simple codage, vous ne trouverez pratiquement aucun accesseur dans votre programme. Le processus est important. J'ai plus à dire sur cette question à la fin de l'article.

Le manque de méthodes getter / setter ne signifie pas que certaines données ne transitent pas par le système. Néanmoins, il est préférable de minimiser autant que possible le mouvement des données. Mon expérience est que la maintenabilité est inversement proportionnelle à la quantité de données qui se déplace entre les objets. Bien que vous ne voyiez peut-être pas encore comment, vous pouvez éliminer la plupart de ce mouvement de données.

En concevant soigneusement et en vous concentrant sur ce que vous devez faire plutôt que sur la façon dont vous allez le faire, vous éliminez la grande majorité des méthodes getter / setter de votre programme. Ne demandez pas les informations dont vous avez besoin pour faire le travail; demandez à l'objet qui possède les informations de faire le travail à votre place.La plupart des accesseurs trouvent leur chemin dans le code parce que les concepteurs ne pensaient pas au modèle dynamique: les objets d'exécution et les messages qu'ils s'envoient pour faire le travail. Ils commencent (de manière incorrecte) par la conception d'une hiérarchie de classes, puis essaient d'intégrer ces classes dans le modèle dynamique. Cette approche ne fonctionne jamais. Pour créer un modèle statique, vous devez découvrir les relations entre les classes et ces relations correspondent exactement au flux de messages. Une association existe entre deux classes uniquement lorsque les objets d'une classe envoient des messages aux objets de l'autre. L'objectif principal du modèle statique est de capturer ces informations d'association au fur et à mesure que vous modélisez dynamiquement.

Sans un modèle dynamique clairement défini, vous ne faites que deviner comment vous utiliserez les objets d'une classe. Par conséquent, les méthodes d'accès se retrouvent souvent dans le modèle car vous devez fournir autant d'accès que possible car vous ne pouvez pas prédire si vous en aurez besoin ou non. Ce type de stratégie de conception par estimation est au mieux inefficace. Vous perdez du temps à écrire des méthodes inutiles (ou à ajouter des capacités inutiles aux classes).

Les accesseurs se retrouvent également dans des dessins par habitude. Lorsque les programmeurs procéduraux adoptent Java, ils ont tendance à commencer par créer du code familier. Les langages procéduraux n'ont pas de classes, mais ils ont le C struct(pensez: classe sans méthodes). Il semble donc naturel d'imiter un structen construisant des définitions de classe avec pratiquement aucune méthode et rien que des publicchamps. Ces programmeurs procéduraux lisent quelque part que les champs devraient être private, cependant, ils créent les champs privateet fournissent publicdes méthodes d'accès. Mais ils n'ont fait que compliquer l'accès public. Ils n'ont certainement pas orienté objet du système.

Dessine-toi

Une des ramifications de l'encapsulation de champ complet est dans la construction de l'interface utilisateur (UI). Si vous ne pouvez pas utiliser d'accesseurs, vous ne pouvez pas demander à une classe de générateur d'interface utilisateur d'appeler une getAttribute()méthode. Au lieu de cela, les classes ont des éléments comme des drawYourself(...)méthodes.

Une getIdentity()méthode peut également fonctionner, bien sûr, à condition qu'elle renvoie un objet qui implémente l' Identityinterface. Cette interface doit inclure une méthode drawYourself()(ou donne-moi-un- JComponentqui-représente-votre-identité). Bien qu'il getIdentitycommence par "get", ce n'est pas un accesseur car il ne renvoie pas seulement un champ. Il renvoie un objet complexe qui a un comportement raisonnable. Même quand j'ai un Identityobjet, je n'ai toujours aucune idée de comment une identité est représentée en interne.

Bien sûr, une drawYourself()stratégie signifie que je mets (halètement!) Le code de l'interface utilisateur dans la logique métier. Considérez ce qui se passe lorsque les exigences de l'interface utilisateur changent. Disons que je veux représenter l'attribut d'une manière complètement différente. Aujourd'hui, une «identité» est un nom; demain c'est un nom et un numéro d'identification; le lendemain, c'est un nom, un numéro d'identification et une photo. Je limite la portée de ces modifications à un seul endroit du code. Si j'ai une JComponentclasse donne-moi- qui-représente-votre-identité, alors j'ai isolé la façon dont les identités sont représentées du reste du système.

Gardez à l'esprit que je n'ai pas mis de code d'interface utilisateur dans la logique métier. J'ai écrit la couche UI en termes d'AWT (Abstract Window Toolkit) ou de Swing, qui sont tous deux des couches d'abstraction. Le code d'interface utilisateur réel se trouve dans l'implémentation AWT / Swing. C'est tout l'intérêt d'une couche d'abstraction: isoler votre logique métier des mécanismes d'un sous-système. Je peux facilement porter vers un autre environnement graphique sans changer le code, donc le seul problème est un peu de désordre. Vous pouvez facilement éliminer cet encombrement en déplaçant tout le code de l'interface utilisateur dans une classe interne (ou en utilisant le modèle de conception Façade).

JavaBeans

Vous pourriez objecter en disant: "Mais qu'en est-il des JavaBeans?" Et eux? Vous pouvez certainement créer des JavaBeans sans getters et setters. La BeanCustomizer, BeanInfoet les BeanDescriptorclasses existent tous dans ce but précis. Les concepteurs de spécification JavaBean ont jeté l'idiome getter / setter dans l'image parce qu'ils pensaient que ce serait un moyen facile de créer rapidement un bean - quelque chose que vous pouvez faire pendant que vous apprenez à le faire correctement. Malheureusement, personne n'a fait ça.

Les accesseurs ont été créés uniquement pour marquer certaines propriétés afin qu'un programme UI-builder ou équivalent puisse les identifier. Vous n'êtes pas censé appeler ces méthodes vous-même. Ils existent pour un outil automatisé à utiliser. Cet outil utilise les API d'introspection de la Classclasse pour rechercher les méthodes et extrapoler l'existence de certaines propriétés à partir des noms de méthode. En pratique, cet idiome basé sur l'introspection n'a pas fonctionné. Cela a rendu le code beaucoup trop compliqué et trop procédural. Les programmeurs qui ne comprennent pas l'abstraction des données appellent en fait les accesseurs et, par conséquent, le code est moins maintenable. Pour cette raison, une fonctionnalité de métadonnées sera intégrée à Java 1.5 (prévue mi-2004). Donc au lieu de:

propriété int privée; public int getProperty () {propriété de retour; } public void setProperty (valeur int} {propriété = valeur;}

Vous pourrez utiliser quelque chose comme:

propriété privée @property int; 

L'outil de construction d'interface utilisateur ou équivalent utilisera les API d'introspection pour trouver les propriétés, plutôt que d'examiner les noms de méthode et de déduire l'existence d'une propriété à partir d'un nom. Par conséquent, aucun accesseur d'exécution n'endommage votre code.

Quand un accesseur est-il d'accord?

Tout d'abord, comme je l'ai discuté précédemment, il est normal qu'une méthode renvoie un objet en termes d'interface que l'objet implémente car cette interface vous isole des modifications apportées à la classe d'implémentation. Ce type de méthode (qui renvoie une référence d'interface) n'est pas vraiment un "getter" dans le sens d'une méthode qui donne simplement accès à un champ. Si vous modifiez l'implémentation interne du fournisseur, il vous suffit de modifier la définition de l'objet renvoyé pour prendre en charge les modifications. Vous protégez toujours le code externe qui utilise l'objet via son interface.