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 Receiverdell'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 Methodoggetti 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 Methodoggetti. 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 è Commandun'interfaccia, che dichiara un'interfaccia per l'esecuzione delle operazioni. Nella sua forma più semplice, questa interfaccia include un'operazione astratta execute. Ogni Commandclasse concreta specifica una coppia ricevitore-azione memorizzando Receivercome variabile di istanza. Fornisce diverse implementazioni del execute()metodo per richiamare la richiesta. L' Receiverha le conoscenze necessarie per effettuare la richiesta.

La figura 1 di seguito mostra Switch- un'aggregazione di Commandoggetti. Ha flipUp()e flipDown()operazioni nella sua interfaccia. Switchè chiamato invoker perché richiama l'operazione di esecuzione nell'interfaccia dei comandi.

Il comando concrete,, LightOnCommandimplementa il executefunzionamento dell'interfaccia di comando. Ha la conoscenza per chiamare l' Receiveroperazione dell'oggetto appropriato . In questo caso funge da adattatore. Con il termine adattatore, intendo che l' Commandoggetto concreto è un semplice connettore, che collega il Invokere il Receivercon diverse interfacce.

Il client istanzia gli oggetti comando Invoker, the Receivere concrete.

La Figura 2, il diagramma di sequenza, mostra le interazioni tra gli oggetti. Illustra come Commanddisaccoppia il Invokerda Receiver(e la richiesta che esegue). Il client crea un comando concreto parametrizzando il suo costruttore con l'appropriato Receiver. Quindi memorizza il file Commandin Invoker. Il Invokerrichiama il comando concreto, che ha le conoscenze per eseguire l' Action()operazione desiderata .

Il client (programma principale nell'elenco) crea un Commandoggetto concreto e lo imposta Receiver. Come Invokeroggetto, Switchimmagazzina l' Commandoggetto concreto . Il Invokeremette una richiesta chiamando executein Commandoggetto. L' Commandoggetto concreto richiama operazioni su di esso Receiverper eseguire la richiesta.

L'idea chiave qui è che il comando concreto si registra con il Invokere lo Invokerrichiama, 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 Fane a Light. Il nostro obiettivo è sviluppare un oggetto Switchche possa accendere o spegnere gli oggetti. Vediamo che il Fane il Lighthanno interfacce diverse, il che significa che Switchdeve essere indipendente Receiverdall'interfaccia o non ha conoscenza del codice> Interfaccia del ricevitore. Per risolvere questo problema, dobbiamo parametrizzare ciascuna delle Switchs con il comando appropriato. Ovviamente il Switchconnesso al Lightavrà un comando diverso da quello Switchconnesso al Fan. La Commandclasse deve essere astratta o un'interfaccia affinché funzioni.

Quando Switchviene 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 Switchavranno 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 - Lighte 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 Switchdichiarazioni 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 TransactionCommandnell'oggetto generico . Il TransactionCommandcostruttore 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, CommandReceivere CommandManagerclassi e sottoclassi di TransactionCommand- vale a dire AddCommande 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' Switchequivalente dell'esempio precedente. Memorizza l' TransactionCommandoggetto generico nella sua myCommandvariabile privata . Quando runCommands( )viene invocato, chiama il valore dell'oggetto execute( )appropriato TransactionCommand.

In Java, è possibile cercare la definizione di una classe data una stringa contenente il suo nome. Durante il execute ( )funzionamento della TransactionCommandclasse, 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' Classoggetto 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' Commandinterfaccia, questo non è un problema.