Parancs programtervezési minta

Az szoftverfejlesztésben a Parancs (Command) minta egy objektumorientált viselkedési minta, melynél egy adott fogadó (Receiver) objektumra történő, a paraméterekkel és minden egyéb függőséggel együtt megadott metódushívást (akció, Action) egy parancs (Command) objektumba zárunk (későbbi meghívás céljából). A parancs objektum tartalmaz egy egyszerű, általában paraméter nélküli publikus metódust (például execute), ennek hívásakor kerül végrehajtásra az akció. A parancs objektum így átadható egy hívó (Invoker) objektumnak, melynek csak az a feladata, hogy (akár szolgaian, akár valamilyen stratégia szerint) végrehajtsa. A parancsokat általában egy vagy több ügyfél (Client) objektum állítja össze és adogatja át a hívónak.

A parancs minta megkönnyíti a generikus komponensek létrehozását. Az ügyfélnek nem kell tudnia arról, hogy a hívó milyen stratégiával és kiegészítésekkel kezeli a végrehajtásokat. A hívónak pedig nem kell ismernie a parancsban lévő hívás részleteit.

Felhasználása

[szerkesztés]

A parancs objektumok az implementáláshoz hasznosak.

GUI gombok és menü elemek

[szerkesztés]

Swing és Borland Delphi programozási nyelvekben, az „Action” (művelet) egy parancs objektum. Amellett, hogy képes végrehajtani a kívánt parancsot, egy „Action” lehet egy társított ikon, egy billentyűzet gyorsbillentyű, tooltip, és így tovább. Egy eszköztár gomb, vagy egy menüpont összetevője létrejöhet kizárólag egy „Action” vezérlése végett.

Makró felvétel

[szerkesztés]

Ha minden felhasználói műveletet (Action) egy parancs objektum reprezentál, akkor a program képes rögzíteni egy összetett művelet lépéseit (Macro) azáltal, hogy egy listába gyűjti a végrehajtott parancs objektumokat. Később ezeket az eredeti sorrendben végighívva újrajátszhatja a műveletsort.

Ha a program "Script" motort is tartalmaz, minden parancs objektum implementálhat egy toScript() metódust, és a felhasználói műveletek akkor könnyebben rögzítve lehetnek "Script"-ként.

Mobil kódolás

[szerkesztés]

Olyan nyelveket használva, mint például Java, ahol a kód "Stream"-elhető egy bizonyos távoli helyről egy másikra "URLClassloader"-ek és "Codebase"-k segítségével, ott a parancsok lehetővé tesznek egy újfajta viselkedéseket, a távoli helyeken. (EJB Command, Master Worker)

Többszintű visszavonás

[szerkesztés]

A parancs objektum egy gyakori bővített változata, amikor a normál akció mellett egy visszavonó (undo) akciót is tárol, melynek meghívására az állapot visszaáll a normál akció meghívása előttire. Előfordulhat, hogy ez az undo akció késleltetett kiértékelésű, főleg ha csak a művelet végrehajtása után állapítható meg, hogy hogyan kell visszavonni (például random vagy a fogadó állapotától függő műveleteknél). Ha minden felhasználói művelet ilyen típusú parancs objektumként van implementálva, és ezeket (vagy legalább a legutóbbiakat) egy veremben tárolja a program, akkor sorra vissza is lehet őket vonni. Amikor a felhasználó a visszavonást kezdeményezi, lekérésre kerül a veremből a legutóbbi parancs objektum, amelyen rögtön végre kell hajtani az `undo` metódust. Ennek sikeres végrehajtása után a parancs kikerül a veremből (illetve opcionálisan átkerül egy redo verembe).

Networking

[szerkesztés]

Lehetőség van rengeteg parancs objektum küldésére a hálózaton keresztül, hogy végrehajtsuk azt egy másik számítógépen.

Párhuzamos feldolgozás

[szerkesztés]

Itt a parancsok egy-egy feladatként (task) vannak megírva egy megosztott erőforrás részére, és potenciálisan több szálon, párhuzamosan lesznek végrehajtva.

Folyamatjelzők

[szerkesztés]

Feltételezzük, hogy egy program parancsok soraiból épül fel melyek sorban hajtódnak végre. Parancs objektumonként van egy getEstimatedDuration() metódusunk, mellyel, a program könnyen megbecsüli annak teljes hosszát (időtartamát). Ez tükröződhet egy folyamatjelző által, mely észszerűen mutatja, hogy milyen közel van a program ahhoz, hogy végezzen az összes feladattal.

Thread pools (Szálcsoportok)

[szerkesztés]

Egy tipikus, általános célú szálcsoport osztálynak rendelkeznie kell egy publikus addTask() metódussal, amely hozzáad egy munkaeszközt egy belső feladat sorához (queue-hoz) várván a feladat befejezésére. Ez azt jelenti, hogy a szálcsoport meghívja a parancsokat a (queue-ból) sorból. A sor elemei parancs objektumok. Általában ezek az objektumok egy közös interfészt (common interface-t) implementálnak, mint például „java.lang.Runnable” amely lehetőséget nyújt a szálcsoportoknak, hogy végrehajtsák a parancsokat még akkor is, ha maga a szálcsoport osztály úgy van megírva, hogy nincs ismerete a konkrét feladatokról.

Tranzakciós viselkedés

[szerkesztés]

Hasonlóan a visszavonáshoz, egy adatbázis motor, vagy egy szoftver telepítő tartalmazhat egy listát a műveletekről, amely végre lettek, vagy végre lesznek hajtva. Amennyiben valamelyikük nem sikerül, az összes többi visszafordítható vagy eldobható (általában „rollback”-nek hívják). Például, ha van két adatbázis tábla, melyek hivatkoznak egymásra, amiket frissíteni kell, és a második frissítés nem sikerül, a tranzakció visszaállítható, így az első tábla nem tartalmaz majd hibás hivatkozásokat.

Wizards (Varázslók)

[szerkesztés]

Gyakran egy varázsló több oldalnyi konfigurációs lapot mutat be egyetlen művelet érdekében, mely csak akkor következik be, ha a felhasználó rákattint a „Kész” gombra az utolsó oldalon. Ezekben az esetekben a természetes út a felhasználói interfész kódjának és az alkalmazás kódjának szétválasztása érdekében, hogy implementáljuk a varázslót parancs objektum használatával. A parancs objektum akkor jön létre, amikor a varázsló először megjelenik. A varázsló minden oldalában tárolja a GUI változásokat a parancs objektumban, tehát az objektum annyira előrehaladott (belakott), amennyire a felhasználó halad. A „Kész” gomb egyszerűen elindít egy „execute()” hívást. Ezen a ponton kezdődik a parancs osztály munkája, a parancsok sorozatának végrehajtása.

Terminológia

[szerkesztés]

A terminológiát arra használjuk, hogy leírjunk, a parancs tervezési minta implementációk nem következetesek és ennél fogva zavaró lehet. Ez a kétértelműség eredménye, a szinonimák és implementációk elfedhetik az eredeti mintát.

1. Kétértelműség

  • A „command” (parancs) kifejezés kétértelmű. Például a „move up” parancs. A „move up” utalhat egy egyszeri „single” parancsra, amelyet kétszer is végrehajthatunk, vagy utalhat két parancsra, melyek mindegyike ugyan azt tudja. Ha a korábbi parancs kétszer van hozzáadva a „vissza verembe”, akkor mindkét elem a veremben ugyan arra a parancs példányra utal/mutat. Ez megfelelő lehet abban az esetben, ha a parancsot vissza lehet vonni mindig ugyan abba az irányba. Mind a „Gang of Four” és a „Java” példa lejjebb használ interpretációt a parancs kifejezésben. Másfelől, ha az utóbbi parancsok hozzá vannak adva a „vissza verembe”, akkor a verem két különálló objektumra mutat. Ez megfelelő lehet abban az esetben, ha a veremben, példányonként tartalmazzák azon információkat, melyek engedélyezik a parancs visszavonását.
  • A(z) „Execute” (végrehajt) kifejezés is kétértelmű. Ez arra utal, hogy futtasd a kódot a parancs objektumok végrehajtási metódusai által. Habár a Windows Presentation Foundationben (WPF), egy parancs végrehajtottnak tekinthető, ha a parancsok végrehajtói metódusára hivatkozva van, de ez nem feltétlenül azt jelenti, hogy az alkalmazás kódja fut.

2. Szinonimák és homonimák

  • Kliens, forrás, felhasználó: a gomb, eszköztár gomb, vagy a menü elemek kattintása, a gyorsbillentyű gombok lenyomása a felhasználó által.
  • Parancs objektum, Irányított parancs objektum, Művelet objektum: Egy „singleton” objektum, mely birtokában van a gyorsbillentyű gomboknak, a képi gomboknak, a parancs szövegeknek, stb. kapcsolódnak a parancshoz. A parancs/művelet objektumok értesítik a megfelelő forrás/felhasználó objektumokat, amikor parancs/művelet objektumok elérhetősége megváltozik. Ez engedélyezi a gomboknak és a menü elemeknek, hogy inaktívvá váljanak (beszürküljenek) amikor egy parancs/művelet nem hajtható végre.
  • Vevő (fogadó), Cél objektum: az objektum amely a másolásért, beillesztésért, áthelyezésért felel. A vevő objektum birtokolja a metódust, amit parancsok végrehajtói metódusnak (command’s execute method) is neveznek. A vevő tipikusan a cél objektum is egyben. Például, ha egy vevő objektum egy kurzor és a metódus egy „moveUp” nevű metódus, akkor elvárható, hogy a kurzor a moveUp művelet célját képezi. Másfelől, ha a kód a parancs objektum által van definiálva, akkor a cél objektum teljesen más objektum lesz.
  • Parancs objektum, irányított esemény argumentumok, esemény objektumok: az objektum , amely el van választva a forrásból a parancs/műveleti objektumok, a cél objektumok, és a kód felé, amelyek végzik a munkát. Minden gomb kattintás, vagy gyorsbillentyű gomb eredménye egy új parancs/esemény objektum. Néhány implementáció több információt nyújt a parancs/esemény objektumnak. Egyéb implementációk parancs/esemény objektumokat tesznek egyéb esemény objektumba (Mint egy doboz, amiben egy még nagyobb doboz van)
  • Kezelő (Handler), ExecutedRoutedEventHandler, metódus, funkció (function): az aktuális kód amely már elvégzi a másolást, beillesztést, mozgatást, stb. Néhány implementációban a kezelő kód része a parancs/műveleti objektumnak. Más implementációkban a kód része a vevő/cél objektumoknak, és még további más implementációkban a kezelő kód külön van választva más egyéb objektumoktól.
  • Parancs menedzser, vissza menedzser, időzítő, sor (queue), diszpécser, felhasználó (invoker): egy objektum mely a parancs/esemény objektumot beleteszi egy „vissza verembe” (undo stack), vagy egy „helyrehoz verembe” (redo stack), vagy addig tartja a parancs/esemény objektumot, amíg egyéb objektumok késszé akarják tenni őket, vagy irányítják a parancs/esemény objektumot a megfelelő vevő/cél objektum felé vagy kezelő kódhoz.

3. Implementációk, melyek már túlmutatnak az eredeti parancs tervezési mintán

  • A Windows Presentation Foundation (WPF) bemutatta az irányított parancsokat, melyek kombinálják a parancs tervezési mintát az esemény feldolgozással. Ennek eredményeképpen a parancs objektum többé már nem tartalmaz hivatkozást a cél objektum felé, és az applikáció kódja felé sem. Ehelyett felhasználja a parancs objektumok meghívási parancsainak eredményeit egy úgynevezett „Executed Routed Event” – Végrehajtott irányított eseményt, amely a „Tunneling” vagy „Bubbling” események alatt fordulhat elő egy úgynevezett „Binding” objektumban, amely azonosítja a célt és az applikáció kódot, amely már el van végezve ezen a ponton.

Példák

[szerkesztés]

Tekintsünk egy egyszerű kapcsolót, legyen a neve „Switch”. Ebben a példában konfigurálunk egy "Switch"-et két paranccsal: ez a kettő nem más mint, „kapcsoljuk LE” a lámpát, és „kapcsoljuk FEL” a lámpát.

Az előnye ennek a különös parancs tervezési minta implementációnak az az, hogy a "Switch" használható bármilyen eszköz által, nem csak a lámpa által. A "switch" a következő példában le és felkapcsolhatja a lámpát, de a "Switch"-ek konstruktora képes elfogadni bármilyen más "Subclass" parancsát aminek két paramétere van. Például úgy is be tudjuk állítani a "Swith"-et, hogy az ki vagy be kapcsoljon egy motort.

A következő kód a „Parancs tervezési minta” egy implementációja C#-ban.

using System;
using System.Collections.Generic;
 
namespace CommandPattern
{
    public interface ICommand
    {
        void Execute();
    }
 
    /* The Invoker class */
    public class Switch
    {
        ICommand _closedCommand;
        ICommand _openedCommand;
 
        public Switch(ICommand closedCommand, ICommand openedCommand)
        {
            _closedCommand = closedCommand;
            _openedCommand = openedCommand;
 
        }
 
        //close the circuit/power on
        public void Close()
        {
            _closedCommand.Execute();
        }
 
        //open the circuit/power off
        public void Open()
        {
            _openedCommand.Execute();
        }
    }
 
    /* An interface that defines actions that the receiver can perform */
    public interface ISwitchable
    {
        void PowerOn();
        void PowerOff();
    }
 
    /* The Receiver class */
    public class Light : ISwitchable
    {
        public void PowerOn()
        {
            Console.WriteLine("The light is on");
        }
 
        public void PowerOff()
        {
            Console.WriteLine("The light is off");
        }
    }
 
    /* The Command for turning on the device - ConcreteCommand #1 */
    public class CloseSwitchCommand: ICommand
    {
        private ISwitchable _switchable;
 
        public CloseSwitchCommand(ISwitchable switchable)
        {
            _switchable = switchable;
        }
 
        public void Execute()
        {
            _switchable.PowerOn();
        }
    }
 
    /* The Command for turning off the device - ConcreteCommand #2 */
    public class OpenSwitchCommand : ICommand
    {
        private ISwitchable _switchable;
 
        public OpenSwitchCommand(ISwitchable switchable)
        {
            _switchable = switchable;
        }
 
        public void Execute()
        {
            _switchable.PowerOff();
        }
    }
 
    /* The test class or client */
    internal class Program
    {
        public static void Main(string[] args)
        {
            string arg = args.Length > 0 ? args[0].ToUpper() : null;
 
            ISwitchable lamp = new Light();
 
            //Pass reference to the lamp instance to each command
            ICommand switchClose = new CloseSwitchCommand(lamp);
            ICommand switchOpen = new OpenSwitchCommand(lamp);
 
 
            //Pass reference to instances of the Command objects to the switch
            Switch @switch = new Switch(switchClose, switchOpen);
 
 
            if (arg == "ON")
            {
                //Switch (the Invoker) will invoke Execute() (the Command) on the command object - _closedCommand.Execute();
                @switch.Close();
            }
            else if (arg == "OFF")
            {
                //Switch (the Invoker) will invoke the Execute() (the Command) on the command object - _openedCommand.Execute();
                @switch.Open();
            }
            else
            {
                Console.WriteLine("Argument \"ON\" or \"OFF\" is required.");
            }
        }
    }
}

Egy egyszerűbb példa:

/*IVSR: Command Pattern*/
using System;
using System.Collections;
using System.Linq;
 
namespace IVSR.Designpatterns.CommandPattern_demo
{
    #region ICommand Interface
    interface ICommand
    {
        string Name { get; set; }
        string Description { get; set; }
        void Run();
    }
    #endregion
 
    #region Command Invoker
    class CInvoker
    {
        // Array to hold list of commands
        private ArrayList listOfCommands = new ArrayList();
 
        //Default constructor to load commands
        public CInvoker()
        {
            LoadCommands();
        }
 
        //Loads the commands to arrylist
        private void LoadCommands()
        {
            listOfCommands.Add(new cmdOpen());
            listOfCommands.Add(new cmdClose());
            listOfCommands.Add(new cmdCreate());
            listOfCommands.Add(new cmdUpdate());
            listOfCommands.Add(new cmdRetrieve());
        }
 
        //Find command using foreach
        public ICommand GetCommand(string name)
        {
            foreach (var item in listOfCommands)
            {
                ICommand objCmd = (ICommand)item;
                if (objCmd.Name == name)
                {
                    return objCmd; //return the command found
                }
            }
            return null; //return if no commands are found
        }
    }
    #endregion
 
    #region Commands
 
    class cmdOpen : ICommand //command 1
    {
        private string _name = "open", _description = "opens a file";
        public string Name { get { return _name; } set { _name = value; } }
 
        public string Description { get { return _description; } set { _description = value; } }
 
        public void Run() { Console.WriteLine("running open command"); }
    }
 
    class cmdClose : ICommand //command 2
    {
        private string _name = "close", _description = "closes a file";
        public string Name { get { return _name; } set { _name = value; } }
        public string Description { get { return _description; } set { _description = value; } }
        public void Run() { Console.WriteLine("running close command"); }
    }
 
    class cmdCreate : ICommand //command 3
    {
        private string _name = "create", _description = "creates a file";
        public string Name { get { return _name; } set { _name = value; } }
        public string Description { get { return _description; } set { _description = value; } }
        public void Run() { Console.WriteLine("running create command"); }
    }
 
    class cmdUpdate : ICommand //Command 4
    {
        private string _name = "update", _description = "updates a file";
        public string Name { get { return _name; } set { _name = value; } }
        public string Description { get { return _description; } set { _description = value; } }
        public void Run() { Console.WriteLine("running update command"); }
    }
 
    class cmdRetrieve : ICommand //command 5
    {
        private string _name = "retrieve", _description = "retrieves a file";
        public string Name { get { return _name; } set { _name = value; } }
        public string Description { get { return _description; } set { _description = value; } }
        public void Run() { Console.WriteLine("running Retrieve command"); }
    }
    #endregion
 
    #region MAIN
    class Program
    {
        static void Main(string[] args)
        {
            //Command pattern example
            CInvoker cmdInvoker = new CInvoker();
            ICommand cmd1 = cmdInvoker.GetCommand("open");
            cmd1.Run();
            cmdInvoker.GetCommand("update").Run();
            //or
            new CInvoker().GetCommand("close").Run();
        }
    }
    #endregion
}
import java.util.List;
import java.util.ArrayList;
 
/* The Command interface */
public interface Command {
   void execute();
}
 
/* The Invoker class */
public class Switch {
   private List<Command> history = new ArrayList<Command>();
 
   public void storeAndExecute(Command cmd) {
      this.history.add(cmd); // optional
      cmd.execute();
   }
}
 
/* The Receiver class */
public class Light {
 
   public void turnOn() {
      System.out.println("The light is on");
   }
 
   public void turnOff() {
      System.out.println("The light is off");
   }
}
 
/* The Command for turning on the light - ConcreteCommand #1 */
public class FlipUpCommand implements Command {
   private Light theLight;
 
   public FlipUpCommand(Light light) {
      this.theLight = light;
   }
 
   public void execute(){
      theLight.turnOn();
   }
}
 
/* The Command for turning off the light - ConcreteCommand #2 */
public class FlipDownCommand implements Command {
   private Light theLight;
 
   public FlipDownCommand(Light light) {
      this.theLight = light;
   }
 
   public void execute() {
      theLight.turnOff();
   }
}
 
/* The test class or client */
public class PressSwitch {
   public static void main(String[] args){
      Light lamp = new Light();
      Command switchUp = new FlipUpCommand(lamp);
      Command switchDown = new FlipDownCommand(lamp);
 
      Switch mySwitch = new Switch();
 
      switch(args[0]) {
         case "ON":
            mySwitch.storeAndExecute(switchUp);
         break;
         case "OFF":
            mySwitch.storeAndExecute(switchDown);
         break;
         default:
            System.out.println("Argument \"ON\" or \"OFF\" is required.");
       }
   }
}

Python

[szerkesztés]
class Switch(object):
    """The INVOKER class"""
    @classmethod
    def execute(cls, command):
        command.execute()
 
class Command(object):
    """The COMMAND interface"""
    def __init__(self, obj):
        self._obj = obj
 
    def execute(self):
        raise NotImplemented
 
class TurnOnCommand(Command):
    """The COMMAND for turning on the light"""
    def execute(self):
        self._obj.turn_on()
 
class TurnOffCommand(Command):
    """The COMMAND for turning off the light"""
    def execute(self):
        self._obj.turn_off()
 
class Light(object):
    """The RECEIVER class"""
    def turn_on(self):
        print("The light is on")
 
    def turn_off(self):
        print("The light is off")
 
class LightSwitchClient(object):
    """The CLIENT class"""
    def __init__(self):
        self._lamp = Light()
        self._switch = Switch()
 
    def switch(self, cmd):
        cmd = cmd.strip().upper()
        if cmd == "ON":
            Switch.execute(TurnOnCommand(self._lamp))
        elif cmd == "OFF":
            Switch.execute(TurnOffCommand(self._lamp))
        else:
            print("Argument 'ON' or 'OFF' is required.")
 
# Execute if this file is run as a script and not imported as a module
if __name__ == "__main__":
    light_switch = LightSwitchClient()
    print("Switch ON test.")
    light_switch.switch("ON")
    print("Switch OFF test.")
    light_switch.switch("OFF")
    print("Invalid Command test.")
    light_switch.switch("****")
/* The Command interface */
trait Command {
   def execute()
}
 
/* The Invoker class */
class Switch {
   private var history: List[Command] = Nil
 
   def storeAndExecute(cmd: Command) {
      cmd.execute()
      this.history :+= cmd
   }
}
 
/* The Receiver class */
class Light {
   def turnOn() = println("The light is on")
   def turnOff() = println("The light is off")
}
 
/* The Command for turning on the light - ConcreteCommand #1 */
class FlipUpCommand(theLight: Light) extends Command {
   def execute() = theLight.turnOn()
}
 
/* The Command for turning off the light - ConcreteCommand #2 */
class FlipDownCommand(theLight: Light) extends Command {
   def execute() = theLight.turnOff()
}
 
/* The test class or client */
object PressSwitch {
   def main(args: Array[String]) {
      val lamp = new Light()
      val switchUp = new FlipUpCommand(lamp)
      val switchDown = new FlipDownCommand(lamp)
 
      val s = new Switch()
 
      try {
         args(0).toUpperCase match {
            case "ON" => s.storeAndExecute(switchUp)
            case "OFF" => s.storeAndExecute(switchDown)
            case _ => println("Argument \"ON\" or \"OFF\" is required.")
         }
      } catch {
         case e: Exception => println("Arguments required.")
      }
   }
}

JavaScript

[szerkesztés]
/* The Invoker function */
var Switch = function(){
    var _commands = [];
    this.storeAndExecute = function(command){
        _commands.push(command);
        command.execute();
    }
}
 
/* The Receiver function */
var Light = function(){
    this.turnOn = function(){ console.log ('turn on') };
    this.turnOff = function(){ console.log ('turn off') };
}
 
/* The Command for turning on the light - ConcreteCommand #1 */
var FlipUpCommand = function(light){
    this.execute = function() { light.turnOn() };
}
 
/* The Command for turning off the light - ConcreteCommand #2 */
var FlipDownCommand = function(light){
    this.execute = function() { light.turnOff() };
}
 
var light = new Light();
var switchUp = new FlipUpCommand(light);
var switchDown = new FlipDownCommand(light);
var s = new Switch();
 
s.storeAndExecute(switchUp);
s.storeAndExecute(switchDown);

Coffescript:
# The Invoker function
class Switch
   _commands = []
   storeAndExecute: (command) ->
     _commands.push(command)
     command.execute()
 
#  The Receiver function
class Light
  turnOn: ->
    console.log ('turn on')
  turnOff: ->
    console.log ('turn off')
 
# The Command for turning on the light - ConcreteCommand #1
class FlipUpCommand
 constructor: (@light) ->
 
 execute: ->
   @light.turnOn()
 
# The Command for turning off the light - ConcreteCommand #2
class FlipDownCommand
 constructor: (@light) ->
 
 execute: ->
   @light.turnOff()
 
light = new Light()
switchUp = new FlipUpCommand(light)
switchDown = new FlipDownCommand(light)
s = new Switch()
 
s.storeAndExecute(switchUp)
s.storeAndExecute(switchDown)

Fordítás

[szerkesztés]

Ez a szócikk részben vagy egészben a Command Pattern című angol Wikipédia-szócikk ezen változatának fordításán alapul. Az eredeti cikk szerkesztőit annak laptörténete sorolja fel. Ez a jelzés csupán a megfogalmazás eredetét és a szerzői jogokat jelzi, nem szolgál a cikkben szereplő információk forrásmegjelöléseként.