Видео курс Шаблоны проектирования

Основные темы, рассматриваемые на уроке:
00:31 1 Назначение паттерна (через метафору)
04:12 2 Пример (с диаграммой классов, и кодом C#)
17:46 3 Структура паттерна на языке UML
19:27 4 Структура паттерна на языке C#
22:23 5 Применимость Паттерна
25:05 6 Назначение паттерна
28:38 7 Использование паттерна

Видео урок расскажет о шаблоне проектирования Command (Команда), который позволяет представить запрос в виде объекта, позволяя клиенту конфигурировать запрос (задавая параметры для его обработки), ставить запросы в очередь, протоколировать запросы, а также поддерживать отмену операций.

Для просмотра полной версии видеокурса и получения доступа к дополнительным учебным материалам Вам необходимо оформить подписку
Оформить подписку

Паттерн Command

Название

Команда

Также известен как

Action (Действие), Transaction (Транзакция)

Классификация

По цели: поведенческий

По применимости: к объектам

Частота использования

Выше средней    -   1 2 3 4 5

Назначение

Паттерн Command – позволяет представить запрос в виде объекта, позволяя клиенту конфигурировать запрос (задавая параметры для его обработки), ставить запросы в очередь, протоколировать запросы, а также поддерживать отмену операций.

Введение

В качестве примера системы, в которой возможно использовать паттерн Command предлагается рассмотреть упрощенный программный калькулятор с четырьмя арифметическими операциями Add, Sub, Mul, Div и двумя операциями Undo и Redo. Структурная схема калькулятора показана на рисунке ниже.

В структуре калькулятора можно выделить UI (наборное поле с кнопками для цифр, знаков отмены и повтора операций, а также панель для отображения результата). В «корпусе» калькулятора расположены программные блоки, обеспечивающие реакцию калькулятора на поток внешних дискретных событий. Такими событиями являются нажатия кнопок наборного поля. 

Состав программных блоков калькулятора, следующий:

  • УУ - Устройство управления (ControlUnit). Оно организует всю работу калькулятора, выдавая в подходящий момент элементарные объекты-команды типа: Add, Sub, Mul, Div, Undo, Redo. При этом УУ сохраняет историю использования команд, а также может отменять и восстанавливать ранее выполненные команды.
  • АУ - Арифметическое устройство (ArithmeticUnit). После получения «сигнала» (одной из четырех команд Add, Sub, Mul, Div) на вход выполняет арифметическую операцию.
  • Команды - Add, Sub, Mul, Div. Специальные объекты-команды, которые УУ использует для управления АУ. Каждый объект-команда связан с АУ и умеет правильно им управлять.

Предлагается рассмотреть диаграмму классов на которой представлена модель калькулятора.

См. пример к главе: \014_Command\002_Command_Undo_Redo

Ниже представлен пример реализации калькулятора.

См. пример к главе: \014_Command\002_Command_Undo_Redo

Удобство такого построения калькулятора с использованием паттерна Command заключается в том, что оказывается легко добавлять новые команды, которые можно тонко конфигурировать. Также можно расширять вычислительные возможности самого калькулятора, например, логическим устройством или устройством для вычислений чисел с плавающей точкой (FPU). На рисунке ниже показана структурная схема калькулятора расширенного новой командой Pow и новым вычислительным устройством FPU.

Структура паттерна на языке UML

См. пример к главе: \014_Command\001_Command

Структура паттерна на языке C#

См. пример к главе: \014_Command\001_Command

Участники

  • Command - Команда:

Предоставляет интерфейс для выполнения операции.

  • ConcreteCommand - Конкретная команда:

Представляет собой объектно-ориентированное выражение соответствия между объектом класса Receiver и выполняемым им действием Action. Реализует операцию Execute которая вызывает соответствующие операции объекта класса Receiver.

  • Client - Клиент:

Создает объект класса ConcreteCommand и устанавливает его получателя - объект класса Receiver.

  • Invoker  - Инициатор:

Обращается к объекту-команде для выполнения запроса.

  • Receiver - Получатель:

Обладает функциональностью достаточной для выполнения определенного запроса. В роли получателя запроса может выступать любой класс.

Отношения между участниками

Отношения между классами

  • Класс Invoker связан связью отношения агрегации с абстрактным классом Command.
  • Класс ConcreteCommand связан связью отношения наследования с абстрактным классом Command и связью отношения ассоциации с конкретным классом Receiver.

Отношения между объектами

  • Клиент создает объект класса ConcreteCommand и устанавливает для него получателя (Receiver).
  • Объект класса Invoker сохраняет в списке историй команд объект класса ConcreteCommand и отправляет запрос на выполнение команды вызывая метод Execute на объекте-команде. Если требуется поддержка отмены (Undo) и повтора (Redo) операций, то объект ConcreteCommand перед выполнением тела метода Execute должен сохранить информацию о своем текущем состоянии, достаточную для выполнения отката (Undo).
  • Объект класса ConcreteCommand вызывает операции (Action) объекта-получателя (Receiver) для выполнения запроса.

На диаграмме взаимодействий между объектами представленной ниже видно, как объект-команда (Command) разрывает связь-отношение между инициатором (Invoker) и получателем (Receiver). 

Мотивация

Рассмотрим пример с использованием паттерна Command в приложении, с пользовательским интерфейсом представленном в виде меню. Заметим, что библиотека пользовательских элементов .Net не содержит информации о способах обработки информации при выборе определенных пунктов меню. Варианты и алгоритмы обработки задаются разработчиками приложений, использующих библиотеки .Net. Кроме того, часто необходимо организовать обработку действий пользователя в условиях, когда изначально неизвестно, какой объект является получателем запроса на обработку, и неясно, выполнение какой конкретной задачи запрошено. Например, такое может случиться при разработке в больших командах, где разные группы разработчиков отвечают за логику работы приложения и за интерфейс или когда на этапе проектирования, проектировщик библиотеки интерфейсов не владеет информацией о том, какие классы будут задействованы в приложении, и какие операции они будут выполнять. В таком случае, нужно каким-то образом инкапсулировать запрос и его параметры в определенном объекте и отделить объект интерфейса, инициирующий запрос, от объекта-исполнителя запроса.

Паттерн Command  позволяет преобразовать запрос в объект и таким образом сделать возможной его хранение и отправку другим объектам приложения, не раскрывая при этом сути запроса и конкретного адресата. Основой паттерна является абстрактный класс Command, который задает интерфейс для выполнения операций, например, в виде абстрактного метода Execute, а также имеет защищенный метод логирования выполненных операций LogExecution:

abstract class Command
{
    public abstract void Execute();

    protected void LogExecution(string text)
    {
        MainForm.MainFormInstance.Log(this.GetType().Name + " -> " + text);
    }
} 

Конкретные классы, отвечающие интерфейсу заданному классом Command должны хранить в себе получателя (адрес получателя), и реализовывать абстрактный метод Execute базового абстрактного класса. Таким образом, эти конкретные классы-команды формируют  связки «получатель-действие», подразумевая, что у получателя есть вся необходимая информация для выполнения действия, на которое получен запрос.

С помощью объектов унаследованных от абстрактного класса Command легко реализовать меню, адаптируя абстрактный класс Command к имеющемуся в библиотеке .Net классу ToolStripMenuItem:

class MyMenuItem : ToolStripMenuItem
{
    public Command MenuCommand { get; set; }

    public MyMenuItem(string text, Command command)
        : base(text)
    {
        MenuCommand = command;
    }
        
    protected override void OnClick(EventArgs e)
    {
        base.OnClick(e);
        if (MenuCommand != null)
            MenuCommand.Execute();
    }
}

Меню приложения будет состоять из объектов типа MyMenuItem, каждый из которых будет сконфигурирован экземпляром одного из конкретных подклассов класса Command: OpenCommand, CopyCommand, CutCommand, SelectAllTextCommand, PasteCommand, CloseCommand, - или набором из таких объектов типа MacroCommand, которые содержат ссылку на получателя запроса. При выборе некоторого пункта меню связанный с ним объект класса  MyMenuItem вызовет метод Execute, реализованный в классе инкапсулированного объекта-команды, который и выполнить необходимую операцию. Заметим, что объекты класса MyMenuItem, работают с интерфейсом заданным абстрактным классом Command и не содержат информацию о том, какую именно операцию они выполняют и каким именно объектом подкласса класса Command они оперируют.  Классы конкретных команд содержат информацию о получателе запросов – объекте типа Document, доступному через статическое свойство CurrentDocument класса MainForm, и вызывают одну или целый набор операций этого получателя.

Таким образом, паттерн Command отделяет объект инициирующий операцию от объекта, который знает, как ее выполнить, что придает гибкости проектному решению при проектировании пользовательского интерфейса.  Используя паттерн Command легко организовать динамическую подмену команд при реализации контекстно-зависимых меню и организацию сценариев (сложных макрокоманд) из более простых команд.

Применимость паттерна

Паттерн Command рекомендуется использовать, когда:

  • необходимо параметризировать объекты-команды выполняемым действием, например, кнопки или элементы меню, а также callback методы, благодаря объектно-ориентированному представлению объектов-запросов. Заметим, что такая параметризация не является параметризацией типов в чистом виде, в языке С#  - она представлена обобщениями, поддерживаемыми платформой .Net, хотя в этом случае обобщения (generics), также могут быть использованы, например, при параметризации сложными  объектами классов Action<T> или Func<T, TResult >, где указатель на место заполнения типа T будет примать тип Command.

 

  • необходимо организовать разнесенное во времени добавление запросов в очередь и их выполнение. При этом срок жизни запроса в очереди и объекта-запроса класса, наследуемого от класса Command, могут быть независимыми и вообще существовать в различных процессах.

 

  • необходимо обеспечить отмену операций. Для этого нужно в интерфейсе абстрактного класса Command объявить метод Unxecute, вызов которого позволял бы осуществить откат действий, произведенных методом Execute. Возможно, для реализации такого действия, понадобиться предварительно сохранить необходимую информацию при выполнении метода Execute и сохранять последовательность действий Execute/Unxecute в списке истории внутри объекта класса, чтобы при необходимости можно было выполнить откат состояния объекта-получателя, выполняя обратные операции. Чтобы предоставить команде доступ к этой информации, не раскрывая внутреннее состояние объектов-носителей информации, можно воспользоваться паттерном Memento.

 

  • необходимо протоколировать (логировать) изменения. Такая возможность может понадобиться для отслеживания времени и инициатора изменений данных, а также для восстановления состояния  объектов после сбоя. В таком случае, необходимо дополнить интерфейс абстрактного класса Command методами сохранения и загрузки протокола изменений из внешнего источника. Тогда после сбоя можно будет загрузить протокол изменений и повторно выполнить последовательность операций при помощи методов Execute/Unxecute.

 

  • необходимо создать систему на основе транзакций т.е. с прозрачной структурой высокоуровневых операций на основе примитивных. Транзакция – объектно-ориентированное представление группы логически объединённых последовательных операций по работе с данными, обрабатываемое или отменяемое целиком. Паттерн Command позволяет проектировать системы с учетом транзакционного подхода благодаря наличию у всех низкоуровневых команд и высокоуровневых транзакций общего интерфейса, что позволяет легко добавлять в систему новые команды и организовывать на их основе новые транзакции, а также работать с ними единообразно.

 

Результаты

Паттерн Command:

  • отделяет объект инициатор операции от объекта, имеющего информацию о том, как ее выполнить.  Достигается благодаря адаптации (см. паттерн Adapter) абстрактного класса или интерфейса Command к классу объекта инициатора. При этом сам объект-инициатор может абсолютно не  владеть информацией об объекте-исполнителе и о сути выполняемой операции.

 

  • позволяет оперировать объектами при маршрутизации и обработке запросов. Конкретные классы команд инкапсулируют в себе адрес обработчика операции и высокоуровневую часть алгоритма обработки.

 

  • предоставляет возможность структурирования команд, путем компоновки сложных операций, например MacroCommand, из более простых составляющих.  При этом объект сложной операции имеет тот же интерфейс, что и все его составные части, т.е. реализует метод Execute базового абстрактного класса Command (см. паттерн Composite).

 

  • дает возможность легко добавлять новые типы команд, поскольку никакие существующие классы, при этом изменять не нужно.

 

 

Реализация

Полезные приемы реализации паттерна Command:

  • широкий спектр возможностей организации команд по уровню сложности. С одной стороны команды могут универсальными, «умными» и выполнять широкий круг обязанностей, совмещая в себе большую информированность об адресате и набор возможностей для доступа к его методам, т.е. уметь динамически находить получателя, а также определять и вызывать его методы, для выполнения нужных операций (например, с помощью механизмов рефлексии, поддерживаемых платформой .Net). С другой стороны, команды могут принимать и вырожденные формы, т.е. полностью или частично не владеть информацией об адресате (например, если он точно неизвестен или подходящего получателя не существует) и выполнять необходимые операции самостоятельно, без привлечения получателей, используя автономные методы. При этом команды, могут  даже не владеть информацией о том, что в точности они выполняют, например, создавать объекты, не различая их по типам (кнопки это, меню, окна или объекты пользовательских классов). Между этими двумя крайними вариантами находятся команды, обладающие достаточной информацией для передачи запроса получателю, и использовать заранее прописанные в коде методы объектов-получателей для выполнения необходимых операций.

 

  • поддержка многоуровневой отмены и повтора операций.  Для реализации такой возможности понадобится реализовать хранение истории изменений с дополнительной информацией об:

 

  • адресе объекта-получателя класса Receiver, который выполняет операции по запросу;
  • аргументы и другие параметры используемые при вызове методов класса объекта-получателя;
  • исходном состоянии объекта-получателя Receiver, т.е. значения его полей, которые могли измениться в результате выполнения команды.

 

Также необходимым условием является предоставление доступа объектом Receiver к методам, позволяющим команде вернуть этот объект в исходное состояние. Обратим внимание, что если в результате выполнения команды меняется состояние не только объекта-получателя, но и самой команды, то объект команды также необходимо хранить в истории изменений, копируя его после выполнения, так как он может быть изменен при следующем вызове, и тогда выполнить откат действий корректно не удастся. Если в результате выполнения команды ее состояние не изменяется (не изменяются атрибуты объекта класса ConcreteCommand), то и хранить копию объекта не обязательно, достаточно хранения ссылки на объект-команду. Команды, которые изменяются при выполнении и нуждаются в хранении копий-клонов в списке истории ведут себя подобно прототипам (см. паттерн Prototype).

 

  • гарантирование целостности и неизменности объектов в процессе хранения истории изменений. Чтобы гарантировать, что объекты будут полностью восстановлены в процессе отката или повтора команд и буду защищены от несанкционированного изменения можно воспользоваться паттерном Memento. Это позволит команде получить доступ объекта-команды к информации необходимой для произведения операций отмены/повтора, без изменения состояния хранимого объекта, и без раскрытия его внутренней структуры.

 

  • применение шаблонов. Для однотипных команд, которые не поддерживают операции отмены/повтора (Undo/Redo) можно воспользоваться подходом создания команды-шаблона – объекта типа SimpleCommand : Command на основе делегатов типов Action, Action<T>, Func<T, TResult>, или delegate, где метод Execute, будет вызывать необходимые callback методы. Такой подход позволяет сократить количество подклассов класса Command. Подробнее использование шаблонных команд рассматривается в примере ниже.

 

Пример кода

Рассмотрим детальнее конкретные классы команд из раздела Мотивация. Класс OpenCommand, реализует команду открытия документа выбранного пользователем в приложении, используя метод AddDocument класса MainForm и реализуя абстрактный метод Execute родительского абстрактного класса Command:

    class OpenCommand : Command
    {
        public override void Execute()
        {
            var filename = AskUser();
            if (!string.IsNullOrEmpty(filename))
            {
                var doc = new Document();
                doc.Open(filename);
                LogExecution(" opened");
                MainForm.MainFormInstance.AddDocument(doc);
            }
            else
            { LogExecution(" opening cancelled"); }
        }
        string AskUser()
        {
            LogExecution("Asking user.");
            var dlg = new OpenFileDialog();
            dlg.InitialDirectory = Application.StartupPath;
            if (dlg.ShowDialog() == DialogResult.OK)
            {
                return dlg.FileName;
            }
            else
                return string.Empty;
        }
    }

    public partial class MainForm : Form
    {
        internal static Document CurrentDocument { get; set; }
        internal static MainForm MainFormInstance { get; private set; }

        public MainForm()
        {
            InitializeComponent();
            toolStripMenuItem1.DropDownItems.AddRange(new ToolStripItem[] { 
                new MyMenuItem("Open",new OpenCommand()),
                new MyMenuItem("Close",new CloseCommand()),
                new ToolStripSeparator(),
                new MyMenuItem("Cut",new CutCommand()),
                new MyMenuItem("Copy",new CopyCommand()),
                new MyMenuItem("Paste",new PasteCommand()),
                new ToolStripSeparator(),
                new MyMenuItem("MacroCopy",new MacroCommand(new OpenCommand(),
                                                            new SelectAllTextCommand(),
                                                            new CopyCommand(),
                                                            new CloseCommand()))
        });
            MainFormInstance = this;
        }

      
        private void AddMenuItemWithTemplateCommands()
        {
            toolStripMenuItem2 = new System.Windows.Forms.ToolStripMenuItem();

            this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
            toolStripMenuItem2});

            toolStripMenuItem2.Name = "toolStripMenuItem2";
            toolStripMenuItem2.Size = new System.Drawing.Size(106, 20);
            toolStripMenuItem2.Text = "DocumentTemplate Menu";

            toolStripMenuItem2.DropDownItems.AddRange(new ToolStripItem[] { 
                new MyMenuItem("Cut",new SimpleCommand(CurrentDocument.Cut,"cut")),
                new MyMenuItem("Copy",new SimpleCommand(CurrentDocument.Copy, "copy")),
                new MyMenuItem("Paste",new SimpleCommand(CurrentDocument.Paste,"paste")),             
            });
        } 

 public void AddDocument(Document document)
        {
            document.MdiParent = this;
            document.Show();

            AddMenuItemWithTemplateCommands();
        }

        public void Log(string logString)
        {
            logLabel.Text += Environment.NewLine + logString;
        }
    }

Классы CloseCommand, CopyCommand, CutCommand, PasteCommand, SelectAllTextCommand и MacroCommand аналогично реализуют соответственно закрытие документа, копирование и вырезание выделенного фрагмента текста, вставку текста из буфера обмена, выделение всего текстового содержимого в документе и заданную в коде приложения последовательность команд.

    class CloseCommand : Command
    {
        public override void Execute()
        {
            if (MainForm.CurrentDocument != null)
            {
                LogExecution("close");
                MainForm.CurrentDocument.Close();
            }
        }
    }

    class CopyCommand : Command
    {
        public override void Execute()
        {
            if (MainForm.CurrentDocument != null)
            {
LogExecution("copy text: " + MainForm.CurrentDocument.DocumentContent.SelectedText);  MainForm.CurrentDocument.Copy();
            }
        }
    }

    class CutCommand : Command
    {
        public override void Execute()
        {
            if (MainForm.CurrentDocument != null)
            {
LogExecution("cut text: " +      MainForm.CurrentDocument.DocumentContent.SelectedText);
MainForm.CurrentDocument.Cut();
            }
        }
    }

    class PasteCommand : Command
    {

        public override void Execute()
        {

            if (MainForm.CurrentDocument != null)
            {
                LogExecution("paste text: " + Clipboard.GetText());
                MainForm.CurrentDocument.Paste();
            }
        }
    }

    class SelectAllTextCommand : Command
    {
        public override void Execute()
        {
            if (MainForm.CurrentDocument != null)
            {
                LogExecution(MainForm.CurrentDocument.Text + "select all text");
                MainForm.CurrentDocument.DocumentContent.SelectAll();
            }
        }
    }

    class MacroCommand : Command
    {
        public readonly List<Command> Commands = new List<Command>();

        public MacroCommand(params Command[] commands)
        {
            Commands.AddRange(commands);
        }
      
        public override void Execute()
        {
            foreach (var c in Commands)
                c.Execute();
        }
    }

Заметим, что класс MacroCommand не содержит ссылок на объекты-исполнители запросов, т.к. они инкапсулированы в объектах типа Command, набор которых хранит в себе данный класс.

    class SimpleCommand : Command
    {
        Action action;
        string actionKeyword;

        public SimpleCommand(Action action, string actionKeyword)
        {
            this.action = action;
            this.actionKeyword = actionKeyword;
        }

        public override void Execute()
        {
            if (action != null)
            {
LogExecution(actionKeyword + " text: " +   MainForm.CurrentDocument.DocumentContent.SelectedText);
             action.Invoke();
            }
        }
    }

В классе SimpleCommand каждая связка «объект-исполнитель – действие» задается делегатом типа Action при вызове конструктора класса, а объекты класса SimpleCommand оперируют только ссылкой на необходимый callback метод, который передан с делегатом класса Action.

 

 

Известные применения паттерна в .Net

System.Data.Odbc.OdbcCommand

http://msdn.microsoft.com/ru-ru/library/system.data.odbc.odbccommand(v=vs.110).aspx

System.Data.OleDb.OleDbCommand

http://msdn.microsoft.com/ru-ru/library/system.data.oledb.oledbcommand(v=vs.110).aspx

System.Data.OracleClient.OracleCommand

http://msdn.microsoft.com/en-us/library/system.data.oracleclient.oraclecommand(v=vs.110).aspx

System.Data.SqlClient.SqlCommand

http://msdn.microsoft.com/ru-ru/library/system.data.sqlclient.sqlcommand(v=vs.100).aspx

System.Windows.Input.ICommand

http://msdn.microsoft.com/en-us/library/system.windows.input.icommand(v=vs.110).aspx

System.Windows.Input.ComponentCommands

http://msdn.microsoft.com/ru-ru/library/system.windows.input.componentcommands(v=vs.110).aspx

System.Windows.Input.MediaCommands

http://msdn.microsoft.com/ru-ru/library/system.windows.input.mediacommands(v=vs.110).aspx

System.Windows.Input.NavigationCommands

http://msdn.microsoft.com/ru-ru/library/system.windows.input.navigationcommands(v=vs.110).aspx

System.Windows.Input.RoutedCommand

http://msdn.microsoft.com/ru-ru/library/system.windows.input.routedcommand(v=vs.110).aspx

System.Windows.SystemCommands

http://msdn.microsoft.com/ru-ru/library/system.windows.systemcommands(v=vs.110).aspx

System.Workflow.ComponentModel.Design.WorkflowMenuCommands

http://msdn.microsoft.com/ru-ru/library/system.workflow.componentmodel.design.workflowmenucommands(v=vs.110).aspx

© 2017 ITVDN, все права защищены