Astuce Java 68: Découvrez comment implémenter le modèle de commande en Java

Les modèles de conception accélèrent non seulement la phase de conception d'un projet orienté objet (OO), mais augmentent également la productivité de l'équipe de développement et la qualité du logiciel. Un modèle de commande est un modèle de comportement d'objet qui nous permet d'obtenir un découplage complet entre l'expéditeur et le récepteur. (Un expéditeur est un objet qui appelle une opération, et un destinataire est un objet qui reçoit la demande d'exécuter une certaine opération. Avec le découplage, l'expéditeur n'a aucune connaissance de l' Receiverinterface de.) Le terme demandese réfère ici à la commande à exécuter. Le modèle de commande nous permet également de varier le moment et la manière dont une demande est satisfaite. Par conséquent, un modèle de commande nous offre une flexibilité ainsi qu'une extensibilité.

Dans les langages de programmation comme C, les pointeurs de fonction sont utilisés pour éliminer les instructions de commutation géantes. (Voir «Astuce Java 30: Polymorphisme et Java» pour une description plus détaillée.) Puisque Java n'a pas de pointeurs de fonction, nous pouvons utiliser le modèle Command pour implémenter des rappels. Vous verrez cela en action dans le premier exemple de code ci-dessous, appelé TestCommand.java.

Les développeurs habitués à utiliser des pointeurs de fonction dans un autre langage pourraient être tentés d'utiliser les Methodobjets de l'API Reflection de la même manière. Par exemple, dans son article «Java Reflection», Paul Tremblett vous montre comment utiliser Reflection pour implémenter des transactions sans utiliser d'instructions switch. J'ai résisté à cette tentation, puisque Sun déconseille d'utiliser l'API Reflection alors que d'autres outils plus naturels au langage de programmation Java suffiront. (Voir Ressources pour les liens vers l'article de Tremblett et pour la page du didacticiel Sun's Reflection.) Votre programme sera plus facile à déboguer et à maintenir si vous n'utilisez pas d' Methodobjets. Au lieu de cela, vous devez définir une interface et l'implémenter dans les classes qui exécutent l'action nécessaire.

Par conséquent, je vous suggère d'utiliser le modèle de commande combiné avec le mécanisme de chargement dynamique et de liaison de Java pour implémenter des pointeurs de fonction. (Pour plus de détails sur le chargement dynamique et le mécanisme de liaison de Java, consultez "The Java Language Environment - A White Paper" de James Gosling et Henry McGilton, répertorié dans Ressources.)

En suivant la suggestion ci-dessus, nous exploitons le polymorphisme fourni par l'application d'un modèle de commande pour éliminer les instructions de commutation géantes, résultant en des systèmes extensibles. Nous exploitons également les mécanismes uniques de chargement dynamique et de liaison de Java pour créer un système dynamique et extensible de manière dynamique. Ceci est illustré dans le deuxième exemple de code ci-dessous, appelé TestTransactionCommand.java.

Le modèle Command transforme la requête elle-même en objet. Cet objet peut être stocké et transmis comme d'autres objets. La clé de ce modèle est une Commandinterface, qui déclare une interface pour l'exécution des opérations. Dans sa forme la plus simple, cette interface comprend une executeopération abstraite . Chaque Commandclasse concrète spécifie une paire récepteur-action en stockant le Receivercomme variable d'instance. Il fournit différentes implémentations de la execute()méthode pour appeler la requête. Le Receiverpossède les connaissances nécessaires pour exécuter la demande.

La figure 1 ci-dessous montre le Switch- une agrégation d' Commandobjets. Il a flipUp()et flipDown()opérations dans son interface. Switchest appelé l' invocateur car il appelle l'opération d'exécution dans l'interface de commande.

La commande concrète,, LightOnCommandimplémente le executefonctionnement de l'interface de commande. Il a les connaissances nécessaires pour appeler l' Receiveropération de l'objet approprié . Il agit comme un adaptateur dans ce cas. Par le terme adaptateur, je veux dire que l' Commandobjet concret est un simple connecteur, reliant le Invokeret le Receiveravec des interfaces différentes.

Le client instancie les objets de commande Invoker, le Receiveret les objets de commande concrets.

La figure 2, le diagramme de séquence, montre les interactions entre les objets. Il illustre comment Commanddissocie le Invokerdu Receiver(et la requête qu'il exécute). Le client crée une commande concrète en paramétrant son constructeur avec le Receiver. Ensuite, il stocke le Commanddans le Invoker. Le Invokerrappelle la commande concrète, qui a les connaissances nécessaires pour effectuer l' Action()opération souhaitée .

Le client (programme principal dans le listing) crée un Commandobjet concret et définit son Receiver. En tant Invokerqu'objet, Switchstocke l' Commandobjet concret . Le Invokerémet une requête en appelant executel' Commandobjet. L' Commandobjet concret invoque des opérations sur lui Receiverpour exécuter la requête.

L'idée clé ici est que la commande concrète s'enregistre avec le Invokeret le Invokerrappelle, exécutant la commande sur le Receiver.

Exemple de code de modèle de commande

Jetons un coup d'œil à un exemple simple illustrant le mécanisme de rappel réalisé via le modèle Command.

L'exemple montre a Fanet a Light. Notre objectif est de développer un Switchqui peut activer ou désactiver l'un ou l'autre des objets. On voit que le Fanet le Lightont des interfaces différentes, ce qui signifie que le Switchdoit être indépendant de l' Receiverinterface ou qu'il n'a pas connaissance du code> Interface du récepteur. Pour résoudre ce problème, nous devons paramétrer chacun des Switchs avec la commande appropriée. De toute évidence, le Switchconnecté au Lightaura une commande différente de celui Switchconnecté au Fan. La Commandclasse doit être abstraite ou une interface pour que cela fonctionne.

Lorsque le constructeur de a Switchest appelé, il est paramétré avec l'ensemble de commandes approprié. Les commandes seront stockées en tant que variables privées du Switch.

Lorsque les opérations flipUp()et flipDown()sont appelées, elles exécutent simplement la commande appropriée execute( ). La Switchvolonté n'a aucune idée de ce qui se passe après execute( )avoir été appelé.

TestCommand.java class Fan {public void startRotate () {System.out.println ("Le ventilateur tourne"); } public void stopRotate () {System.out.println ("Le ventilateur ne tourne pas"); }} classe Light {public void turnOn () {System.out.println ("Light is on"); } public void turnOff () {System.out.println ("La lumière est éteinte"); }} classe Switch {Commande privée UpCommand, DownCommand; commutateur public (commande vers le haut, commande vers le bas) {UpCommand = haut; // La commande concrète s'enregistre avec l'appelant DownCommand = Down; } void flipUp () {// l'invocateur rappelle la commande concrète, qui exécute la commande sur le récepteur UpCommand. exécuter (); } void flipDown () {DownCommand. exécuter (); }} la classe LightOnCommand implémente Command {private Light myLight; public LightOnCommand (Light L) {myLight = L;} public void execute () {myLight. allumer( ); }} la classe LightOffCommand implémente Command {private Light myLight; public LightOffCommand (Light L) {myLight = L; } public void execute () {myLight. éteindre( ); }} la classe FanOnCommand implémente Command {private Fan myFan; public FanOnCommand (Fan F) {myFan = F; } public void execute () {myFan. startRotate (); }} la classe FanOffCommand implémente Command {private Fan myFan; public FanOffCommand (Fan F) {myFan = F; } public void execute () {myFan. stopRotate (); }} classe publique TestCommand {public static void main (String [] args) {Light testLight = new Light (); LightOnCommand testLOC = new LightOnCommand (testLight); LightOffCommand testLFC = new LightOffCommand (testLight); Commutateur testSwitch = nouveau commutateur (testLOC, testLFC); testSwitch.flipUp (); testSwitch.flipDown ();Fan testFan = nouveau ventilateur (); FanOnCommand foc = new FanOnCommand (testFan); FanOffCommand ffc = new FanOffCommand (testFan); Switch ts = new Switch (foc, ffc); ts.flipUp (); ts.flipDown (); }} Command.java interface publique Commande {public abstract void execute (); }

Notez dans l'exemple de code ci-dessus que le modèle Command dissocie complètement l'objet qui appelle l'opération - (Switch )- de ceux qui ont les connaissances pour l'exécuter - Lightet Fan. Cela nous donne beaucoup de flexibilité: l'objet émettant une requête ne doit savoir que comment l'émettre; il n'a pas besoin de savoir comment la demande sera exécutée.

Modèle de commande pour implémenter des transactions

Un modèle de commande est également appelé modèle d' action ou de transaction. Considérons un serveur qui accepte et traite les transactions livrées par les clients via une connexion socket TCP / IP. Ces transactions consistent en une commande, suivie de zéro ou plusieurs arguments.

Les développeurs peuvent utiliser une instruction switch avec un cas pour chaque commande. L'utilisation d' Switchinstructions lors du codage est un signe de mauvaise conception lors de la phase de conception d'un projet orienté objet. Les commandes représentent une manière orientée objet de prendre en charge les transactions et peuvent être utilisées pour résoudre ce problème de conception.

Dans le code client du programme TestTransactionCommand.java, toutes les requêtes sont encapsulées dans l' TransactionCommandobjet générique . Le TransactionCommandconstructeur est créé par le client et enregistré avec le CommandManager. Les requêtes en file d'attente peuvent être exécutées à des moments différents en appelant le runCommands(), ce qui nous donne beaucoup de flexibilité. Cela nous donne également la possibilité d'assembler des commandes dans une commande composite. J'ai aussi CommandArgument, CommandReceiveret les CommandManagerclasses et sous - classes de TransactionCommand- à savoir AddCommandet SubtractCommand. Voici une description de chacune de ces classes:

  • CommandArgumentest une classe d'assistance, qui stocke les arguments de la commande. Il peut être réécrit pour simplifier la tâche consistant à transmettre un nombre important ou variable d'arguments de tout type.

  • CommandReceiver implémente toutes les méthodes de traitement des commandes et est implémenté en tant que modèle Singleton.

  • CommandManagerest l'invocateur et est l' Switchéquivalent de l'exemple précédent. Il stocke l' TransactionCommandobjet générique dans sa myCommandvariable privée . Quand runCommands( )est invoqué, il appelle le execute( )de l' TransactionCommandobjet approprié .

En Java, il est possible de rechercher la définition d'une classe à partir d'une chaîne contenant son nom. Dans le execute ( )fonctionnement de la TransactionCommandclasse, je calcule le nom de la classe et je le lie dynamiquement au système en cours d'exécution - c'est-à-dire que les classes sont chargées à la volée selon les besoins. J'utilise la convention de dénomination, nom de commande concaténé par la chaîne "Command" comme nom de la sous-classe de commande de transaction, afin qu'elle puisse être chargée dynamiquement.

Notez que l' Classobjet renvoyé par le newInstance( )doit être converti dans le type approprié. Cela signifie que la nouvelle classe doit soit implémenter une interface, soit sous-classer une classe existante qui est connue du programme au moment de la compilation. Dans ce cas, puisque nous implémentons l' Commandinterface, ce n'est pas un problème.