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.

C# szerkesztés

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
}

JAVA szerkesztés

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("****")

Scala szerkesztés

/* 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.