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.