Команда (шаблон проєктування)
Команда (англ. Command) — шаблон проєктування, відноситься до класу шаблонів поведінки. Також відомий як Дія (англ. Action), Транзакція (англ. Transaction).
Призначення
Інкапсулює запит у формі об'єкта, дозволяючи тим самим задавати параметри клієнтів для обробки відповідних запитів, ставити запити у чергу або протоколювати їх, а також підтримувати скасовування операцій.
Мотивація
Створення структури, в якій клас-відправник і клас-отримувач не залежать один від одного напряму. Організація зворотного виклику до класу, який містить у собі клас-відправник.
Застосовність
Слід використовувати шаблон Команда коли:
- треба параметризувати об'єкти дією. У процедурній мові таку параметризацію можна виразити за допомогою функції зворотнього виклику, тобто такою функцією, яка реєструється, щоби бути викликаною пізніше. Команди є об'єктно-орієнтованою альтернативою функціям зворотньогоо виклику;
- визначати, ставити у чергу та виконувати запити у різний час. Термін життя об'єкта Команда не обов'язково залежить від терміну життя початкового запиту. Якщо отримувача вдається реалізувати таким чином, щоб він не залежав від адресного простору, то об'єкт-команду можна передати іншому процесу, який займеться його виконанням;
- потрібна підтримка скасовування операцій. Операція Execute об'єкта Команда може зберегти стан, що необхідний для скасування дій, виконаних Командою. У цьому разі у інтерфейсі класу Command повинна бути додаткова операція Unexecute, котра скасовує дії, виконанні попереднім викликом операції Execute. Виконані команди зберігаються у списку історії. Для реалізації довільної кількості рівней скасування та повтору команд треба обходити цей список відповідно в зворотньому та прямому напрямках, викликаючи під час відвідування кожного елементу операцію Unexecute або Execute;
- підтримати протоколювання змін, щоб їх можна було виконати повторно після аварійної зупинки системи. Доповнивши інтерфейс класу Command операціями зберігання та завантаження, можна вести протокол змін у внутрішній пам'яті. Для відновлення після збою треба буде завантажити збереженні команди з диску та повторно виконати їх за допомогою операції Execute;
- треба структурувати систему на основі високорівневих операцій, що побудовані з примітивних. Така структура є типовою для інформаційних систем, що підтримують транзакції. Транзакція інкапсулює множину змін даних. Шаблон Команда дозволяє моделювати транзакції. В усіх команд є спільний інтерфейс, що надає можливість працювати однаково з будь-якими транзакціями. За допомогою цього шаблону можна легко додавати у систему нові види транзакцій.
Структура
- Command — команда:
- оголошує інтерфейс для виконання операції;
- ConcreteCommand — конкретна команда:
- визначає зв'язок між об'єктом-отримувачем Receiver та дією;
- реалізує операцію Execute шляхом виклику відповідних операцій об'єкта Receiver;
- Client — клієнт:
- створює об'єкт класу ConcreteCommand та встановлює його отримувача;
- Invoker — викликач:
- звертається до команди щоб та виконала запит;
- Receiver — отримувач:
- має у своєму розпорядженні усю інформацію про способи виконання операцій, необхідних для задоволення запиту. У ролі отримувача може виступати будь-який клас.
Відносини
- клієнт створює об'єкт ConcreteCommand та встановлює для нього отримувача;
- викликач Invoker зберігає об'єкт ConcreteCommand;
- викликач надсилає запит, викликаючи операцію команди Execute. Якщо підтримується скасування виконаних дій, то ConcreteCommand перед викликом Execute зберігає інформацію про стан, достатню для виконання скасування;
- об'єкт ConcreteCommand викликає операції отримувача для виконання запиту
На діаграмі видно, як Command розриває зв'язок між викликачем та отримувачем (а також запитом, що повинен бути виконаний останнім).
Переваги
- Відокремлює класи, які викликають операцію від об'єкта, який вміє виконувати операцію
- Дозволяє створювати послідовність команд за допомогою системи черги
- Розширення для додавання нової команди є простими і можуть бути виконані без зміни існуючого коду
- Ви також можете визначити систему відкату з командним шаблоном, наприклад, у прикладі майстра, ми можемо написати метод відкату
Недоліки
- Збільшення кількості класів для кожної окремої команди
Реалізація
C++
#include <iostream>
#include <vector>
using namespace std;
struct Command // Основа патерну
{
virtual void execute() = 0;
};
// Уточнення
struct Hello : public Command
{
virtual void execute() { cout << " Hello "; };
};
struct World : public Command
{
virtual void execute() { cout << " World!"; };
};
struct IAm : public Command
{
virtual void execute()
{
cout << " I am the command pattern!";
}
};
// Місце, де застосовують команду
class Macro
{
private:
vector< Command*> commands;
public:
void add(Command* c) { commands.push_back(c); }
void run()
{
vector< Command*> ::iterator it = commands.begin();
while (it != commands.end()) (*it++)->execute();
}
};
void main()
{
Macro macro;
macro.add(new Hello);
macro.add(new World);
macro.add(new IAm);
macro.run();
}
C#
using System;
using System.Linq;
using System.Collections.Generic;
namespace Command
{
// основа патерну
public interface ICommand
{
string Name { get; }
void Execute();
void UnExecute();
}
// різноманітні команди
class ChangeColorCommand : ICommand
{
// стан об'єкта до і після застосування команди
private readonly ConsoleColor newColor;
private readonly ConsoleColor prevColor;
public ChangeColorCommand(ConsoleColor newColor)
{
this.newColor = newColor;
this.prevColor = Console.ForegroundColor;
}
public string Name => $"Change foreground color to {newColor}";
public void Execute()
{
Console.ForegroundColor = newColor;
}
public void UnExecute()
{
Console.ForegroundColor = prevColor;
}
}
class ChangeBackColorCommand : ICommand
{
private readonly ConsoleColor newColor;
private readonly ConsoleColor prevColor;
public ChangeBackColorCommand(ConsoleColor newColor)
{
this.newColor = newColor;
this.prevColor = Console.BackgroundColor;
}
public string Name => $"Change background color to {newColor}";
public void Execute()
{
Console.BackgroundColor = newColor;
}
public void UnExecute()
{
Console.BackgroundColor = prevColor;
}
}
// Місце, де застосовують команди
class UndoRedoManager
{
// історії команд
Stack<ICommand> undoStack;
Stack<ICommand> redoStack;
public event EventHandler StateChanged;
public UndoRedoManager()
{
undoStack = new Stack<ICommand>();
redoStack = new Stack<ICommand>();
}
public bool CanUndo => undoStack.Count > 0;
public bool CanRedo => redoStack.Count > 0;
public void Undo()
{
if (CanUndo)
{
ICommand command = undoStack.Pop();
command.UnExecute();
redoStack.Push(command);
StateChanged?.Invoke(this, EventArgs.Empty);
}
}
public void Redo()
{
if (CanRedo)
{
ICommand command = redoStack.Pop();
command.Execute();
undoStack.Push(command);
StateChanged?.Invoke(this, EventArgs.Empty);
}
}
// усі команди виконуються через метод Execute
public void Execute(ICommand command)
{
command.Execute();
undoStack.Push(command);
redoStack.Clear();
StateChanged?.Invoke(this, EventArgs.Empty);
}
public IEnumerable<string> UndoItems => undoStack.Select(c => c.Name);
public IEnumerable<string> RedoItems => redoStack.Select(c => c.Name);
public void Undo(int count)
{
for (int i = 0; i < count; ++i) Undo();
}
public void Redo(int count)
{
for (int i = 0; i < count; ++i) Redo();
}
}
class Program
{
// напишемо фасад для легшого керування системою
class Menu
{
// FIELDS
UndoRedoManager undoRedoManager;
bool exit;
// CONSTRUCTORS
public Menu()
{
undoRedoManager = new UndoRedoManager();
}
// METHODS
public void Run()
{
while (!exit)
{
ShowMenuItems();
int userChoice = GetInput();
Perform(userChoice);
}
}
private void ShowMenuItems()
{
Console.Clear();
Console.WriteLine("Choose option");
Console.WriteLine();
Console.WriteLine("0 - Get undo commands list");
Console.WriteLine("1 - Get redo commands list");
Console.WriteLine("2 - Change foreground color");
Console.WriteLine("3 - Change background color");
Console.WriteLine("4 - Undo");
Console.WriteLine("5 - Redo");
Console.WriteLine("6 - Exit");
Console.WriteLine();
}
private int GetInput()
{
// get user choice
int userChoice;
do
{
Console.WriteLine("Your input:");
} while (!int.TryParse(Console.ReadLine(), out userChoice));
return userChoice;
}
private void Perform(int userChoice)
{
switch (userChoice)
{
case 0: GetUndoCommandList(); break;
case 1: GetRedoCommandList(); break;
case 2: ChangeForegroundColor(); break;
case 3: ChangeBackgroundColor(); break;
case 4: Undo(); break;
case 5: Redo(); break;
case 6: Exit(); break;
default: Console.WriteLine("Wrong choice"); break;
}
Console.WriteLine("Press enter");
Console.ReadLine();
}
// ACTIONS
private void GetUndoCommandList()
{
Console.WriteLine("Undo list:");
foreach(string commandName in undoRedoManager.UndoItems)
{
Console.WriteLine(commandName);
}
}
private void GetRedoCommandList()
{
Console.WriteLine("Redo list:");
foreach (string commandName in undoRedoManager.RedoItems)
{
Console.WriteLine(commandName);
}
}
private void ChangeForegroundColor()
{
// get user input
ConsoleColor newForegroundColor;
string color = string.Empty;
do
{
Console.WriteLine("Write new color");
color = Console.ReadLine();
} while (!Enum.TryParse(color, out newForegroundColor));
// execute command
undoRedoManager.Execute(new ChangeColorCommand(newForegroundColor));
}
private void ChangeBackgroundColor()
{
// get user input
ConsoleColor newBackgroundColor;
string color = string.Empty;
do
{
Console.WriteLine("Write new color");
color = Console.ReadLine();
} while (!Enum.TryParse(color, out newBackgroundColor));
// execute command
undoRedoManager.Execute(new ChangeBackColorCommand(newBackgroundColor));
}
private void Undo()
{
undoRedoManager.Undo();
}
private void Redo()
{
undoRedoManager.Redo();
}
private void Exit()
{
exit = true;
}
}
static void Main(string[] args)
{
new Menu().Run();
}
}
}
Swift[1]
protocol DoorCommand {
func execute() -> String
}
class OpenCommand : DoorCommand {
let doors:String
required init(doors: String) {
self.doors = doors
}
func execute() -> String {
return "Opened \(doors)"
}
}
class CloseCommand : DoorCommand {
let doors:String
required init(doors: String) {
self.doors = doors
}
func execute() -> String {
return "Closed \(doors)"
}
}
class HAL9000DoorsOperations {
let openCommand: DoorCommand
let closeCommand: DoorCommand
init(doors: String) {
self.openCommand = OpenCommand(doors:doors)
self.closeCommand = CloseCommand(doors:doors)
}
func close() -> String {
return closeCommand.execute()
}
func open() -> String {
return openCommand.execute()
}
}
let podBayDoors = "Pod Bay Doors"
let doorModule = HAL9000DoorsOperations(doors:podBayDoors)
doorModule.open()
doorModule.close()
Посилання
Література
Алан Шаллоуей, Джеймс Р. Тротт. Шаблоны проектирования. Новый подход к объектно-ориентированному анализу и проектированию = Design Patterns Explained: A New Perspective on Object-Oriented Design. — М. : «Вильямс», 2002. — 288 с. — ISBN 0-201-71594-5.