Команда (шаблон проєктування)

Команда (англ. Command) — шаблон проєктування, відноситься до класу шаблонів поведінки. Також відомий як Дія (англ. Action), Транзакція (англ. Transaction).

Призначення

Інкапсулює запит у формі об'єкта, дозволяючи тим самим задавати параметри клієнтів для обробки відповідних запитів, ставити запити у чергу або протоколювати їх, а також підтримувати скасовування операцій.

Мотивація

Створення структури, в якій клас-відправник і клас-отримувач не залежать один від одного напряму. Організація зворотного виклику до класу, який містить у собі клас-відправник.

Застосовність

Слід використовувати шаблон Команда коли:

  • треба параметризувати об'єкти дією. У процедурній мові таку параметризацію можна виразити за допомогою функції зворотнього виклику, тобто такою функцією, яка реєструється, щоби бути викликаною пізніше. Команди є об'єктно-орієнтованою альтернативою функціям зворотньогоо виклику;
  • визначати, ставити у чергу та виконувати запити у різний час. Термін життя об'єкта Команда не обов'язково залежить від терміну життя початкового запиту. Якщо отримувача вдається реалізувати таким чином, щоб він не залежав від адресного простору, то об'єкт-команду можна передати іншому процесу, який займеться його виконанням;
  • потрібна підтримка скасовування операцій. Операція Execute об'єкта Команда може зберегти стан, що необхідний для скасування дій, виконаних Командою. У цьому разі у інтерфейсі класу Command повинна бути додаткова операція Unexecute, котра скасовує дії, виконанні попереднім викликом операції Execute. Виконані команди зберігаються у списку історії. Для реалізації довільної кількості рівней скасування та повтору команд треба обходити цей список відповідно в зворотньому та прямому напрямках, викликаючи під час відвідування кожного елементу операцію Unexecute або Execute;
  • підтримати протоколювання змін, щоб їх можна було виконати повторно після аварійної зупинки системи. Доповнивши інтерфейс класу Command операціями зберігання та завантаження, можна вести протокол змін у внутрішній пам'яті. Для відновлення після збою треба буде завантажити збереженні команди з диску та повторно виконати їх за допомогою операції Execute;
  • треба структурувати систему на основі високорівневих операцій, що побудовані з примітивних. Така структура є типовою для інформаційних систем, що підтримують транзакції. Транзакція інкапсулює множину змін даних. Шаблон Команда дозволяє моделювати транзакції. В усіх команд є спільний інтерфейс, що надає можливість працювати однаково з будь-якими транзакціями. За допомогою цього шаблону можна легко додавати у систему нові види транзакцій.

Структура

UML діаграма, що описує структуру шаблону проєктування Команда
  • Command — команда:
    • оголошує інтерфейс для виконання операції;
  • ConcreteCommand — конкретна команда:
    • визначає зв'язок між об'єктом-отримувачем Receiver та дією;
    • реалізує операцію Execute шляхом виклику відповідних операцій об'єкта Receiver;
  • Client — клієнт:
    • створює об'єкт класу ConcreteCommand та встановлює його отримувача;
  • Invoker — викликач:
    • звертається до команди щоб та виконала запит;
  • Receiver — отримувач:
    • має у своєму розпорядженні усю інформацію про способи виконання операцій, необхідних для задоволення запиту. У ролі отримувача може виступати будь-який клас.

Відносини

UML діаграма, що описує взаємовідносини поміж об'єктів шаблону проєктування Команда
  • клієнт створює об'єкт 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]

Приклад реалізації на мові Swift
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()

Посилання

  1. Design Patterns implemented in Swift 2. Архів оригіналу за 30 Січня 2016. Процитовано 10 Березня 2016.

Джерела

Prefix: a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9

Portal di Ensiklopedia Dunia

Kembali kehalaman sebelumnya