En savoir plus sur les getters et les setters

C'est un principe vieux de 25 ans de conception orientée objet (OO) selon lequel vous ne devez pas exposer l'implémentation d'un objet à d'autres classes du programme. Le programme est inutilement difficile à gérer lorsque vous exposez l'implémentation, principalement parce que la modification d'un objet qui expose son implémentation oblige à modifier toutes les classes qui utilisent l'objet.

Malheureusement, l'idiome getter / setter que de nombreux programmeurs considèrent comme orienté objet viole ce principe OO fondamental à la pelle. Prenons l'exemple d'une Moneyclasse qui contient une getValue()méthode qui renvoie la «valeur» en dollars. Vous aurez du code comme celui-ci dans tout votre programme:

double orderTotal; Montant en argent = ...; // ... orderTotal + = montant.getValue (); // orderTotal doit être en dollars

Le problème avec cette approche est que le code précédent fait une grande hypothèse sur la façon dont la Moneyclasse est implémentée (que la «valeur» est stockée dans a double). Code qui rend les hypothèses d'implémentation cassées lorsque l'implémentation change. Si, par exemple, vous devez internationaliser votre application pour prendre en charge des devises autres que le dollar, cela getValue()ne renvoie rien de significatif. Vous pourriez ajouter un getCurrency(), mais cela rendrait tout le code entourant l' getValue()appel beaucoup plus compliqué, surtout si vous persistez à utiliser la stratégie getter / setter pour obtenir les informations dont vous avez besoin pour faire le travail. Une implémentation typique (imparfaite) pourrait ressembler à ceci:

Montant en argent = ...; // ... valeur = montant.getValue (); devise = montant.getCurrency (); conversion = CurrencyTable.getConversionFactor (devise, USDOLLARS); total + = valeur * conversion; // ...

Ce changement est trop compliqué pour être géré par une refactorisation automatisée. De plus, vous auriez à faire ce genre de changements partout dans votre code.

La solution au niveau de la logique métier à ce problème consiste à effectuer le travail dans l'objet contenant les informations nécessaires pour effectuer le travail. Au lieu d'extraire la «valeur» pour effectuer une opération externe dessus, vous devriez demander à la Moneyclasse de faire toutes les opérations liées à l'argent, y compris la conversion de devises. Un objet correctement structuré gérerait le total comme ceci:

Total monétaire = ...; Montant en argent = ...; total.increaseBy (montant);

La add()méthode déterminerait la devise de l'opérande, effectuerait toute conversion de devise nécessaire (qui est, correctement, une opération sur l' argent ) et mettrait à jour le total. Si vous avez utilisé cette stratégie d'objet-qui-a-l'information-fait-le-travail pour commencer, la notion de devise pourrait être ajoutée à la Moneyclasse sans aucune modification requise dans le code qui utilise des Moneyobjets. Autrement dit, le travail de refactorisation d'un dollar uniquement vers une mise en œuvre internationale serait concentré en un seul endroit: la Moneyclasse.

Le problème

La plupart des programmeurs n'ont aucune difficulté à saisir ce concept au niveau de la logique métier (bien que cela puisse prendre un certain effort pour penser de cette façon de manière cohérente). Cependant, des problèmes commencent à apparaître lorsque l'interface utilisateur (UI) entre en scène. Le problème n'est pas que vous ne pouvez pas appliquer des techniques comme celle que je viens de décrire pour créer une interface utilisateur, mais que de nombreux programmeurs sont enfermés dans une mentalité getter / setter en ce qui concerne les interfaces utilisateur. Je blâme ce problème sur les outils de construction de code fondamentalement procéduraux comme Visual Basic et ses clones (y compris les constructeurs de l'interface utilisateur Java) qui vous obligent à adopter cette façon de penser procédurale, getter / setter.

(Digression: Certains d'entre vous rechigneront à la déclaration précédente et crieront que VB est basé sur l'architecture sacrée Model-View-Controller (MVC), tout comme l'est sacro-saint. Gardez à l'esprit que MVC a été développé il y a près de 30 ans. Au début Dans les années 1970, le plus gros supercalculateur était à égalité avec les ordinateurs de bureau d'aujourd'hui. La plupart des ordinateurs (comme le DEC PDP-11) étaient des ordinateurs 16 bits, avec 64 Ko de mémoire et des vitesses d'horloge mesurées en dizaines de mégahertz. Votre interface utilisateur était probablement une pile de cartes perforées. Si vous aviez la chance d'avoir un terminal vidéo, vous utilisiez peut-être un système d'entrée / sortie (E / S) de console basé sur ASCII. Nous avons beaucoup appris au cours des 30 dernières années. Même Java Swing a dû remplacer MVC par une architecture de «modèle séparable» similaire, principalement parce que le MVC pur n'isole pas suffisamment les couches d'interface utilisateur et de modèle de domaine.)

Alors, définissons le problème en un mot:

Si un objet peut ne pas exposer les informations d'implémentation (via des méthodes get / set ou par tout autre moyen), il va de soi qu'un objet doit en quelque sorte créer sa propre interface utilisateur. Autrement dit, si la façon dont les attributs d'un objet sont représentés est masquée du reste du programme, vous ne pouvez pas extraire ces attributs afin de créer une interface utilisateur.

Notez, en passant, que vous ne cachez pas le fait qu'un attribut existe. (Je définis ici l' attribut comme une caractéristique essentielle de l'objet.) Vous savez qu'un Employeedoit avoir un attribut de salaire ou de salaire, sinon ce ne serait pas un Employee. (Ce serait un Person, un Volunteer, un Vagrant, ou quelque chose d'autre qui n'a pas de salaire.) Ce que vous ne savez pas - ou voulez savoir - c'est comment ce salaire est représenté à l'intérieur de l'objet. Il peut s'agir d'un décimal double, d'un a String, d'une échelle longou d'un code binaire. Il peut s'agir d'un attribut «synthétique» ou «dérivé», qui est calculé au moment de l'exécution (à partir d'un niveau de rémunération ou d'un titre de poste, par exemple, ou en récupérant la valeur dans une base de données). Bien qu'une méthode get puisse effectivement masquer certains de ces détails d'implémentation,comme nous l'avons vu avec leMoney exemple, il ne peut pas se cacher assez.

Alors, comment un objet produit-il sa propre interface utilisateur et reste maintenable? Seuls les objets les plus simplistes peuvent prendre en charge quelque chose comme une displayYourself()méthode. Les objets réalistes doivent:

  • S'afficher dans différents formats (XML, SQL, valeurs séparées par des virgules, etc.).
  • Afficher différentes vues d'eux-mêmes (une vue peut afficher tous les attributs; une autre peut afficher uniquement un sous-ensemble des attributs; et une troisième peut présenter les attributs d'une manière différente).
  • S'afficher dans différents environnements (côté client ( JComponent) et servi au client (HTML), par exemple) et gérer à la fois l'entrée et la sortie dans les deux environnements.

Certains des lecteurs de mon précédent article getter / setter ont sauté à la conclusion que je préconisais d'ajouter des méthodes à l'objet pour couvrir toutes ces possibilités, mais cette «solution» est évidemment absurde. Non seulement l'objet lourd résultant est beaucoup trop compliqué, mais vous devrez constamment le modifier pour gérer les nouvelles exigences de l'interface utilisateur. En pratique, un objet ne peut tout simplement pas créer toutes les interfaces utilisateur possibles pour lui-même, si pour aucune autre raison que la plupart de ces interfaces utilisateur n'ont même pas été conçues lorsque la classe a été créée.

Construire une solution

La solution à ce problème consiste à séparer le code de l'interface utilisateur de l'objet métier principal en le plaçant dans une classe d'objets distincte. Autrement dit, vous devez séparer complètement certaines fonctionnalités qui pourraient être dans l'objet en un objet distinct.

Cette bifurcation des méthodes d'un objet apparaît dans plusieurs modèles de conception. Vous êtes probablement familier avec Strategy, qui est utilisé avec les différentes java.awt.Containerclasses pour faire la mise en page. Vous pouvez résoudre le problème de mise en page avec une solution de dérivation: FlowLayoutPanel, GridLayoutPanel, BorderLayoutPanel, etc., mais que les mandats trop de classes et beaucoup de code dupliqués dans ces classes. Une seule solution de classe lourde (ajouter des méthodes à Containerlike layOutAsGrid(), layOutAsFlow()etc.) est également peu pratique car vous ne pouvez pas modifier le code source pour le Containersimplement parce que vous avez besoin d'une mise en page non prise en charge. Dans le modèle de stratégie, vous créez une Strategyinterface ( LayoutManager) mis en œuvre par plusieurs Concrete Strategyclasses ( FlowLayout, GridLayout, etc.). Vous dites alors à un Contextobjet (unContainer) comment faire quelque chose en lui passant un Strategyobjet. (Vous passez un Containera LayoutManagerqui définit une stratégie de mise en page.)

Le modèle Builder est similaire à Stratégie. La principale différence est que la Builderclasse implémente une stratégie pour construire quelque chose (comme un JComponentflux ou XML qui représente l'état d'un objet). Builderles objets construisent généralement leurs produits à l'aide d'un processus en plusieurs étapes. Autrement dit, les appels à diverses méthodes du Buildersont nécessaires pour terminer le processus de construction, et Buildergénéralement ne sait pas l'ordre dans lequel les appels seront effectués ou le nombre de fois qu'une de ses méthodes sera appelée. La caractéristique la plus importante de Builder est que l'objet métier (appelé le Context) ne sait pas exactement ce que l' Builderobjet construit. Le modèle isole l'objet métier de sa représentation.

La meilleure façon de voir comment fonctionne un simple constructeur est d'en regarder un. Examinons d'abord Contextl'objet métier qui doit exposer une interface utilisateur. Le listing 1 montre une Employeeclasse simpliste . Les attributs Employeehas name, idet salary. (Les talons de ces classes se trouvent au bas de la liste, mais ces talons ne sont que des espaces réservés pour la réalité. Vous pouvez - j'espère - facilement imaginer comment ces classes fonctionneraient.)

Ce particulier Contextutilise ce que je considère comme un constructeur bidirectionnel. Le classique Gang of Four Builder va dans une direction (sortie), mais j'ai également ajouté un Builderqu'un Employeeobjet peut utiliser pour s'initialiser. Deux Builderinterfaces sont nécessaires. L' Employee.Exporterinterface (liste 1, ligne 8) gère la direction de sortie. Il définit une interface avec un Builderobjet qui construit la représentation de l'objet courant. Le Employeedélègue la construction réelle de l'interface utilisateur au Builderdans la export()méthode (à la ligne 31). Le Builderne passe pas les champs réels, mais utilise à la place Strings pour transmettre une représentation de ces champs.

Listing 1. Employé: le contexte du constructeur

1 import java.util.Locale; 2 3 classe publique Employé 4 {nom privé nom; 5 identifiant privé EmployeeId; 6 salaire privé en argent; 7 8 public interface Exporter 9 {void addName (String name); 10 void addID (ID de chaîne); 11 void addSalary (String salaire); 12} 13 14 public interface Importer 15 {String provideName (); 16 Chaîne provideID (); 17 Chaîne provideSalary (); 18 vide ouvert (); 19 vide fermer (); 20} 21 22 Employé public (constructeur importateur) 23 {builder.open (); 24 this.name = nouveau nom (builder.provideName ()); 25 this.id = new EmployeeId (builder.provideID ()); 26 this.salary = new Money (builder.provideSalary (), 27 new Locale ("en", "US")); 28 constructeur.close (); 29} 30 31 exportation publique void (générateur d'exportateur) 32 {builder.addName (nom.toString ()); 33 builder.addID (id.toString ()); 34 builder.addSalary (salaire.toString ()); 35} 36 37// ... 38} 39 // ---------------------------------------- ------------------------------ 40 // Trucs de test unitaire 41 // 42 nom de classe 43 {valeur de chaîne privée; 44 public Name (valeur de chaîne) 45 {this.value = value; 46} 47 public String toString () {valeur de retour; }; 48} 49 50 class EmployeeId 51 {valeur de chaîne privée; 52 public EmployeeId (valeur de chaîne) 53 {this.value = value; 54} 55 public String toString () {valeur de retour; } 56} 57 58 classe Money 59 {valeur de chaîne privée; 60 public Money (valeur de chaîne, emplacement des paramètres régionaux) 61 {this.value = value; 62} 63 public String toString () {valeur de retour; } 64}

Regardons un exemple. Le code suivant crée l'interface utilisateur de la figure 1:

Employé wilma = ...; JComponentExporter uiBuilder = nouveau JComponentExporter (); // Créer le générateur wilma.export (uiBuilder); // Construire l'interface utilisateur JComponent userInterface = uiBuilder.getJComponent (); // ... someContainer.add (userInterface);

Le listing 2 montre la source du JComponentExporter. Comme vous pouvez le voir, tout le code lié à l'interface utilisateur est concentré dans le Concrete Builder(le JComponentExporter), et le Context(le Employee) pilote le processus de construction sans savoir exactement ce qu'il construit.

Listing 2. Exportation vers une interface utilisateur côté client

1 import javax.swing. *; 2 import java.awt. *; 3 import java.awt.event. *; 4 5 classe JComponentExporter implémente Employee.Exporter 6 {chaîne privée nom, id, salaire; 7 8 public void addName (String name) {this.name = nom; } 9 public void addID (String id) {this.id = id; } 10 public void addSalary (String salaire) {this.salary = salaire; } 11 12 JComponent getJComponent () 13 {JComponent panel = new JPanel (); 14 panel.setLayout (nouveau GridLayout (3,2)); 15 panel.add (nouveau JLabel ("Nom:")); 16 panel.add (nouveau JLabel (nom)); 17 panel.add (nouveau JLabel ("Employee ID:")); 18 panel.add (nouveau JLabel (id)); 19 panel.add (nouveau JLabel ("Salary:")); 20 panel.add (nouveau JLabel (salaire)); 21 panneau de retour; 22} 23}