Декоратор (на английски: Decorator) е структурен шаблон за дизайн, който се използва в обектно-ориентираното програмиране.
Този шаблон може да бъде използван за разширяването на функционалността на опеределен клас по времето на изпълнение на програмата, като запазва интерфейса му. Той е много подходящ, когато се следва Принципът за еднолична отговорност, според който един клас трябва да е отговорен за една-единствена операция.
Този вид шаблон може да се използва за добавяне на функционалност към определен обект динамично, без това да засяга другите инстанции на същия клас.
Това се постига чрез създаването на нов клас Декоратор (англ. wrapper-опаковка), който „обвива“ класа. Това лесно може да се представи с примера, “опаковане на подарък, слагане на подаръка в кутия, опаковане на кутията“[1]. Създава се поредица от обекти, която започва с декоратор обектите, отговорни за новите функционалности, и завършва с оригиналния обект.[2].
Класовете и обектите, използвани при този модел са:
Особеностите на декоратора (методи, свойства и други) обикновено се определят от интерфейса, който е еднакъв за декораторите и за декорираните обекти. В посочения пример Компонент класът е наследен едновременно от Конктерния компонент и от подкласовете на Декоратор класа (англ. ConcreteDecoratorA, англ. ConcreteDecoratorB).
Трябва да се отбележи, че декораторите и оригиналният обект имат общи свойства. В посочената диаграма примерният методdoThis()
е достъпен и във вече декорираната, и в началната версия.
При този шаблон за дизайн няколко декоратора могат да бъдат използвани върху една инстанция на даден обект, като всеки от тях добавя нова функционалност.
Като пример, нека вземем прозореца (англ. window) в една прозоречна система (англ.windowing system). За да се разреши скролирането (англ. scrolling) през съдържанието на даден прозорец може да му се добави хоризонтална или вертикална плъзгаща лента (англ. scrollbar) в зависимост от прозореца. Нека приемем, че прозорците представляват инстанции (англ. instances) oт Прозоръчния клас (англ. Window class) и да приемем, че този клас няма функционалност (англ. functionality) за добавяне на плъзгащи ленти. В такъв случай, може да се създаде един подклас „ScrollingWindow” или „ScrollingWindowDecorator” , който добавя тази функционалност към вече съществуващия прозоречен обект (англ. Window object). На този етап и двете решения ще свършат работа.
Сега, нека приемем, че също желаем да добавим рамка (англ. borders) към този прозорец. Отново Прозоречният клас не поддържа такава функционалност. Подкласът „ ScrollingWindow” представлява проблем, защото ефективно създава нов вид прозорец. Ако трябва да се добави функционалност за рамки на много прозорци, но не и на всички, трябва да се създадат подкласове „WindowWithBorder” /прозорец с рамка/ и „ScrollingWindowWithBorder” /прозорец с плъзгаща лента и рамка/. Проблемът се влошава с добавянето на всяко едно допълнително свойство или прозоречен подтип. За декоративни решения трябва само да се добави „BorderedWindowDecorator” /декоратор на прозоречните рамки/, когато вече програмата е инициализирана (англ. runtime), може да се декорират съществуващите прозорци със „ScrollingWindowDecorator” или „BorderedWindowDecorator” , или и с двете, по преценка. Забележете, че ако се изисква да се добави една функционалност към всички прозорци, трябва само да се промени основния клас (англ. base class) . От друга страна , понякога / ако се използва външен софтуер (англ. framework)/ това е невъзможно да се направи, непозволено е или в случая е неподходящо да се промени основният клас.
Пояснение – в предишния пример, класовете „SimpleWindow” и „WindowDecorator” имплементират интерфейсът (англ. interface) на „Window”, което дефинира методите „draw()” и „getDescription()”, които са необходими в разиграния сценарий, за да се декорира прозоречен контрол.
Декораторът е удобна алтернатива на наследяването. При наследяването добавянето на функционалност става при компилация и промените се отнасят за всички инстанции на оригиналния клас. Декораторът от своя страна добавя функционалност към даден обект динамично т.е по време на изпълнение на програмата, без да променя неговата структура.
Посочват се две главни предимства и два недостатъка на декоратор шаблона[4] :
Пример за Декоратор [5]
using System;
using System.Collections.Generic;
using System.Text;
class DecoratorPatterns
{
// Decorator Pattern програмен шаблон за Декоратор – автор Judith Bishop Dec 2006
// Показва 2 декоратора и изводът от различни компбинации
// на декоратори върху основния компонент
interface IComponent
{
string Operation ();
}
class Component : IComponent
{
public string Operation ()
{
return "I am walking "; //Аз вървя
}
}
class DecoratorA : IComponent
{
IComponent component;
public DecoratorA (IComponent c )
{
component = c;
}
public string Operation ()
{
string s = component.Operation ();
s += "and listening to Classic FM "; //и слушам радио Классик
return s;
}
}
class DecoratorB : IComponent
{
IComponent component;
public string addedState = "past the Coffee Shop "; //вече съм минал покрай кафето
public DecoratorB (IComponent c )
{
component = c;
}
public string Operation ()
{
string s = component.Operation ();
s += "to school "; //към училище
return s;
}
public string AddedBehavior () //прибавено поведение
{
return "and I bought a cappuccino "; //и си купих капучино
}
} //eof class
class Client
{
static void Display (string s, IComponent c )
{
Console.WriteLine (s + c.Operation () );
} //eof method
static void Main ()
{
Console.WriteLine ("Decorator Pattern\n" );
IComponent component = new Component ();
Display ("1. Basic component: ", component );
Display ("2. A-decorated : ", new DecoratorA (component ) );
Console.ReadLine (); //изчакай удар по конзолата от потребителя
Display ("3. B-decorated : ", new DecoratorB (component ) );
Console.ReadLine ();
Display ("4. B-A-decorated : ", new DecoratorB (new DecoratorA (component ) ) );
// Explicit DecoratorB
Console.ReadLine ();
DecoratorB b = new DecoratorB (new Component () );
Display ("5. A-B-decorated : ", new DecoratorA (b ) );
// Invoking its added state and added behavior
Console.WriteLine ("\t\t\t" + b.addedState + b.AddedBehavior () );
Console.ReadLine ();
} //eof Main
} //eof class Client
} //eof class DecoratorPatterns
/* Output --- изводът на информация по конзолният прозорец
Decorator Pattern
1. Basic component: I am walking
2. A-decorated : I am walking and listening to Classic FM -
3. B-decorated : I am walking to school
4. B-A-decorated : I am walking and listening to Classic FM to school
5. A-B-decorated : I am walking to school and listening to Classic FM
past the Coffee Shop and I bought a cappuccino
* /
public class Testing {
public static void main(String[] args) {
// Създаваме два нови обекта от класовете Name и NameWrapper
Name name = new Name();
NameWrapper testWrapper = new NameWrapper(name);
/* Принтираме резултатът от методът getName()
при двата обекта (оригиналният и "обвитият") */
System.out.println(name.getName());
System.out.println(testWrapper.getName());
}
// Създаваме клас наречен Name с два метода
public static class Name {
// Задаваме първоначална стойност на името
private String name = "Иван";
// Метод, който ни дава възможността да променим името (неговата стойност)
public void setName(String name) {
this.name = name;
}
// Метод, даващ ни името (неговата стойност)
public String getName() {
return name;
}
}
/* Създаваме клас наречен NameWrapper, който "обвива" класът Name
и му придава нови функции */
public static class NameWrapper extends Name {
// Създаваме променлива от тип Name
private Name name;
/* В нашия конструктор задаваме стойността на променливата ни name
да е същата като на обектът който
сме дали като параметър на конструкторът*/
public NameWrapper(Name name1) {
this.name = name1;
}
/* Пренаписваме методът от клас Name за да можем да ползваме
неговите функции от нашия NameWrapper клас */
@Override
public void setName(String name) {
this.name.setName(name);
}
/* Пренаписваме методът от клас Name
и му добавяме допълнителна функционалност */
@Override
public String getName() {
// Добавяме допълнителен текст
return "Името е: " + name.getName() + ".";
}
}
}
/* Резултатът изписан на конзолата след изпълнението на програмата:
При оригиналния обект: Иван
При "обвитият" обект: Името е: Иван.
* /
Когато трябва да добавим уникални особености към даден клас, една добра алтернатива на наследяването е да използваме декоратор. Следващият пример обяснява какви са предимствата на втория подход.
Ето клас, който генерира съдържание за имейл. В долния блок ясно се вижда, че за генериране на стандартни съобщения, класът ще функционира добре.
class eMailBody {
private $header = 'This is email header';
private $footer = 'This is email Footer';
public $body = '';
public function loadBody() {
$this->body .= "This is Main Email body.<br />";
}
}
Да приемем, че идва Коледа и искаме да изпратим поздравително съобщение на някого. За целта една опция е директно да променим горния клас, което не искаме да правим, защото той работи добре. Може да постигнем същия ефект с наследяване. За целта създаваме дъщерен клас, който добавя допълнителното съдържание:
class christmasEmail extends eMailBody {
public function loadBody() {
parent::loadBody();
$this->body .= "Added Content for Xmas<br />";
}
}
$christmasEmail = new christmasEmail();
$christmasEmail->loadBody();
echo $christmasEmail->body;
Готови сме с новия код, но няколко дни по-късно идва Нова Година и на нас ни трябва още един дъщерен клас.
class newYearEmail extends eMailBody {
public function loadBody() {
parent::loadBody();
$this->body .= "Added Content for New Year<br />";
}
}
$newYearEmail = new newYearEmail();
$newYearEmail->loadBody();
echo $newYearEmail->body;
Какво става, ако по някаква причина искаме да добавим двете модификации в един-единствен имейл. Целта може да се постигне лесно, но ще трябва да се добавят още редове ненужен код. За щастие има алтернатива, която в някои случаи е много по-подходяща от множественото наследяване. Ако трябва да се добавят още повече отличителни характеристики, нашият код ще стане твърде претрупан. В такива ситуации използваме декоратори.
Да започнем с прост интерфейс, който да се имплементира в основния клас.
interface eMailBody {
public function loadBody();
}
Следващата стъпка е да добавим основния имейл клас, който ще използваме при всяко изпращане на съобщение.
class eMail implements eMailBody {
public function loadBody() {
echo "This is Main Email body.<br />";
}
}
За да променяме нещо по горния клас, без да променяме него, създаваме клас-декоратор, който пази референция към class eMail
. Дефинираме и един абстрактен метод, loadBody
, чрез който ще променяме основния клас.
abstract class emailBodyDecorator implements eMailBody {
protected $emailBody;
public function __construct(eMailBody $emailBody) {
$this->emailBody = $emailBody;
}
abstract public function loadBody();
}
Различните вариации се осъществяват чрез дъщерни класове-декоратори.
class christmasEmailBody extends emailBodyDecorator {
public function loadBody() {
echo 'This is Extra Content for Christmas<br />';
$this->emailBody->loadBody();
}
}
class newYearEmailBody extends emailBodyDecorator {
public function loadBody() {
echo 'This is Extra Content for New Year.<br />';
$this->emailBody->loadBody();
}
}
Как ще използваме всичко създадено до този момент.
/*
* Обикновен мейл.
*/
$email = new eMail();
$email->loadBody();
// Output
This is Main Email body.
/*
* Мейл с поздрав за Коледа
*/
$email = new eMail();
$email = new christmasEmailBody($email);
$email->loadBody();
// Output
This is Extra Content for Christmas
This is Main Email body.
/*
* Мейл с поздравление за Нова Година.
*/
$email = new eMail();
$email = new newYearEmailBody($email);
$email->loadBody();
// Output
This is Extra Content for New Year.
This is Main Email body.
/*
* Мейл с Коледен и Новогодишен поздрав
*/
$email = new eMail();
$email = new christmasEmailBody($email);
$email = new newYearEmailBody($email);
$email->loadBody();
// Output
This is Extra Content for New Year.
This is Extra Content for Christmas
This is Main Email body.
Ето как може да променяме функционалността на един клас, без да променяме самия него. [6]
|