понедельник, 27 февраля 2012 г.

Design Pattern - Proxy Pattern

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



В статье из wikipedia Proxy Pattern разделяют еще на несколько групп:
Шаблон proxy бывает нескольких видов, а именно:
  • Удаленный заместитель (англ. remote proxies) : обеспечивает связь с «Субъектом», который находится в другом адресном пространстве или на удалённой машине. Так же может отвечать за кодирование запроса и его аргументов и отправку закодированного запроса реальному «Субъекту»,
  • Виртуальный заместитель (англ. virtual proxies): обеспечивает создание реального «Субъекта» только тогда, когда он действительно понадобится. Так же может кэшировать часть информации о реальном «Субъекте», чтобы отложить его создание,
  • Копировать-при-записи: обеспечивает копирование «субъекта» при выполнении клиентом определённых действий (частный случай «виртуального прокси»).
  • Защищающий заместитель (англ. protection proxies): может проверять, имеет ли вызывающий объект необходимые для выполнения запроса права.
  • Кэширующий прокси: обеспечивает временное хранение результатов расчёта до отдачи их множественным клиентам, которые могут разделить эти результаты.
  • Экранирующий прокси: защищает «Субъект» от опасных клиентов (или наоборот).
  • Синхронизирующий прокси: производит синхронизированный контроль доступа к «Субъекту» в асинхронной многопоточной среде.
  • Smart reference proxy: производит дополнительные действия, когда на «Субъект» создается ссылка, например, рассчитывает количество активных ссылок на «Субъект».
В компьютерной графике также присутствует понятие Proxy-объектов и они часто используются для визуализации крупных сцен, где они также выполняют роль "замещающих контейнеров" для высокополигональных объектов, что позволяет сильно разгрузить оперативную память. Подробнее о использовании Proxy в Maya можно посмотреть тут.

Простым примером прокси шаблона в AS3.0 может служить класс Loader, который наследует все свойства и методы класса DisplayObject, т.е. может быть добавлен на экран, иметь маску, изменять свой размер и т.д., но саму графику на экране он не будет отображать до тех пор, пока она не загрузиться.



То есть, Loader выступает в качестве "обертки" для загружаемой в него графики.

В ActionScript 3.0 также есть специальный класс Proxy, расширение которого позволяет создать объекты с собственным набором функциональных параметров, т.е. расширить\переопределить возможности параметров и методов базового объекта, создав над ним "надстройку" (контейнер). К примеру, можно создать "прокси" массив с тем же функционалом что и у стандартного массива, но еще добавить к нему вывод всех элементов в виде строки или сумму всех элементов. Более подробно, с несколькими примерами, о возможностях этого класса можно посмотреть тут.

Я же, хочу рассмотреть использование этого шаблона на примере постройки юнита, как в игре Starcraft, на подобии юнита у зергов, когда сначала мы начинаем "выращивать" юнита, он еще не может перемещаться, но мы уже можем отдать ему команду двигаться к определенной точке, куда он и переместиться после того как вырастет.

Разработке будет приводиться в FlashDevelop.
РЕЗУЛЬТАТ МОЖНО ПОСМОТРЕТЬ ТУТ (Ctrl+Click - начало создание юнита)
В это примере обойдемся без каких либо MVC и напишем все в одном классе.

1. IUnit Interface
Как всегда, проще всего начать с определения (описания) функционала объектов, для этих целей лучше всего подходит "интерфейс" (interface). 


Для простейшего юнита нам понадобятся следующие свойства: выделение (select) и имя (name), а также всего одна операция над ним - это перемещение (move). Весь этот функционал будет реализован на прямую в самом юните, потому как является уникальным для данного типа юнита. 
Также, я недавно увидел, что интерфейсы можно использовать для хранения значений переменных, правда не вижу в этом особых преимуществ по отношению к классам, но и такое существует.

2. Unit class
Здесь мы начинаем реализацию класса самого юнита, конечно на основе интерфейса IUnit, т.е. нам обязательно нужно описать все, что мы там определили. Начнем сверху вниз.


1. Статические константы для цвета, ничего криминального, просто статические константы, к ним мы еще будем обращаться из класса "прокси", их, кстати, можно перенести и в сам интерфейс IUnit. Затем переменная _selected - хранит в себе выбран ли наш юнит, задается и читается c помощью get и set (accessor functions). Переменная scaleSelected - определяет размер выбранного юнита. И наконец, velocity - постоянная скорость перемещения объектов, ее мы задаем при добавлении объекта на сцену, и равна она ширине сцены, что обеспечивает нам перемещение по всей ширине за одну секунду. При инициализации (init()) мы рисуем графику определяющую внешний вид юнита.


2. Функция move - та самая функция движения объекта, которая вызывается при клике на сцене. Здесь я ввожу новый параметр ptime (pathtime) - время перемещения в указанную точку с постоянной скоростью, которое высчитывается по следующей формуле:


Движение мы создаем с помощью одного из самых популярных анимационных flash-движков TweenLite. Конечно можно было бы вручную написать анимацию с помощью вспомогательного таймера, или, уж если совсем упрощать и оптимизировать код, то можно создать один анимационный таймер (статический) для всех юнитов, который отсчитывал бы интервал перемещения объектов на сцене. Здесь движется не сам объект-юнит, а его "прокси" оболочка (this.parent).


3. Реализация select выполнена с помощью set и get. Хочу отметить одну особенность в использовании проверки значения знаком вопроса ( condition ? [return if true] : [return if false]; ). Как известно, используя эту методику проверки мы можем вернуть только одно значение и это обычно ограничивало использование данной конструкции если нужно было выполнить дополнительно несколько действий. Однако, если в качестве результата проверки возвращать массив с несколькими элементами (к примеру, функциями), то все они будут выполнены. Данная методика отмечена пунктирной линией на рисунке выше. Здесь сформирован массив из двух элементов, первый - это возвращаемое (присваиваемое) значение (о чем говорит нолик после массива - ][0]), второй - выполняемая при определении массива функция. Таким образом, при проверке с помощью знака вопроса можно выполнять последовательно несколько действий, и возвращать результат любого из них.

Далее идут функции, которые делают наш юнит выбранным или снимают с него выделение. Здесь нужно иметь ввиду, что при использовании в движке TweenLite какого-нибудь дополнительного функционала (в нашем случае это tint) нужно обязательно подключить необходимые для него библиотеки. Этого не нужно делать если вы используете TweenMax, который изначально включает в себя все возможности анимационного движка Greensock. В нашем случае необходимо импортировать и подключить:

import com.greensock.easing.Elastic;
import com.greensock.plugins.TintPlugin;
import com.greensock.plugins.TweenPlugin;
TweenPlugin.activate([TintPlugin]);

Также обращаю ваше внимание, что после каждого использования TweenLite (onComplete) я очищаю объект от "твина" TweenLite.killTweensOf(this, false);

3. UnitCreate (Proxy) class
Теперь перейдем к реализации самого главного класса этой статьи, того самого Proxy - UnitCreate.


1. Самое важное в этом классе то, что он "имплементирует" (implements) интерфейс IUnit. То есть, в UnitCreate обязательно должен быть реализован тот же самый функционал, что и в самом классе юнита. Таким образом мы организуем "контейнер-обертку" для нашего юнита, а так как он будет располагаться на экране, то базовым классом будет Sprite.
Также нам понадобятся следующие объекты:
  • public var unitObject :Unit; - создаваемый юнит;
  • private var _selected:Boolean; - хранит в себе выбран ли юнит;
  • private var _navigationPoint:Point; - точка к которой нужно переместить юнита.
При создании "контейнера-обертки" (proxy) будем рисовать базовый внешний вид (функция init_MakeBaseForm()) и запустим таймер начала производства юнита с одним циклом.


2. По завершению работы таймера выполняется функция handler_CreationComplete, в ней нет необходимости останавливать таймер, так как цикл был запущен один раз, все что нужно сделать это удалить с него слушателя события TimerEvent.TIMER_COMPLETE, а затем обнулить ссылку на объект таймер - temp = null, таким образом сделав возможным очистку памяти от этого объекта. Конечно мы могли бы и не удалять прослушку события с объекта, но в этом случае нам необходимо выставить WeakReference при создании слушателя. Также здесь мы очищаем "контейнер" от графики (this.graphics.clear()).

3. Здесь мы создаем юнита, присваиваем ему имя, определяем его выбор, добавляем его в наш контейнер (можно добавлять и на сцену, но тогда нужно изменить логику перемещения и выбора юнита, так как в этом случае новый твин прекращает действие текущего) и также заставляем его двигаться в точку указанную пользователем, если такая была указана (строчка 82), о чем ниже.

4. Обязательная функция move (так как она описана в интерфейсе). В ней, по сути, можно ничего и не писать, но так как мы хотим, чтобы наш юнит имел возможность после создания переместиться в точку, которую указал пользователь, то здесь мы запомним эту точку в переменной _navigationPoint, координаты которой передадим юниту при его создании (строка 83).

5. Выбор юнита. Ничего криминального, просто запоминаем выбор юнита во внутренней переменной _selected.

Итого. Наш прокси объект - UnitCreate, выполняет прямую функцию контейнера для юнита. На время пока юнит "строится" он заменяет его соответствующей графикой, которая в дальнейшем будет стерта.

Нам осталось только рассмотреть реализацию взаимодействия пользователя.

4. Main
Основной класс (Document Class), который запускается при компиляции приложения. Его задача инспектировать действия пользователя (проверять правильность чьих-либо действий в порядке надзора). Основным действием является клик мыши. Проверяется в трех случаях:
  • клик по пустому пространству, с зажатой клавишей Ctrl = создание юнита
  • клик по пустому пространству, без Ctrl = задание координаты перемещения выбранного юнита
  • клик на юните = выбор юнита


1. Создание нового юнита при клике в пустое пространство сцены с зажатой клавишей Ctrl.
Первое, что делаем (строчки 42-43) - снимаем выделение с выбранного юнита, так как всегда выделенным юнитом считается последний построенный. Чтобы это отключить нужно также на строчке 47 задать unit.select = false.
Затем создаем "прокси-контейнер" для нашего юнита, передаем ему координаты клика, а также имя будущего юнита (тоже имя присваиваем и самому объекту). Выделяем "прокси" объект и запоминая ссылку на него - selectedUnit. Как выбранный юнит добавляем его на экран.

2. В этой части кода, мы проверяем на какой объект кликнули (нам нужен только IUnit) и не является ли он выбранным. Если условие выполняется, то мы делаем этот объект выделенным.

3. Если же, ни одно из выше описанных условий не выполняется, то мы перемещаем объект в ту точку куда кликнули.

Вот и все, проект с дополнительными комментариями и рисунками можно скачать отсюда.

Далее хотелось бы рассмотреть еще один пример использования прокси паттерна - сервер клиентское "соккет" соединение на Java.


Server Client Socket Connection via Proxy

Сразу хочу сказать, что я (пока что) не профессиональный Java программист и самостоятельно изучаю эту область, поэтому если у вас появятся какие-нибудь замечания по коду - прошу написать мне на почту (you_have@mail.ru) или вконтакт. Буду благодарен за любую помощь! : )
Разработку я буду проводить в IntelliJ Idea.

1. SocketServer
О том как создать свой "соккет" сервер в интернете много всего написано, я нашел пример тут и сделал на по аналогии, далее я опишу как.


0. Импортирование библиотек. Здесь, я хочу отметить прекрасную работу IntelliJ Idea, она все определят сама. Можно поставить "звездочку" в java.io.* и в java.net.*, а Idea в дальнейшем сама избавиться от лишних включений и заменит их только на то, что нужно, конечно же подсказки и hotkey радуют. : )

1. Определяем необходимые для работы объекты:
  • ServerSocket socket - объект, который ожидает входящего запроса от сети. В доках Oracle ему описание следующее:
    "public class ServerSocket extends Object
    This class implements server sockets. A server socket waits for requests to come in over the network. It performs some operation based on that request, and then possibly returns a result to the requester."
  • PrintWriter out - "отправщик" потоковых сообщений.
    "public class PrintWriter extends Writer
    Print formatted representations of objects to a text-output stream."
  •  Socket communicationSocket - связующий объект (socket - "розетка"), конечная точка соединения двух машин.
    "public class Socket extends Object
    This class implements client sockets (also called just "sockets"). A socket is an endpoint for communication between two machines."
2. public static void main(String args[]) - так называемая "точка входа программы" (entry point for the program). Статическая функция main - первая функция, которая будет вызвана виртуальной Java-машиной, при запуске программы. Если в классе несколько статических функции, то только main будет считаться точкой входа. Такая же система присутствует в виртуальной машине платформы .NET.

3. Создаем в памяти необходимые нам объекты и связываем их с нашими ссылками в классе. Также, создаем дополнительный поток для обработки входящих запросов:
Thread thread = new Thread(this);
thread.start();
Для выполнения действий в отдельном потоке необходимо создать специальную функцию -  public void run(), на что указывает имплементация класса интерфейсом Runnable (13 строчка).

4. Набор функций-откликов клиенту подключенному к данному порту на SocketServer-е. Вывод информации происходит через (PrintWriter) out.println("Text Message");
"println(String x) - Print a String and then terminate the line.The println() methods use the platform's own notion of line separator rather than the newline character."
5. В функции run происходит чтение и обработка входящих сообщений с клиента (конструкция взята из доков по Java SocketServer). В зависимости от того какое сообщение было отправлено с клиента, вызывается та или иная функция на сервере (из (4)) откуда также возвращается ответ клиенту.

Вот такой простенький "розеточный" сервер : )

2. ServerProxy (Client)
Класс ServerProxy встраивается в клиентскую логику и позволяет общаться с сервером через socket-соединение. Таким образом клиенту нет необходимости беспокоиться о создании соединения, преобразовании вызова в строку и получении ответа сервера - в чем есть основная идея Proxy паттерна (прокси-оболочки). Пример ниже взят из доков Oracle Java clientServer и немного модифицирован.


1. Определяем необходимые нам объекты. Здесь нам понадобятся:
    Thread thread - отдельный поток для просчета входящих сообщений;
    Socket socket - "розетка";
    InputStream in - объект-буфер входящих байт-данных;
    PrintWriter out -  "отправщик" потоковых сообщений;
    int character - номер входящего символа;

Хочу отметить, что здесь мы используем character как число - номер передаваемого символа.

2. Создание соединения с сервером через socket (указываем локальный адрес и порт подключения). Также определяем считывающий с "канала" socket-а и отправляющий объекты - in\out. Выделяем отдельный поток (thread = new Thread(this)) для анализа входящих данных (в этом случае класс обязательно должен иметь функцию run, на что указывает имплементация - implements Runnable).

3. Те самые public "прокси"-функции, вызываемые клиентом, через которые идет запрос на сервер (out.println("Message To Server")).

4. Обработка ответа с socket-а. Здесь происходит вывод сообщения с сервера до тех пор пока в входящем "канале" (in = socket.getInputStream()) есть какие либо символы. Строка (char) character переводит числовой индекс символа в сам символ, таким образом формируется строчка, в которую, при отправке запроса с сервера методом out.println(), автоматически подставлялись переносы.

Осталось сделать тестовый вызов запросов сервера через ServerProxy.

3. Test
Нам осталось обратиться к классу ServerProxy и вызвать в нем соответствующие функции.


Поскольку мы будем запускать этот файл, то нам необходима "точка входа" - функция main, в которой вызывается конструктор класса.

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

Проект можно скачать отсюда.

НА ЭТОМ ВСЕО.
Таким образом мы разобрали и посмотрели как можно работать с Proxy Pattern на двух примерах. Надеюсь это вам поможет.


Дополнение: Память и TweenLite
Решил посмотреть, как используется память при анимации TweenLite, через стандартный FlashDevelop profiler.


Результаты меня порадовали, Flash Player очень хорошо очищает память, и библиотека TweenLite весьма оптимизированно работает, так как все следы ее деятельности чистятся GC. Опишу поэтапно:
  1. Создание swf.
  2. Со временем память продолжает расти, но не значительно, лично я могу связать это с тем, что работает "профайлер", через некоторое время эта утечка чистится GarbageCollector-ом.
  3. Начинаю тестить. Создаю объекты (3.1). Память возросла.
  4. Принудительная очистка памяти из профайлера немного освобождает память, теперь, как и ожидалось, на сцене только реальные объекты (4.1)
  5. Начинаю множественное перемещение и выбор юнитов - работа TweenLite. Создается куча твинов и им сопутствующие объекты (Object, PropTween).
  6. Форсирование GC. Все "отработанные" объекты, которые были в памяти, удалены (6.1). 
  7. Со временем произошла полная очистка от неиспользуемых объектов. Вполне возможно, что временная задержка связана с profiler-ом.

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

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