Декоратор (на английски: Decorator) е структурен шаблон за дизайн, който се използва в обектно-ориентираното програмиране.

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

Същност редактиране

 
Основна схема на Декоратор модела

Този вид шаблон може да се използва за добавяне на функционалност към определен обект динамично, без това да засяга другите инстанции на същия клас.

Това се постига чрез създаването на нов клас Декоратор (англ. wrapper-опаковка), който „обвива“ класа. Това лесно може да се представи с примера, “опаковане на подарък, слагане на подаръка в кутия, опаковане на кутията“[1]. Създава се поредица от обекти, която започва с декоратор обектите, отговорни за новите функционалности, и завършва с оригиналния обект.[2].

Структура редактиране

 
UML – диаграма на Декоратор шаблон

Класовете и обектите, използвани при този модел са:

  • Компонент (англ. Component) – интерфейсът на обектите, към които могат да се добавят допълнителни функционалности или качества динамично;
  • Конкретен компонент (англ. ConcreteComponent) – обектът, към който ще се добавя нова функционалност;
  • Декоратор (англ. Decorator) – пази референция към компонент обекта и създава интерфейса, който съвпада с този на компонента;
  • Конкретен декоратор (англ. ConcreteDecorator) – прибавя нови функционалности към обекта[3];

Особеностите на декоратора (методи, свойства и други) обикновено се определят от интерфейса, който е еднакъв за декораторите и за декорираните обекти. В посочения пример Компонент класът е наследен едновременно от Конктерния компонент и от подкласовете на Декоратор класа (англ. ConcreteDecoratorA, англ. ConcreteDecoratorB).

Трябва да се отбележи, че декораторите и оригиналният обект имат общи свойства. В посочената диаграма примерният методdoThis() е достъпен и във вече декорираната, и в началната версия.

При този шаблон за дизайн няколко декоратора могат да бъдат използвани върху една инстанция на даден обект, като всеки от тях добавя нова функционалност.

Основание за използване редактиране

Като пример, нека вземем прозореца (англ. window) в една прозоречна система (англ.windowing system). За да се разреши скролирането (англ. scrolling) през съдържанието на даден прозорец може да му се добави хоризонтална или вертикална плъзгаща лента (англ. scrollbar)  в зависимост от прозореца. Нека приемем, че прозорците представляват инстанции (англ. instances) oт Прозоръчния клас (англ. Window class) и да приемем, че този клас няма функционалност (англ. functionality) за добавяне на плъзгащи ленти. В такъв случай, може да се създаде един подклас „ScrollingWindow” или „ScrollingWindowDecorator” , който добавя тази функционалност към вече съществуващия прозоречен обект (англ. Window object). На този етап и двете решения ще свършат работа.

 
UML – диаграма, пример за прозорец

Сега, нека приемем, че също желаем да добавим рамка (англ. borders) към този прозорец. Отново Прозоречният клас не поддържа такава функционалност. Подкласът „ ScrollingWindow” представлява проблем, защото ефективно създава нов вид прозорец. Ако трябва да се добави функционалност за рамки на много прозорци, но не и на всички, трябва да се създадат подкласове „WindowWithBorder” /прозорец с рамка/ и „ScrollingWindowWithBorder” /прозорец с плъзгаща лента и рамка/. Проблемът се влошава с добавянето на всяко едно допълнително свойство или прозоречен подтип. За декоративни решения трябва само да се добави „BorderedWindowDecorator” /декоратор на прозоречните рамки/, когато вече програмата е инициализирана (англ. runtime), може да се декорират съществуващите прозорци със „ScrollingWindowDecorator” или „BorderedWindowDecorator” , или и с двете, по преценка. Забележете, че ако се изисква да се добави една функционалност към всички прозорци, трябва само да се промени основния клас (англ. base class) . От друга страна , понякога / ако се използва външен софтуер (англ. framework)/ това е невъзможно да се направи, непозволено е или в случая е неподходящо да се промени основният клас.

Пояснение – в предишния пример, класовете „SimpleWindow” и „WindowDecoratorимплементират интерфейсът (англ. interface) на „Window”, което дефинира методите „draw()” и „getDescription()”, които са необходими в разиграния сценарий, за да се декорира прозоречен контрол.

Оценка редактиране

Декораторът е удобна алтернатива на наследяването. При наследяването добавянето на функционалност става при компилация и промените се отнасят за всички инстанции на оригиналния клас. Декораторът от своя страна добавя функционалност към даден обект динамично т.е по време на изпълнение на програмата, без да променя неговата структура.

Посочват се две главни предимства и два недостатъка на декоратор шаблона[4] :

  • Повече гъвкавост от колкото при статично наследяване. При някои обектно-ориентирани езици, създаването на класове по време на изпълнение е невъзможно и обикновено по време на компилация не може да се предвидят какви комбинации от нови функционалности ще са нужни. Това би означавало да се създава нов клас за всяка възможна комбинация. Това може да доведе до създаването на много класове и да усложни структурата на системата. Декораторите, от своя страна, са обекти, създадени по време на изпълнение на програмата и могат да се комбинират според всяка нужда.
  • Избягва поставянето на прекалено много свойства в класовете, които са по-високо в йерархията. Декораторът, както вече беше споменато, прибавя нужните функционалности и съответно исканите комбинации. Вместо да се предвидят и поддържат всички свойства и характеристики чрез сложна поредица от отделни класове, може да се създаде обикновен клас, към който последователно да се добавят свойства чрез декоратор класовете. Така даденото приложение не се усложнява с особености, от които няма нужда.
  • Декораторът и неговия компонент не са идентични. Декораторът е просто обвивка. От гледна точка на програмната логика, клас с добавен декоратор е различен от оригиналния клас. В този смисъл не може да се разчита на типа обект, когато се използват декоратори.
  • Наличието на много малки обекти. Дизайн, който използва декоратор, често съдържа множество малки обекти, които изглеждат по един и същи начин. Тези обекти се различават по начина, по който са свързани помежду си, а не по класа им или по стойностите на техните променливи. Оттук идва и трудността при изучаването и разбирането им.

Примери редактиране

C# редактиране

Пример за Декоратор [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
* /

Java редактиране

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() + ".";
 }
 }
}

/* Резултатът изписан на конзолата след изпълнението на програмата:
При оригиналния обект: Иван
При "обвитият" обект: Името е: Иван.
* /

PHP редактиране

Когато трябва да добавим уникални особености към даден клас, една добра алтернатива на наследяването е да използваме декоратор. Следващият пример обяснява какви са предимствата на втория подход.

Ето клас, който генерира съдържание за имейл. В долния блок ясно се вижда, че за генериране на стандартни съобщения, класът ще функционира добре.

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]

Източници редактиране

  1. Decorator Design Pattern
  2. Alan Shalloway, James R. Trott. Design Patterns Explained: A New Perspective on Object-Oriented Design. Reading, MA, Addison-Wesley Publishing Co, Inc., 2001. ISBN 0-201-71594-5.
  3. Gamma, Helm, Johnson, Erich. Design Patterns: Elements of Reusable Object-Oriented Software. Reading, MA, Addison-Wesley Publishing Co, Inc., 1995. ISBN 0-201-63361-2. с. 199.
  4. Gamma, Helm, Johnson, Erich. Design Patterns: Elements of Reusable Object-Oriented Software. Reading, MA, Addison-Wesley Publishing Co, Inc., 1995. ISBN 0-201-63361-2. с. 120.
  5. C# 3.0 Design Patterns – Judith Bishop
  6. PHP Design Patterns