Декоратор (Decorator) - паттерн проектирования, с помощью которого можно добавить объекту новые обязанности, опции, не изменяя его внутренней структуры. Известен также под названием Обёртка (Wrapper), которое во многом раскрывает его суть. Он оборачивает собой целевой объект и дополняет его основные функции. Это дает возможность расширять объект, добавлением или удалением обрамляющего декоратора без изменения самого объекта.
Для лучшего понимания, что собой представляет шаблон Декоратор, а я бы назвал его русским именем Matryoshka, рассмотри конкретный пример. Допустим нам необходимо расширить класс MyButton для добавления в него отступов (padding), рамки (border) и фона (background), то есть изменить отображение класса, причем добавляться элементы оформления должны как одновременно, так и по отдельности. Конечно можно добавить новые обязанности с помощью наследования, но это породит большое количество классов и будет более статическим решением, что не даст достаточной гибкости в управлении.
Более рациональный и гибкий подходом к данной ситуации: поместить объект класса MyButton , в дальнейшем компонент, в другой объект, который и будет имплементировать интерфейс компонента и добавлять необходимые элементы, он и называется декоратором. Важно понимать что декоратор следует интерфейсу декорируемого объекта, из-за чего он абсолютно прозрачен для клиента, поэтому декораторы могут вкладываться друг в друга, в произвольном порядке. Декоратор переадресует внешние вызовы компоненту, но может выполнять и дополнительные действия до или после переадресации. По этому клиент не может отличить декорированный объект от недекорированного и никоим образом не зависит от наличия или отсутствия оформления.
А теперь давайте рассмотрим пример реализации шаблона на actionscritp 3:
Сначала нам необходимо создать интерфейс для компонента, а также и декоратора. Назовем его IComponent
Так как я собираюсь наследовать MyButton от Sprite, важно и необходимо следить за тем, чтобы общие функции интерфейса и наследуемого класса были идентичны:
public interface IComponent
{
function set width(value:Number):void;
function get width():Number;
function set height(value:Number):void;
function get height():Number;
//для назначения стиля
function set decor(value:String):void;
function get decor():String;
function addEventListener(type:String, listener:Function, useCapture:Boolean=false, priority:int=0, useWeakReference:Boolean=false):void;
function dispatchEvent(event:Event):Boolean;
}
Так как декоратор прозрачен для клиента и неизвестен для декорируемого объекта, то изменять состояния самого декоратора слегка проблематично. Первый вариант - прописать в интерфейсе возможные состояния декораторов и записывать их в компоненте, откуда декоратор будет брать данные. Этот способ не гибок и ведет к созданию лишних параметров объекта.
Второй вариант - заставить декоратор реагировать на события объекта, при чем событие должно подыматься по цепочке пузырей и обновлять каждый декоратор. Этим вариантом и воспользуемся, событие назовем REDRAW, так как оно фактически перерисовывает декораторы.
Далее стоит создать супер классы компонентов и декораторов, важно что оба класса расширяют интерфейс IComponent, но каждый по своему:
Component.as
//супер класс компонентов, желательно сделать его internal
public class Component extends Sprite implements IComponent
{
private var decorator:String = 'all';//для того чтобы определить какой декоратор должен быть включен, по умолчанию это все декораторы
public static const REDRAW:String = 'component_redraw';//событие по которому обновляются декораторы
//контсанты событий желательно выделить в отдельный класс для меньшей связаности классов
public function set decor(value:String):void{
this.decorator = value;
//высылаем событие для изменения состояния декоратора
//важно чтобы событие проходило по цепочке пузырей
//только в этом случае все декораторы получат событие REDRAW
this.dispatchEvent(new Event(REDRAW, true, true));
}
public function get decor():String{return this.decorator;}
}
Decorator.as
//супер класс декораторов, желательно сделать его internal
internal class Decorator extends Sprite implements IComponent
{
protected var content:IComponent;
protected var hitSprite:Boolean;
public function Decorator(content:IComponent)
{
//записываем копонент в себя
this.content = content;
this.addChild(this.content as DisplayObject);
//прослушиваем компонент на его изменения
this.addEventListener(Component.REDRAW, this.redraw);
}
//функция перересовки декоратора, так как при дальнейшем redraw может быть вызвано
// и не событием Component.REDRAW, в получаемых аргументах пишем ...args
protected function redraw(...args):void{}
//Переопределяем полученные данные компоненту, вниз по цепочке декораторов
override public function set width(value:Number):void{this.content.width = value;}
override public function get width():Number{return this.content.width;}
override public function set height(value:Number):void{this.content.width = height;}
override public function get height():Number{return this.content.height;}
public function set decor(value:String):void{this.content.decor = value;}
public function get decor():String{return this.content.decor};
override public function addEventListener(type:String, listener:Function, useCapture:Boolean=false, priority:int=0, useWeakReference:Boolean=false):void{
//тригер для назначения декоратора областью прослушивания событий мыши
//желательно подобного не допускать, но для примера пойдет
if(!hitSprite)this.content.addEventListener(type, listener, useCapture, priority, useWeakReference);
else super.addEventListener(type, listener, useCapture, priority, useWeakReference);
}
override public function dispatchEvent(event:Event):Boolean{
return this.content.dispatchEvent(event);
}
}
Приступим к созданию конкретных классов, первый идет наш класс MyButton:
//подкласс MyButton класса Component, который надо декорировать
public class MyButton extends Component
{
public function MyButton()
{
//просто надпись для удобства
var label:TextField = new TextField();
label.autoSize = TextFieldAutoSize.LEFT;
label.selectable = false;
this.addChild(label);
}
}
А теперь посмотрите насколько просты классы декораторов:
Paddingю.as
//Декоратор отступов
public class Padding extends Decorator
{
private var size:int;
private var triger:Boolean;
public function Padding(content:IComponent){super(content);}
override protected function redraw(...args):void{
//если декоратор включен делаем отступы иначе возращаем все в 0
if( this.decor == 'all' || this.decor == 'padding'){
this.size = 5;
//так делать не стоит но для примера я не хотел сильно
//перегружать интерфейс и суппер класс декоратора
this.content['x'] = this.size*2;
this.content['y'] = this.size;
this.triger = true;
}else{
this.size = 0;
this.content['x'] = this.size;
this.content['y'] = this.size;
this.triger = false;
}
}
//расширяет состояние
override public function get width():Number{return this.content.width+this.size*4;}
override public function get height():Number{return this.content.height+this.size*2;}
}
Background.as
//Декоратор Фона
public class Background extends Decorator
{
public function Background(content:IComponent){super(content);}
override protected function redraw(...args):void{
this.graphics.clear();
if( this.decor == 'all' || this.decor == 'backgroud'){
//если декоратор включен то рисуем фон
this.graphics.beginFill(0x99cccc, 1);
this.graphics.drawRect(0,0,this.width, this.height);
this.graphics.endFill();
}
}
}
Border.as
//Декоратор рамки
public class Border extends Decorator
{
public function Border(content:IComponent){super(content);}
override protected function redraw(...args):void{
this.graphics.clear();
if( this.decor == 'all' || this.decor == 'border'){
//если декоратор включен то рисуем рамку
this.graphics.lineStyle(1, 0x669999);
this.graphics.drawRect(-1, -1, this.width+1, this.height+1);
this.graphics.endFill()
}
}
}
HitArea.as
//Декоратор зоны наведения мыши
public class HitArea extends Decorator
{
public function HitArea(content:IComponent)
{
//просто включает прослушивание событий мыши
super(content);
this.mouseChildren = false;
this.hitSprite = true;
}
}
И в конечном итоге соберем все части вместе и выведем на экран.
main.as - главный класс приложения
public function main()
{
var button:IComponent = this.createButton();
this.addChild(button as DisplayObject).x;
}
private function createButton():IComponent{
//создаем декораторы и компонент и вкладываем их друг в друга
//Думаю такой порядок их вложения логичен ине требует объяснений
var button:IComponent = new HitArea(new Border(new Background(new Padding(new MyButton()))));
//отсылаем событие на активацию всех декораторов
button.dispatchEvent(new Event(Component.REDRAW, true, true));
button.addEventListener(MouseEvent.CLICK, this.handlerClick);
return button;
}
Пример того что получилось. Каждый прямоугольник по своему реагирует на нажатие, оставляя лишь тот декоратор который написан на нем. Все прямоугольники созданы с помощью createButton и ничем друг от друга не отличаются:
Подведем небольшой итог:
Паттерн Декоратор (Decorator) более гибок нежели наследование, он может добавлять и удалять обязанности во время выполнения программы. При использовании же наследования придется создавать новый класс для каждой дополнительной обязанности. Кроме того декоратор позволяет добавить одно и тоже свойство дважды. Например, если добавить Border два раза, то получим двойное обрамление.
Но есть и явные недостатки, такие как множество мелких объектов, которые похожи друг на друга и различаются только способом взаимосвязи а не классом. А также детектор и его компонент не идентичны, о чем постоянно необходимо помнить.
RSS Feed
Twitter
Posted in
Tags: 
Объяснил хорошо! Никогда не пользовался вот так этим патерном. Но теперь точно попробую его где-то использовать
Спасибо