понедельник, 8 августа 2011 г.

Design Pattern - Command, Decorator

Когда я только начинал эту статью, то планировалось, что она будет посвящена рассмотрению только Command Pattern-а, но по ходу ейо совершенствования и доработки статья разрослась и в нейо вошло рассмотрение еще двух паттернов, это Singleton Pattern и Decorator Pattern. Поэтому, статья получилась весьма обширная, я разделил ейо на блоки в которых рассмотрены дополнительные шаблоны, их реализация и примеры (в том числе и на Java). Но основной частью все равно является рассмотрение работы Command Pattern-а с интеграцией его в MVC.

Введение

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

Рисунок 1. Схематическое представление Command Pattern

Реализация команд проста и интуитивно понятна. Они указывают объекту на действие (набор действий).

Command Pattern один из самых распространенных в мире шаблонов программирования. Его применение можно найти в командах GUI (File-New, File-Save,Edit Mesh-Merge, Norma-Flip Normals, Wizards ...), серверных framework-ах и взаимодействиях с базами данных (Transaction, Connect to BD, Test Conntection, Disconnect ...), Actions - Macro Recording  (как Actions в Photoshop), а также как множественные "Отмены" (Undo List). Но это еще не весь спектр применения Command Pattern, его также можно использовать в игровых механиках, к примеру командовать "юниту" атаковать цель или перемещаться в выбранное место на карте, при этом команда будет действовать для любого типа "юнита" если она делегирует действие на сам "юнит", т.е действия "движение" или "атака" заранее определены в самом объекте (см. пример в разделе Control Unit with Command Pattern, ниже)

Небольшое отступление.
Совершенно не важно для какого языка программирования применяется тот или иной Pattern, их архитектура устроена так, что их можно использовать для любого объектно-ориентированного языка, и главное это понимать суть работы и получаемый результат выбранного шаблона.

Ну что же начнем! : )

В этом примере я буду использовать уже рассмотренный ранее "паттерн" Model-View-Controller и на его основе реализую Command Pattern, а также Decorator и Singleton Patterns. Также хочу сказать, что это простейший пример показывающий только основные принципы работы с данным шаблоном. В разработке я буду пользоваться Flash Develop 4 Beta.

0. MVC

Рисунок 2. Представление MVC Pattern

На рисунке выше представлена основа MVC "паттерна" - три класса Model (обрабатывает и хранит данные), Controller (управляет данными), View (отображает данные). Как вы могли заметить, model здесь создается без участия конструктора. В данном случае я использую Singleton Pattern ("Одиночный Шаблон" wiki). 

1. Singleton Pattern 

Этот "шаблон" применяется для того, чтобы создать один единственный экземпляр класса во всей программе, а также защититься от возможности создания его копии в дальнейшем, таким образом Singleton Pattern обеспечивая работу всегда с одним экземпляром данных. ActionScript 3.x имеет определенные особенности реализации этого "паттерна", ввиду того, что в этом языке нельзя определить private конструктор, а это есть основная особенность Singleton Pattern-а. Поэтому проходиться прибегать ко всяким уловкам и вводить дополнительный параметр или структуру.

Рисунок 3. Model - "Одиночный шаблон"(parameter)

В своем примере я реализовал Singleton Pattern с введением внутреннего параметра allowInstantiation:Boolean, который гарантирует единственную инициализацию класса. Также можно прибегнуть к созданию встроенного private класса или переопределению Model класса в private и создать public оболочку-класс SingletonModel. Еще один из вариантов реализации этого "паттерна" можно посмотреть здесь
По своей сути Singleton Pattern есть дальнейшая реализация метода рефакторинга "Замена конструктора фабричным методом" (Replace Constructor with Factory Method). В котором, конструктор делается private, а создание объекта передается статической функции, см. пример ниже (на Java).
class Order 
{
      public Order (String customer)
      {
            _customer = Customer.create(customer);
      }
...
}

class Customer
{
      private Customer(String name)
      {
             _name = name;
      }
      public static Customer create(String name)
      {
             return new Customer(name);
      }
...
}

Для сравнения, хотелось бы привести пример реализации Singleton Patter на языке Java, который отличается от "Фабричного метода" только тем, что в нем экземпляр класса можно создаеть только один раз.
public class Database
{
     private static Database singletonObject;
     private String name;
    
     private Database(String n)
    {
           name = n;
    }

    public static Database getInstance(String n)
   {
          if(singletonObject == null){
                singletonObject= new Database(n);
          }

          return singletonObject;
   }
...
}

1.2. Описание Приложения.

Приложение которые здесь будет создано показано на следующем рисунке, ниже дана ссылка на swf-файл:
Рисунок 4. Вид приложения.

Приложение представляет из себя два "псевдо" экрана, только с одним из которых пользователь может совершать действия определенные кнопками в guiClass. В рассматриваемом случае эти  кнопки выполняют следующие операции:
Action 1 = Change Color - Изменять цвет выбранного экрана.
Action 2 = Show Text - Отображать введенный в "TEXT FIELD" текст на выбранном экране.
UNDO - отмена действий, реализована в части SyncCommand работает только при использовании команд с клавишей Shift
Также есть дополнительная кнопка Trace Operations, которая выводит весь список команд (работает только для ASyncCommand)

2. View

Рассмотрим структуру объектов в классе View.

Рисунок 4. Представление View

1. В конструкторе принимается только один параметр - controller, а модель создается из описанного выше Singleton Pattern-а. Таким образом это хороший пример того, как можно избавиться от лишнего делегирования объектов. 

2. Initialization. Я использую ее практически всегда. В данном случае происходит определение всех кнопок в одном классе gui = new GUI("gui"), который является контейнером. Сами кнопки создаются на основе одного класса GUIButton("Title", uint(color)) и, для того чтобы в дальнейшем было проше определять действия пользователя, им присваивается имя соответствующее выполняемому действию, определенное в отдельном классе utils.actions.GUIAction.as

Рисунок 5. Создание кнопок в классе GUI.as

Ниже строчкой создаются основные и самые комплексные объекты приложения - "дисплеи" (на рис. 4 "салатовый" квадрат : ). Здесь я сразу применил один из методов Рефакторинга - "Введение внешнего метода" (Introduce Foreign Method), чтобы избавиться от повторения кода - 2.1 на рисунке 4. Для добавления имени "экрана" (displayClass на рис. 4) применяется Decorator Pattern, о чем рассказано в следующей части.
Далее идет определение изначально выбранного монитора: 
display_selected = display_one;
display_selected.Select();
где функция .Select() (а также .Deselect()) реализована в самом классе BaseDisplay, подробнее о реализации класса Display написано ниже.

3. Обработка взаимодействий пользователя и отправка их на "контроллер". Во всех своих приложениях я стараюсь минимизировать количество "слушателей событий", это позволяет значительно ускорить работу приложения. Благодаря особенности построения Flash и языка ActionScript 3.x, прослушивание события происходит по всему массиву DisplayObjects, но только если у "родителя" не стоит .mouseChildren = false и не используется "слушатель" MouseEvent.ROLL_OVER (.ROLL_OUT), у которого по умолчанию этот параметр равен false. Эта особенность позволяет ставить всего один EventListener на родительский объект и детектировать взаимодействие с внутренними объектами через переменную e.target, в которой передается последний в массиве DisplayObjects объект (последний на родителе которого не стоит .mouseChildren = false;). Как раз так я делаю, и сортировку провожу на основе имени объекта e.target.name (e.target.parent.name), либо по идентификатору класса var className:String = getQualifiedClassName(e.target), который можно разложить в массив className.split("::")[1]  или разрезать className.slice(className.lastIndexOf("::") + 2), оба варианта работают одинаково хорошо, только первый возвращает массив, а второй строку. Также здесь происходит выполнение выбора пользователем активного "дисплея" - 
1) display_selected.Deselect(); 
2) display_selected = e.target as Display;
3) display_selected.Select();

Подробнее об устройстве Display читайте дальше, в разделе 3.

4. И наконец функционал отвечающий за обновление View из класса Model. Ничего необычного простой вывод сохраненных в модели данных.

3. Decorator Pattern and Display class

"Декоратор" также один из самых распространенных шаблонов программирования. Основная задача которого заключается в дополнении функционала объекта. Decorator Pattern также носит название Wrapper-а или оберточного шаблона. Decorator можно использовать чтобы динамически добавить объекту какой нить атрибут, параметр, свойство или другой объект, к примеру добавить описание. Используя Decorator Pattern можно программно составлять объект из заранее созданных компонентов, к примеру расположить в окне кнопку "закрыть" и/или "ок", добавить текстовое поле, описания и/или картинку. Как раз добавление компонента - имени дисплея, мы и рассмотрим в качестве примера.

Рисунок 6. Реализация Decorator Pattern.




Прежде чем рассмотреть реализацию Decorator Pattern-а давайте посмотрим на то, как реализован сам класс Display.
Display class

Рисунок 7. Реализация Display класса

Как можно видеть из рис. 7 сам класс Display не содержить никакого графического наполнения, пока он пустой и содержит только информацию о первоначальном цвете экрана, которая "вынимается" из расширяемого им класса BaseDisplay. Поскольку в нашем примере, все дисплеи будут иметь одинаковую основу, то разумно вынести реализацию графического наполнения в общий класс - BaseDisplay, а в сам класс Display включить только обработку комманд пользователя, о чем будет сказано ниже, в разделе 4. Класс BaseDisplay работает с небольшой хитростью : ) и создает основу нашего дисплея, его background. Как вы можете видеть необходимая нам в дальнейшем переменная _color (стрелочка 3) класса Display присваивается после генерации случайного цвета функцией getRandoColor() (стрелочка 1) и делегирования значения в отрисовку background (стрелочка 2). Такой способ передачи в метод значения другого метода называется "Замена временной переменной вызовом метода" (Replace Temp with Query), это один из видов рефакторинга. Также в этом классе реализованы два метода присущих всем дисплеям, это "выбор" (Select) и "снятие выбора" (Deselect).

Decorator Pattern

Определение Decorator Pattern (GoF): "Присоединяет дополнительные возможности для объекта. "Декораторы" обеспечивают гибкую альтернативу подклассов позволяя динамически расширить функционал объекта."

UML (GoF)
Элементы паттерна:
1. Component (LibraryItem) - определяет интерфейс объектов, которые могут быть расширены динамически.
2. ConcreteComponent (Book, Video) - сам объект в который могут быть добавлены динамические атрибуты-параметры
3. Decorator (Decorator - Wrapper) - сохраняет ссылку на объект component и реализует функции интрефейса объекта component (без внутреннего заполнения).
4. ConcreteDecorator (Report) - добавляет функционал объекту.


Чтобы этот паттерн работал, объект который будет им обрабатываться должен иметь подходящие "свойства". К примеру, нельзя "надеть шляпу на человека без головы"). То есть объект должен быть готов к внедрению или изменению своих "свойств". Если эти "свойства" только изменяются, то ему не нужно дополнительное расширение под эти "свойства", потому как он ими уже обладает (хотя они могут быть реализованы в другом классе, расширяемым этим объектом), к примеру "апгрейд" атаки или брони юнита (см. реализацию "Update Unit with Decorator Pattern" ниже). Далее необходимо "пометить\выделить" эти "свойства" как изменяемые. Реализовать эту "пометку\выделение" позволяют интерфейсы (Interface)  или абстрактные классы (но они не поддерживаются в AS3.0). Интерфейс будет описывать "псевдо" объект, который обязан иметь "свойства" описанные в этом интерфейсе, т.е. обращаясь к этому "псевдо"-объекту вы можете быть уверены, что он содержит только тот набор переменных и методов которые описаны в интерфейсе. Внедрение (implements) интерфейса позволяет выделить в отдельный "псевдо"-объект только необходимые "свойства" и работать уже с объектом-интерфейса, и только с теми методами и переменными, которые "выделены" в интерфейсе. Таким образом это есть "один из вариантов обеспечения полиморфизма в объектных языках". Интерфейсы являются основой многих шаблонов программирования, в их числе Decorator и Command паттерн. 
В нашем случае класс BaseDisplay (и все его "потомки") принадлежат одному интерфейсу IWrapper спрятанному в классе Wrapper, который реализует в себе методы и свойства этого интерфейса.

Рисунок 8. Внедрение интерфейса IWrapper через дополнительный класс Wrapper.

Поскольку, изначально, не планировалось добавлять к нашим дисплеям никаких компонентов, то они не имели "свойств" позволяющих это делать. Поэтому был введен дополнительный класс Wrapper который включает в себя всео, что относиться к изменению и дополнению расширяющих его объектов. 
Для реализации Decorator Pattern-а все возможные модификации объекта, необходимые для этого свойства и методы, должны быть занесены в интерфейс, у нас это IWrapper, этот шаг является первоначальным для построения большинства шаблонов программирования. 

Конечно можно было бы реализовать все о одной куче, прямо в классе Wrapper, но это было бы не красиво и неудобно в дальнейшем, или же можно было бы найти другой, более подходящий для этого случая паттерн, но здесь мне хотелось реализовать именно Decorator, чтобы посмотреть как он работает в связке с другими уже реализованными шаблонами (MVC и Command). Если вы знаете лучшее решение, расскажите пожалуйста!

Основной класс TitleDecorator (ConcreteDecorator GoFявляется тем самым оберточным классом и в нем происходит  добавление соответствующего компонента в наш дисплей.

 Рисунок 9. Класс TitleDecorator и его реализация во View.

TitleDecorator принимает в своем конструкторе ссылку на объект соответствующий интерфейсу IWrapper (таким является класс Display (extends BaseDisplay extends Wrapper implements IWrapper)). Затем заносит его в свою внутреннюю переменную this.wrappedObject = wrappedObject. Наконец из класса View вызывается необходимая нам функция addDisplayName, которая возвращает во View уже обработанный объект.
Вот и весь паттерн. Дополнительные примеры реализации Decorator Pattern-а на Java и AS3.0 можно прочитать в разделе Дополнительные примеры ниже.

4. Command Pattern

Определение Command Pattern (GoF): Применяя принцип инкапсуляции выделяем запрос на действие (вызов действия) в отдельный объект, тем самым позволяя параметризовать клиента различными запросами, "очередью" (queue or log requests), а также позволяет реализовать отменяемые действия (support undoable operations).
Элементы паттерна:
1. Command (Command) - интерфейс для выполнения (execute) операций.
2. ConcreteCommand (CalculatorCommand) - определяет связь между объектом Receiver и действием. Реализует выполнение (execute) ссылаясь на соответствующие методы в Receiver
3. Client (CommandApp) - создает команды и устанавливает для них Receiver объект.
4. Invoker (User) - вызывает нужную команду для выполнения запроса.
5. Receiver (Calculator) - знает как обрабатывать операции поступившие в запросе.

Прежде чем начать описание реализации этого паттерна в MVC давайте еще раз взглянем на рисунок его работы.


Здесь, также все начинается с определения интерфейса для объекта, над которым будут выпоняться комманды. В нашем случае этим объектом является Display и в этот класс внедрен (implements) интерфейс IReceiver.

Рисунок 10. IReceiver - интерфейс для объектов обрабатываемых командами.

1. Чтобы реализовать действие комманд наш объект обязательно должен иметь соответствующие функции, что и закрепляется в интерфейсе IReceiver, т.е. (еще раз) объект, в который внедрен этот интерфейс обязан иметь описанные в интерфейсе методы.

Рисунок 11. Класс Display c внедренными функциями из интерфейса IReceiver

Функция ChangeColor - изменяет цвет дисплея, ShowText - выводит на дисплей тект, set и get вспомогательные функции для установки значений. Хотелось бы выделить функцию ClearTween, которая удаляет всю анимацию с объекта после того как она завершена, это необходимая мера дабы разгрузить память от не нужной анимацией, после ейо проигрыванию. Вместо TweenMax можно было использовать его облегченный аналог TweenLite, но в этом случае пришлось бы импортировать и активировать плагин ColorMatrixFilterPlugin .

2. Теперь нужно определить команды. В Command Pattern у них должна быть единственная функция execute, которая будет запускать выполнение действий над объектом, ейо конечно же нужно описать в интерфейсе ICommand.

Рисунок 12. Интерфейс ICommand

3. Определим комманды. Основной особенностью комманды является то, что в ней заложено действие (2) и она хранит в себе ссылку на объект (1), над которым совершается действие (или только на его часть, определяемую интерфейсом IReceiver).

Рисунок 13. Комманды.

4. Последнее, что осталось рассмотреть это вызов команд. Так как основой этого примера служит MVC паттерн, то разумно воспользоваться его функционалом и реализовать вызов команд через Controller класс.

Рисунок 14. Запуск выполнения комманд из Controller класса.

View "прослушивает" все клики пользователя и если имя объекта (или класса) на который он кликнул соответствует классу "gui", то происходит передача действия в контроллер.

_controller.LookForAction(
display_selected, // Selected Display
e.target.name, // Action 
gui.message.text // Message
);

(1) Контроллер обрабатывает запросы пользователя на основе поступившего действия, которое изначально зашито в имени каждой кнопки, определенного в классе GUIAction (2).
(3) Принимаемая ссылка на объект сразу типизируется под объект интерфейса IReceiver.
(4) В соответствии с действие создается определенная команда, в нее передается ссылка на обрабатываемый объект, а также вспомогательные данные. Затем вызывается единственный присущий команде метод execute, который запускает ее действие.

5. ASyncCommands and Undo

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

Рисунок 15. Интерфейс IAsyncCommand

Поскольку мы хотим сделать так, чтобы команды выполнялись одна за другой, то необходима функция отвечающая за добавление действие после выполнения команды, как раз такой функцией является addCompleteCallback(callback:Function):void, которая хранит в себе функцию-отклик выполняемую после завершения действия команды. Также в интерфейсе описана функция executeUndo, которая реализует откат к предыдущей команде (предыдущим параметрам). Этот интерфейс мы внедряем в основу наших асинхронных команд класс ASyncCommand, в котором реализовано добавление, сохранение и вызов callback-функции.

Рисунок 16. Реализация интерфейса IAsyncCommand в классе AsyncCommand

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

Рисунок 17. ASyncCommands.

Мы хотим чтобы команды выполнялись одна за другой, а следовательно нам нужно отслеживать завершение действия команды. onComplete запускает функцию complete класса AsyncCommand, из которой вызывается заранее сохраненная ответная функция _callback.call(), рисунок 16. В этом заключается "асинхронизм" команд. Отмена действия (executeUndo) происходит по тому же принципу, что и нормально действие над объектом (через тот же твин), только с другими параметрами - _undoColor или _undoText, которые сохраняются  из предыдущих значений переданного объекта каждый раз при создании новой команды (благодаря set в IReceiver). А теперь самое интересное! Для сохранения и обработки выполнения команд мы создадим специальный класс CommandList, теперь именно из него будут выполняться команды, он является как бы Invoker в нашем паттерне, хотя он немного не так построен, но выполняет теже функции (см. пример Client - Server Application with Command Pattern (Java) ниже).

Рисунок 18. CommandList.

Работает он следующим образом. Каждый раз когда мы добавляем команду (add), первым делом в нее добавляется функция отклика (addCompleteCallback), определенная в интерфейсе IAsyncCommand (реализованная в ASyncCommand). Фунция executeNext запускает выполнение следующей команды, если только она есть в массиве команд. Массив commands является стэком, в который добавляются команды. Чтобы проверять, выполняется ли при добавлении команды какая либо другая команда, запускается функция attemptExecute(), если isBusy = false, то делаем executeNext, в которой следующая команда будет только что добавленной. Затем извлекаем команду из начала массива (commands.shift()). Смотрим на то, какое действие должно выполняться, отмена или "прямое". Если "прямое", то выполняем команду и заносим ейо в массив publicCommands, который является массивом содержащим выполненные команды, т.е. те, которые можно будет отменить. Вот так выполняются команды. До того, как рассмотреть выполнение Undo-команд, давай те взглянем на Controller и добавление команд.

Рисунок 19. Вызов асинхронных команд.

Во View поставлено ограничение, что только при зажатой клавише Shift можно вызвать создать последовательность выполнения команд. Контроллер обрабатывает клики пользователя точно также - по имени кнопки на которую мы нажали. Сам CommandList находиться в модели, чтобы к нему можно было получить доступ из других частей приложения. Перед добавление команды мы выставляем значение _model.commandsList.undo = true, тем самым говоря, что эта команда идет от пользователя, и может быть сохранена в массиве publicCommands. При выполнение Undo, мы не создаем команду, а на прямую обращаемся к функции Undo в классе CommandList (см. второй синий квадратик на рисунке 18), в которой происходит следующие действия:
1) undo ? false : undo - говорит о том, что команда будет выполнять действие "отмены", а не "прямое" действие от пользователя.
2) Затем смотрится не пуст ли массив отмены - if (publicCommands.length > 0)
3) Если он не пуст, то из него извлекается последняя команда: publicCommands.splice(publicCommands.length-1,1).shift(). А затем запускается ее выполнение: obj.command.executeUndo();
При выполнении этих команд они также как и нормальные команды добавляются в массив commands, в котором может накопиться несколько последовательных команд, но выполнять они будут отмену действия, без повторного внесения в массив publicCommands (см. рисунок 18, первый синий квадратик).

В заключении.
Давайте подытожим.

Singleton Pattern - служит для создания единственного экземпляра объекта в любом месте программы, обеспечивает доступ всегда к одним и тем же данным. Особенностью является закрытый конструктор или ограничения на его вызов, а также статическая функция возвращающая только один единственный экземпляр класса.

Decorator Pattern - выполняет функции "обертки" (Wrapper), обновления и добавления данных (т.е. команд над объектом). Особенностью реализации является единый с обрабатываемым объектом интерфейс или абстрактый класс расширяющий обрабатываемый объект (см. пример на Java ниже). В конструкторе принимает ссылку на обрабатываемый объект.

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


Вот собственно и все. Далее предлагаю вашему вниманию еще несколько примеров CommandPattern и DecoratorPattern на Java и AS3.x 

Дополнительная часть: примеры.

Control Unit with Command Pattern (AS3.x), ссылка на скачивание проекта FD4 здесь. Функционал такой же как и в рассмотренном выше примере, только проще, так как нету Undo. Этот пример демонстрирует управление юнитом (unit), на основе команд: 
- перемещение (move) - щелчок мыши на экране.
- атака (attack) - клавиша 1 на клавиатуре.
- сбор ресурсов (gather) - клавиша 2 на клавиатуре.
Выполнение команд вшито внутри юнита - класс com.patterns.commands.Unit (методы - move, attack, gather). Обработка пользовательский действий (нажатия клавиши) происходит в корневом классе CommandPattern. При этом, процесс выполнения команд зависит от того, какая дополнительная клавиша зажата: 
Shift - добавление команд в простой массив и их постепенный вызов по таймеру.
Ctrl - выполнение асинхронных команд из стэка - commandListClass.

Client - Server Application with Command Pattern (Java)
Это простейшее приложение, только пример того, как еще можно реализовать Command Pattern. В нем клиент будет запрашивать у разных серверов один и те же действия (connect, diagnostic, reboot, shutdown, disconnect), которые будут выполняться через команды. Пользоваться я буду IntelliJ IDEA.


Как всегда начинаем с определения интерфейса. Для Command Pattern это IReceiver:
public interface IReceiver
{
         public void connect();
         public void diagnostics();
         public void reboot();
         public void shutdown();
         public void disconnect();
}

Далее заносим действия в объект:
public class EuroServer implements IReceiver
{
         public EuroServer()
         {
           // Пустой конструтор
         }

         public void connect() { System.out.println("Connected to Euro server - Done");}
         public void reboot() { System.out.println("Start Reboot Euro server");}
         public void diagnostics() { System.out.println("Euro server start Diagnostic");}
         public void shutdown() { System.out.println("Shutting down the Euro server");}
         public void disconnect() { System.out.println("Disconect from Euro server - Done");}
}

Тоже самое и для остальных серверов.
Теперь создаем интерфейс для команд и сами команды.
public interface ICommand
{
         public void execute();
}

Для примера рассмотрим только одну команду - DiagnosticCommand:
public class DiagnosticCommand implements ICommand
{
         Receiver receiver;
         public EuroServer(receiver r) { receiver = r; }
         public void execute()
         {
               receiver.connect();
               receiver.reboot();
               receiver.diagnostic();
               receiver.reboot();
               receiver.disconnect();
         }
}

Здесь вызов 6-ти методов объединен в вызов одной команды. При этом данная команда может работать с любым объектом интерфейса IReceiver, который гарантирует, что в принимаемом командой объекте будут реализованы вызываемые методы. Это есть чистой воды принцип инкапсуляции ООП.
Следующим шагом будет создание Invoker, класса который будет запускать команды. Можно конечно обойтись без него и запускать каждую команду индивидуально, но так уж устроен Command Pattern в своем изначально виде.
public class Invoker
{
         Command command;
         public Invoker(receiver r) { }
         public void setCommand(Command c) { command = c; }
         public void run() { command.execute(); }
}

Окей, теперь проверим работу приложения:
public TestCommand()
{
         Invoker invoker = new Invoker();
       
// Create Receivers
         EuroServer euroServer = new EuroServer();
         ...
// Create Commands
         DiagnosticCommand diagnosticEuro =  new DiagnosticCommand (euroServer );
         ...

         invoker.setCommand(diagnosticEuro);
         invoker.run();
}

Invoker может служить как стэк для хранения команд, также с его помощью можно реализовать отмену действия, для этого нужно изменить его следующим образом:
public class Invoker {
    ICommand commands[] = new ICommand[5]; // Создаем массив длины 5
    int position; 

    public Invoker() {
        position = -1; // Начальная позиция будет ноль
    }

    public void setCommand(ICommand c) {
        if (position < commands.length - 1) {
            position++; // Так как добавляемая команда будет следующая
            commands[position] = c;
        } else { // Если количество команда привысило размер массива
            for (int loopIndex = 0; loopIndex < commands.length - 2;
                 loopIndex++) {
                commands[loopIndex] = commands[loopIndex + 1]; // Сдвигаем все команды на одну,                                
                                                                                                // стирая первую команду
            }
            commands[commands.length - 1] = c;
        }
    }

    public void run() {
        commands[position].execute();
    }

    public void undo() {
        if (position >= 0) {
            commands[position].undo();
        }
        position--;
    }
}

А также нужно добавить описание undo в интерфейс ICommand и соответствующие обработки в сами команды. К примеру:
public void undo() {
    System.out.println("Undoing...");
    receiver.connect();
    receiver.shutdown();
    receiver.disconnect();
    System.out.println();
}

Более подробно этот пример вы можете посмотреть скачав его проект отсюда.


Update Unit with Decorator Pattern (AS3.0)
Для примера рассмотрим юнита - морпеха из игры Starcraft. Это весьма комплексный объект содержищий различные части: модель, анимацию, различные команды (атака, движение, оборона), характеристики урона и брони и т.д. Как раз обновление этих характеристик нас и будет интересовать.
Как всегда начнем все с описания интерфейса:

Описание интерфейса IUnit и Marine класс.

Сам юнит не будет иметь никакого внешнего вида, а только параметры damage и armor. Теперь напишем вспомогательный класс UnitDecorator, в котором будем хранить ссылку на обрабатываемый объект, а также получать доступ к его параметрам:

Класс UnitDecorator

Теперь мы может создать сам класс Update, который будет расширять UnityDecorator и в котором будет происходить обновление параметров юнита.

Update.

В этих двух классах и происходит Update. Для начала мы получаем наш юнит в конструкторе и передаем его в "родительский" класс UnitDecorator, где ссылка на него сохраняется и мы получаем доступ к его параметрам. Затем, переопределением функций (override)  присваиваем (добавляем) новое значение параметрам. Собственно говоря можно было и не передавать объект на более низкий уровень и вообще этот уровень не создавать (класс UnitDecorator), а на прямую обращаться к параметрам объекта. Все это необходимо для удобства понимания кода и его будущей модификации.
Работает это все следующим образом:

Результат работу Decorator Pattern-a.

Отлично! Таким образом этот пример показывает хороший пример использованию шаблонов программирования. Скачать и посмотреть внимательней этот проект можно отсюда.


Build Up a Computer with Decorator Pattern in Java.
Последний пример, который мне хотелось бы рассмотреть посвящен применению Decorator Pattern-а для добавления компонентов к объекту - сборка компьютера. Изначально есть пустой блок, имеющий только информацию о себе, мол "я вот просто блок", затем к нему будут добавляться различные части - материнка, видео, моник, хард, сидюшник. Сделано это будет на основе абстрактного класса (класс который определяется (выполняются методы) до обязательного переопределения его методов). 
ComputerCase.java
package objects;
ublic class ComputerCase {
    public ComputerCase() {}
    public String description() {
        return "Start with just a computer case \n";
    }
}

ComponentDecorator.java
package decoration.base;
import objects.ComputerCase;
public abstract class ComponentDecorator extends ComputerCase {
    public abstract String description();
}

Затем идут компоненты, вот пример одного из них: 
MotherBoard.java
package decoration;
import decoration.base.ComponentDecorator;
import objects.ComputerCase;
public class MotherBoard extends ComponentDecorator {
    ComputerCase computerCase;
    public MotherBoard(ComputerCase computerCase) {
        this.computerCase = computerCase;
    }
    public String description() {
        return computerCase.description() + " + MotherBoard with 8Gb DDR4 \n";
    }
}

Здесь такой же принцип, что в примере для Action Script 3.x. Сначала сохраняем объект во внутренней переменной, затем обновляем его параметры или добавляем новые и возвращаем обратно в первоначальный объект.
И на конец собирается это во едино в классе TestDecorator.java:
public class TestDecorator {
    public static void main(String args[]) {
        ComputerCase computer = new ComputerCase();

        computer = new MotherBoard(computer);
        computer = new GraphicCard(computer);
        computer = new Disk(computer);
        computer = new Monitor(computer);
        computer = new CD(computer);

        System.out.println("You're getting a: \n" + computer.description());
    }
}

В результате мы увидим следующее сообщение в дебаге:

You're getting a:
Start with just a computer case
 + MotherBoard with 8Gb DDR4
 + NVidia GeForce5800Fx
 and a HDD 1Gb with 7200 Speed
 and a 26 inch UltraHD Monitor
 and a BlueRay RWDrive

Подробнее ознакомиться с проектом можно скачав его отсюда (IntelliJ IDEA).

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

Большая просьба, если вы найдете ошибку или посчитаете  что нибудь и выше сказанного не корректным, напишите мне. Также если возникнут вопросы связывайтесь со мной по скайпу: rimidalv.niknim или e@mail: you_have@mail.ru

На этом я хотел бы откланяться.

Комментариев нет:

Отправить комментарий