Suggerimento Java 68: scopri come implementare il modello di comando in Java
I modelli di progettazione non solo accelerano la fase di progettazione di un progetto orientato agli oggetti (OO), ma aumentano anche la produttività del team di sviluppo e la qualità del software. Un modello di comando è un modello comportamentale dell'oggetto che ci consente di ottenere il disaccoppiamento completo tra il mittente e il destinatario. (Un mittente è un oggetto che richiama un'operazione e un destinatario è un oggetto che riceve la richiesta di eseguire una determinata operazione. Con il disaccoppiamento, il mittente non ha alcuna conoscenza Receiver
dell'interfaccia di.) Il termine richiestaqui si riferisce al comando che deve essere eseguito. Il modello di comando ci consente anche di variare quando e come viene soddisfatta una richiesta. Pertanto, un modello di comando ci fornisce flessibilità ed estensibilità.
Nei linguaggi di programmazione come C, i puntatori a funzione vengono utilizzati per eliminare le istruzioni switch giganti. (Vedere "Suggerimento Java 30: polimorfismo e Java" per una descrizione più dettagliata.) Poiché Java non dispone di puntatori a funzione, è possibile utilizzare il modello di comando per implementare i callback. Lo vedrai in azione nel primo esempio di codice di seguito, chiamato TestCommand.java
.
Gli sviluppatori abituati a utilizzare i puntatori a funzione in un altro linguaggio potrebbero essere tentati di utilizzare gli Method
oggetti dell'API di Reflection allo stesso modo. Ad esempio, nel suo articolo "Java Reflection", Paul Tremblett mostra come utilizzare Reflection per implementare transazioni senza utilizzare istruzioni switch. Ho resistito a questa tentazione, poiché Sun sconsiglia di utilizzare l'API Reflection quando saranno sufficienti altri strumenti più naturali per il linguaggio di programmazione Java. (Vedere Risorse per i collegamenti all'articolo di Tremblett e per la pagina del tutorial su Sun Reflection.) Il programma sarà più facile da eseguire il debug e da mantenere se non si utilizzano Method
oggetti. Invece, dovresti definire un'interfaccia e implementarla nelle classi che eseguono l'azione necessaria.
Pertanto, ti suggerisco di utilizzare il modello di comando combinato con il caricamento dinamico e il meccanismo di associazione di Java per implementare i puntatori a funzione. (Per i dettagli sul caricamento dinamico di Java e sul meccanismo di associazione, vedere "The Java Language Environment - A White Paper" di James Gosling e Henry McGilton, elencato in Risorse.)
Seguendo il suggerimento di cui sopra, sfruttiamo il polimorfismo fornito dall'applicazione di un pattern Command per eliminare le istruzioni switch giganti, ottenendo sistemi estensibili. Sfruttiamo inoltre gli esclusivi meccanismi di caricamento e associazione dinamici di Java per creare un sistema dinamico ed estensibile dinamicamente. Ciò è illustrato nel secondo esempio di codice riportato di seguito, chiamato TestTransactionCommand.java
.
Il pattern Command trasforma la richiesta stessa in un oggetto. Questo oggetto può essere memorizzato e passato in giro come altri oggetti. La chiave di questo modello è Command
un'interfaccia, che dichiara un'interfaccia per l'esecuzione delle operazioni. Nella sua forma più semplice, questa interfaccia include un'operazione astratta execute
. Ogni Command
classe concreta specifica una coppia ricevitore-azione memorizzando Receiver
come variabile di istanza. Fornisce diverse implementazioni del execute()
metodo per richiamare la richiesta. L' Receiver
ha le conoscenze necessarie per effettuare la richiesta.
La figura 1 di seguito mostra Switch
- un'aggregazione di Command
oggetti. Ha flipUp()
e flipDown()
operazioni nella sua interfaccia. Switch
è chiamato invoker perché richiama l'operazione di esecuzione nell'interfaccia dei comandi.
Il comando concrete,, LightOnCommand
implementa il execute
funzionamento dell'interfaccia di comando. Ha la conoscenza per chiamare l' Receiver
operazione dell'oggetto appropriato . In questo caso funge da adattatore. Con il termine adattatore, intendo che l' Command
oggetto concreto è un semplice connettore, che collega il Invoker
e il Receiver
con diverse interfacce.
Il client istanzia gli oggetti comando Invoker
, the Receiver
e concrete.
La Figura 2, il diagramma di sequenza, mostra le interazioni tra gli oggetti. Illustra come Command
disaccoppia il Invoker
da Receiver
(e la richiesta che esegue). Il client crea un comando concreto parametrizzando il suo costruttore con l'appropriato Receiver
. Quindi memorizza il file Command
in Invoker
. Il Invoker
richiama il comando concreto, che ha le conoscenze per eseguire l' Action()
operazione desiderata .
Il client (programma principale nell'elenco) crea un Command
oggetto concreto e lo imposta Receiver
. Come Invoker
oggetto, Switch
immagazzina l' Command
oggetto concreto . Il Invoker
emette una richiesta chiamando execute
in Command
oggetto. L' Command
oggetto concreto richiama operazioni su di esso Receiver
per eseguire la richiesta.
L'idea chiave qui è che il comando concreto si registra con il Invoker
e lo Invoker
richiama, eseguendo il comando sul Receiver
.
Codice di esempio del modello di comando
Diamo un'occhiata a un semplice esempio che illustra il meccanismo di callback ottenuto tramite il pattern Command.
L'esempio mostra a Fan
e a Light
. Il nostro obiettivo è sviluppare un oggetto Switch
che possa accendere o spegnere gli oggetti. Vediamo che il Fan
e il Light
hanno interfacce diverse, il che significa che Switch
deve essere indipendente Receiver
dall'interfaccia o non ha conoscenza del codice> Interfaccia del ricevitore. Per risolvere questo problema, dobbiamo parametrizzare ciascuna delle Switch
s con il comando appropriato. Ovviamente il Switch
connesso al Light
avrà un comando diverso da quello Switch
connesso al Fan
. La Command
classe deve essere astratta o un'interfaccia affinché funzioni.
Quando Switch
viene richiamato il costruttore di a , viene parametrizzato con il set di comandi appropriato. I comandi verranno memorizzati come variabili private del file Switch
.
Quando vengono chiamate le operazioni flipUp()
e flipDown()
, eseguiranno semplicemente il comando appropriato execute( )
. Non Switch
avranno idea di cosa succede dopo execute( )
essere stati chiamati.
Classe TestCommand.java Fan {public void startRotate () {System.out.println ("La ventola sta ruotando"); } public void stopRotate () {System.out.println ("La ventola non ruota"); }} class Light {public void turnOn () {System.out.println ("Light is on"); } public void turnOff () {System.out.println ("Light is off"); }} classe Switch {private Command UpCommand, DownCommand; interruttore pubblico (Comando su, Comando giù) {Comando su = Su; // concrete Command si registra con l'invoker DownCommand = Down; } void flipUp () {// invoker richiama il comando concreto, che esegue il comando sul ricevitore UpCommand. eseguire ( ) ; } void flipDown () {DownCommand. eseguire ( ); }} la classe LightOnCommand implementa Command {private Light myLight; public LightOnCommand (Light L) {myLight = L;} public void execute () {myLight. accendere( ); }} la classe LightOffCommand implementa Command {private Light myLight; public LightOffCommand (Light L) {myLight = L; } public void execute () {myLight. Spegni( ); }} la classe FanOnCommand implementa Command {private Fan myFan; public FanOnCommand (Fan F) {myFan = F; } public void execute () {myFan. startRotate (); }} la classe FanOffCommand implementa il comando {private Fan myFan; public FanOffCommand (Fan F) {myFan = F; } public void execute () {myFan. stopRotate (); }} public class TestCommand {public static void main (String [] args) {Light testLight = new Light (); LightOnCommand testLOC = nuovo LightOnCommand (testLight); LightOffCommand testLFC = new LightOffCommand (testLight); Switch testSwitch = new Switch (testLOC, testLFC); testSwitch.flipUp (); testSwitch.flipDown ();Fan testFan = nuovo Fan (); FanOnCommand foc = nuovo FanOnCommand (testFan); FanOffCommand ffc = nuovo FanOffCommand (testFan); Switch ts = nuovo Switch (foc, ffc); ts.flipUp (); ts.flipDown (); }} Command.java interfaccia pubblica Command {public abstract void execute (); }
Si noti nell'esempio di codice sopra che il pattern Command disaccoppia completamente l'oggetto che richiama l'operazione - (Switch )
- da quelli che hanno la conoscenza per eseguirla - Light
e Fan
. Questo ci dà molta flessibilità: l'oggetto che emette una richiesta deve sapere solo come emetterla; non è necessario sapere come verrà eseguita la richiesta.
Modello di comando per implementare le transazioni
Un modello di comando è noto anche come modello di azione o transazione. Consideriamo un server che accetta ed elabora le transazioni fornite dai client tramite una connessione socket TCP / IP. Queste transazioni consistono in un comando, seguito da zero o più argomenti.
Gli sviluppatori potrebbero utilizzare un'istruzione switch con un caso per ogni comando. L'utilizzo di Switch
dichiarazioni durante la codifica è un segno di cattiva progettazione durante la fase di progettazione di un progetto orientato agli oggetti. I comandi rappresentano un modo orientato agli oggetti per supportare le transazioni e possono essere utilizzati per risolvere questo problema di progettazione.
Nel codice client del programma TestTransactionCommand.java
, tutte le richieste sono incapsulate TransactionCommand
nell'oggetto generico . Il TransactionCommand
costruttore viene creato dal client ed è registrato con il CommandManager
. Le richieste in coda possono essere eseguite in momenti diversi chiamando il runCommands()
, il che ci offre molta flessibilità. Ci dà anche la possibilità di assemblare i comandi in un comando composito. Ho anche CommandArgument
, CommandReceiver
e CommandManager
classi e sottoclassi di TransactionCommand
- vale a dire AddCommand
e SubtractCommand
. Di seguito è una descrizione di ciascuna di queste classi:
CommandArgument
è una classe helper, che memorizza gli argomenti del comando. Può essere riscritto per semplificare il compito di passare un numero elevato o variabile di argomenti di qualsiasi tipo.CommandReceiver
implementa tutti i metodi di elaborazione dei comandi ed è implementato come pattern Singleton.CommandManager
è l'invocatore ed è l'Switch
equivalente dell'esempio precedente. Memorizza l'TransactionCommand
oggetto generico nella suamyCommand
variabile privata . QuandorunCommands( )
viene invocato, chiama il valore dell'oggettoexecute( )
appropriatoTransactionCommand
.
In Java, è possibile cercare la definizione di una classe data una stringa contenente il suo nome. Durante il execute ( )
funzionamento della TransactionCommand
classe, calcolo il nome della classe e lo collego dinamicamente al sistema in esecuzione, ovvero le classi vengono caricate al volo come richiesto. Uso la convenzione di denominazione, nome del comando concatenato dalla stringa "Comando" come nome della sottoclasse del comando di transazione, in modo che possa essere caricato dinamicamente.
Si noti che l' Class
oggetto restituito da newInstance( )
deve essere sottoposto a cast al tipo appropriato. Ciò significa che la nuova classe deve implementare un'interfaccia o una sottoclasse di una classe esistente nota al programma in fase di compilazione. In questo caso, poiché implementiamo l' Command
interfaccia, questo non è un problema.