الگوی آذینگر
در برنامهنویسی شئ گرا، الگوی آذینگر یا دکوراتر (decorator) یک الگوی طراحی است که امکان افزودن رفتار (behavior) به یک شئ، را بهطور پویا (dynamic) یا ایستا (static) فراهم میسازد بی آنکه رفتار اشیاء دیگر از همان کلاس (که شئ مورد بحث از آن ساخته شده) دستخوش تغییر شوند.[1] الگوی طراحی آذینگر معمولاً برای پایبندی به قاعدهٔ تک وظیفهای مورد استفاده قرار میگیرد چرا که این الگوی طراحی، امکان تقسیم عملکردها (functionality) بین کلاسهای مختلف که هر کدام دغدغههای (concern) خاص را پوشش میدهند، فراهم میسازد.[2]
هدف
به منظور گسترش (آذین کردن) عملکرد یک شئ به صورت ایستا یا بعضی مواقع به صورت پویا، در زمان اجرا(Runtime) و بهطور مستقل از اشیاء دیگر همان کلاس، از الگوی طراحی آذینگر استفاده میشود. این مهم از طریق ایجاد یک کلاس آذینگر یا کلاس پیچه، که کلاس اصلی (کلاسی که میخواهیم عملکردش را آذین کنیم)، در آن پیچیده میگردد، برقرار میشود. این عمل پیچیدن کلاس اصلی در کلاس آذینگر از طریق مراحل زیر انجام میشود:
- ایجاد یک زیر کلاسِ آذینگر از کلاس اصلی
- افزودن یک اشارهگر به صورت فیلد (field) که به کلاس اصلی اشاره میکند.
- ارسال یک شئ از طریق سازنده (constructor) به فیلد برای مقدار دهی به اشارهگر.
- در کلاس آذینگر از تمام متدهای(Method) کلاس آذینشده یا همان کلاس اصلی باید استفاده کرد.
- در کلاس آذینگر، هر متد از کلاس اصلی را که میبایست عملکردش اصلاح شود (گسترش یابد) باید override کرد.
این الگو طراحی شدهاست تا چندین آذینگر به راحتی بتوانند برهم سوار شوند (دور هم بپیچند) و هر بار که یک آذینگر به دور آذینگر دیگر میپیچد، یک عملکرد جدید به متدهای override شده افزوده میشود.
باید دقت شود که آذینگرها و کلاس اصلی مجموعهای از ویژگیها را به اشتراک میگذراند. در شکل قبل، اسلوب (متد) operation() هم در کلاسهای آذینشده و هم درکلاسهای آذین نشده حضور دارد.
اجزاء آذین (مانند اسلوبها (متدها)، فیلدها یا اعضای دیگر) معمولاً از طریق یک رابط یا همان اینترفیس(interface)، یا از طریق حواله (استفاده از شئ یک کلاس در شئ کلاس دیگر)، یا ارثبَری از یک کلاس، که هم اشیاء آذینگر و هم اشیاء آذینشده از آنها استفاده میکنند، تعریف میشوند. در مثال قبلی کلاس ConcreteComponent و کلاس Decorator و تمامی کلاسهایی که از آن ارث میبرند، از کلاس Component نیز ارث میبرند.
الگوی طراحی آذینگر جایگزینی برای ارثبری از طریق زیر کلاس شدن، میباشد. وراثت، در زمان کامپایل رفتار(behavior) را به یک کلاس و متعاقباً اشیاء آن کلاس اضافه میکند ولی آذینگری، امکان افزودن رفتارهای جدید را در زماناجرا آنهم برای اشیاء دلخواه برنامهنویس فراهم میسازد.
در بسیاری از زبانهای برنامهنویسی شئگرا، امکان ایجاد کلاسها در زمان اجرا وجود ندارد و در بسیاری از مواقع پیشبینی تمامی حالات توسعهٔ عملکرد (functionality extension) دشوار است. توسعهٔ عملکرد به معنای ایجاد یک کلاس برای هر ترکیب ممکن میباشد. آذینگرها اشیائی هستند که در زماناجرا ساخته میشوند، و میتوان آنها را با یکدیگر ترکیب کرد. در جاوا و داتنت برای پیادهسازی جریان ورودی/خروجی(I/O Streams)، از الگوی آذینگر استفاده میشود. en:Decorator pattern
شرح مسئله
به عنوان مثال، یک پنجره را در سامانهٔ پنجرهای در نظر بگیرید. برای ایجاد قابلیت اسکرول کردن نوار نورد برای پنجرهها ممکن است بخواهیم نوار نورد افقی یا عمودی ایجاد کنیم. فرض کنید که به ازای هر پنجره یک شئ از کلاس Window داریم (این کلاس موجبات به نمایش درآمدن و عمل کردن پنجره را در برنامه فراهم میکند) و فرض کنید که این کلاس عملکردی برای افزودن نوار نورد ندارد. ممکن است برنامهنویسی برای افزودن این قابلیت به برنامه کلاسی با نام ScrollingWindow به وجود آورد که از کلاس Windows فرزند این کلاس است و از این کلاس ارث میبرد، یا اینکه فرض کنید برنامهنویس به جای اینکار کلاسی با نام
ScrollingWindowDecorator میسازد که به اشیاء کلاس Window قابلیت جدید مربوط به نوار نبرد را میافزاید. همانطور که میبینیم، تا اینجای کار هر دو روش، صحیح و کارآمد هستند.
حالا، فرض کنید میخواهیم به پنجرهها حاشیه اضافه کنیم، در این موارد هم فرض کنید که کلاس Windows قابلیت اینکار را ندارد. حالا با طرح این قابلیت جدید روش ارثبری دچار مشکل میشود چرا که فرض کنید کسی میخواهد بعضی پنجرهها دارای حاشیه و دارای قابلیت اسکرول باشند و بعضی فقط دارای قابلیت اسکرول باشند و بعضی دیگر فقط دارای حاشیه باشند در اینصورت اگر بخواهیم از روش ارثبری استفاده کنیم باید کلاسهای زیر را به بسازیم: WindowWithBorder و ScrollingWindowsWithBorder (هر دوی این کلاسها فرزند کلاس Window هستند) در این صورت میتوان پیشبینی کرد که با افزودن هر قابلیت جدید باید چندین زیر کلاس از کلاس Window بسازیم. برای حل این مشکل میتوان به جای روش ارثبری از الگوی طراحی آذینگر استفاده کرد. در این روش به ازای هر قابلیت جدید (قابلیت اسکرول، قابلیت داشتن حاشیه و …) فقط لازم است، یک کلاس آذینگر درست کنیم. مثلاً در این مثال میتوانیم دو کلاس آذینگر با نامهای BorderedWindowDecorator و ScrollingWindowDecorator بسازیم و با استفاده از این دو کلاس در زماان اجرا اشیاء از کلاس Widnow را آذین کنیم. دقت کنید کنید که در صورتی که این قابلیتها باید به تمام پنجرهها افزوده شود میتوان، کلاس والد یعنی Window را تغییر داد تا با نیازهای ما همخوانی داشته باشد. از طرف دیگر گاهی (مثلاً زمانی که از چارچوبهای (Framework) خارجی استفاده میکنیم)، تغییر و دست بردن در کلاس والد قانونی و راحت نیست، بنابراین در این مواقع استفاده از این الگوی طراحی میتواند کمک بزرگی به برنامهنویس باشد.
توجه داشته باشید که در مثال پنجره که آورده شد، کلاس SimpleWindow و دو کلاس آذینگر رابط window را که دارای دو اسلوب (متد) draw() و getDescription() است، پیادهسازی میکنند.
مثالها
مثال اول (پنجره/نوار نبرد)
کلاس زیر نحوهٔ استفاده از آذینگر را در مثال پنجره/نوار نبرد، نشان میدهد.
// The Window interface class
public interface Window {
public void draw(); // برای کشیدن پنجره
public String getDescription(); // رشتهای حاوی توضیح در مورد قابلیتها پنجره را برمیگرداند
}
// پیادهسازی رابط فوق به صورت یک پنجرهٔ بدون نوار نبرد
class SimpleWindow implements Window {
public void draw() {
// کدهای مربوط به کشیدن پنجره
}
public String getDescription() {
return "simple window";
}
}
کلاسهای زیر آذینگرهای کلاس Window و کلاسهای آذینگر دیگر، هستند.
// کلاس تجریدی آذینگر، دقت کنید که رابط ویندو را یپادهسازی میکند
abstract class WindowDecorator implements Window {
protected Window windowToBeDecorated; // the Window being decorated
public WindowDecorator (Window windowToBeDecorated) {
this.windowToBeDecorated = windowToBeDecorated;
}
public void draw() {
windowToBeDecorated.draw(); //Delegation
}
public String getDescription() {
return windowToBeDecorated.getDescription(); //Delegation
}
}
// The first concrete decorator which adds vertical scrollbar functionality
class VerticalScrollBarDecorator extends WindowDecorator {
public VerticalScrollBarDecorator (Window windowToBeDecorated) {
super(windowToBeDecorated);
}
@Override
public void draw() {
super.draw();
drawVerticalScrollBar();
}
private void drawVerticalScrollBar() {
// Draw the vertical scrollbar
}
@Override
public String getDescription() {
return super.getDescription() + ", including vertical scrollbars";
}
}
// The second concrete decorator which adds horizontal scrollbar functionality
class HorizontalScrollBarDecorator extends WindowDecorator {
public HorizontalScrollBarDecorator (Window windowToBeDecorated) {
super(windowToBeDecorated);
}
@Override
public void draw() {
super.draw();
drawHorizontalScrollBar();
}
private void drawHorizontalScrollBar() {
// Draw the horizontal scrollbar
}
@Override
public String getDescription() {
return super.getDescription() + ", including horizontal scrollbars";
}
}
در ذیل این مطلب برنامهای آورده شدهاست که در آن یک نمونه (شئ) از کلاس SimpleWindow ساخته میشود و این کلاس با استفاده از کلاسهای آذینگر با نامهای HorizontalScrollBarDecorator و VerticalScrollBarDecorator آذینمیشود.
public class DecoratedWindowTest {
public static void main(String[] args) {
// Create a decorated Window with horizontal and vertical scrollbars
Window decoratedWindow = new HorizontalScrollBarDecorator (
new VerticalScrollBarDecorator (new SimpleWindow()));
// Print the Window's description
System.out.println(decoratedWindow.getDescription());
}
}
در زیر هم کلاس بررسی صحت با استفاده از Junit آورده شدهاست:
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class WindowDecoratorTest {
@Test
public void testWindowDecoratorTest() {
Window decoratedWindow = new HorizontalScrollBarDecorator(new VerticalScrollbarDecorator(new SimpleWindow()));
// assert that the description indeed includes horizontal + vertical scrollbars
assertEquals("simple window, including vertical scrollbars, including horizontal scrollbars", decoratedWindow.getDescription())
}
}
خروجی برنامه متن زیر است:
"simple window, including vertical scrollbars, including horizontal scrollbars"
به این نکته توجه کنید که متد (اسلوب) getDescription() از دو کلاس آذینگر که در مثال آمدهاند، ابتدا getDescription() پنجرهٔ اصلی یعنی همان شئ از کلاس SimpleWindow را صدا میزند و پس از آن Description خود را پس از آن اضافه میکنند.
مثال دوم (آماده کردن قهوه)
این مثال جاوا، کاربرد آذینگرها را در مثال آمادهکردن قهوه نشان میدهد. در این مثال برای آماده کردن قهوه فقط قیمت و مخلفات قهوه مدنظر هستند.
// The interface Coffee defines the functionality of Coffee implemented by decorator
public interface Coffee {
public double getCost(); // Returns the cost of the coffee
public String getIngredients(); // Returns the ingredients of the coffee
}
// Extension of a simple coffee without any extra ingredients
public class SimpleCoffee implements Coffee {
@Override
public double getCost() {
return 1;
}
@Override
public String getIngredients() {
return "Coffee";
}
}
کلاسهای زیر آذینگرها هستند
// Abstract decorator class - note that it implements Coffee interface
public abstract class CoffeeDecorator implements Coffee {
protected final Coffee decoratedCoffee;
public CoffeeDecorator(Coffee c) {
this.decoratedCoffee = c;
}
public double getCost() { // Implementing methods of the interface
return decoratedCoffee.getCost();
}
public String getIngredients() {
return decoratedCoffee.getIngredients();
}
}
// Decorator WithMilk mixes milk into coffee.
// Note it extends CoffeeDecorator.
class WithMilk extends CoffeeDecorator {
public WithMilk(Coffee c) {
super(c);
}
public double getCost() { // Overriding methods defined in the abstract superclass
return super.getCost() + 0.5;
}
public String getIngredients() {
return super.getIngredients() + ", Milk";
}
}
// Decorator WithSprinkles mixes sprinkles onto coffee.
// Note it extends CoffeeDecorator.
class WithSprinkles extends CoffeeDecorator {
public WithSprinkles(Coffee c) {
super(c);
}
public double getCost() {
return super.getCost() + 0.2;
}
public String getIngredients() {
return super.getIngredients() + ", Sprinkles";
}
}
در ذیل این متن مثالی برای بررسی صحت برنامهٔ فوق بیان شدهاست در این مثال یک نمونه از کلاس Coffee ساخته شده و با آذینگرهای WithMilk و WithSprinkles آذین میشود و قیمت قهوهٔ تولید شده به همراه مخلفات (شیر و …) را محاسبه کرده و مخلفات آن را نمایش میدهد.
public class Main {
public static void printInfo(Coffee c) {
System.out.println("Cost: " + c.getCost() + "; Ingredients: " + c.getIngredients());
}
public static void main(String[] args) {
Coffee c = new SimpleCoffee();
printInfo(c);
c = new WithMilk(c);
printInfo(c);
c = new WithSprinkles(c);
printInfo(c);
}
}
خروجی این کد فوق:
Cost: 1.0; Ingredients: Coffee
Cost: 1.5; Ingredients: Coffee, Milk
Cost: 1.7; Ingredients: Coffee, Milk, Sprinkles
منابع
- Gamma، Erich (۱۹۹۵). Design Patterns. Addison-Wesley Publishing Co, Inc. صص. ۱۷۵ff. شابک ۰-۲۰۱-۶۳۳۶۱-۲.
- «How to Implement a Decorator Pattern». ۲۰۱۵-۰۷-۰۷. بایگانیشده از اصلی در ۷ ژوئیه ۲۰۱۵. دریافتشده در ۲۷ مه ۲۰۱۹.
- مشارکت کنندگان ویکیپدیا (Decorator pattern)، در دانشنامه ویکیپدیای انگلیسی. بازبینی در ۲۷ می ۲۰۱۹.