زمان تخمینی مطالعه: 14 دقیقه

الگوی Decorator به کاربر اجازه می‌دهد تا عملکرد جدیدی را به یک شیء موجود بدون تغییر ساختار آن اضافه کند. این نوع الگوی طراحی تحت الگوهای ساختاری قرار می‌گیرد زیرا این الگو به عنوان یک پوشش برای یک کلاس موجود عمل می‌کند. این الگو یک کلاس دکوراتور ایجاد می‌کند که کلاس اصلی را می‌پوشاند و عملکرد اضافی را ارائه می‌کند تا امضای متدهای کلاس را دست نخورده نگه دارد.

Decorator design pattern

بیان مسئله: تصور کنید که روی یک کتابخانه notification(اعلان) کار می‌کنید که به برنامه‌های دیگر اجازه می‌دهد تا کاربران خود را در مورد رویدادهای مهم مطلع کنند. نسخه اولیه کتابخانه مبتنی بر کلاس Notifier است که فقط چند فیلد، سازنده و یک متد send واحد دارد. این متد می‌تواند یک آرگومان پیام را از یک کلاینت بپذیرد و پیام را به لیستی از ایمیل‌هایی که از طریق سازنده آن به اطلاع‌دهنده(notifier) ارسال شده‌اند ارسال کند. یک برنامه شخص ثالث که به عنوان یک کلاینت عمل می‌کرد قرار بود یک بار شی اطلاع‌دهنده(notifier) را ایجاد و پیکربندی کند و سپس هر بار که اتفاق مهمی رخ می‌داد از آن استفاده کند.

Structure of the library before applying the Decorator pattern
یک برنامه می‌تواند از کلاس اطلاع‌دهنده(notifier) برای ارسال اعلان‌ها در مورد رویدادهای مهم به مجموعه‌ای از ایمیل‌های از پیش تعریف شده استفاده کند.

در برخی مواقع متوجه می‌شوید که کاربران کتابخانه انتظار بیشتری از اعلان‌های ایمیلی دارند. بسیاری از آنها مایلند پیامکی در مورد مسائل مهم دریافت کنند. دیگران مایلند در فیس بوک مطلع شوند و البته، کاربران شرکتی دوست دارند اعلان های Slack را دریافت کنند.

Structure of the library after implementing other notification types
هر نوع اعلان به عنوان زیرکلاس اطلاع‌دهنده(notifier) پیاده‌سازی می‌شود.

چقدر می‌تواند این کار دشوار باشد؟ شما کلاس Notifier را گسترش دادید و متدهای اعلان اضافی را در زیر کلاس‌های جدید قرار دادید. حال قرار بود کلاینت کلاس اعلان مورد نظر را نمونه سازی کند و از آن برای تمام اعلان‌های بعدی استفاده کند. اما اینجا یک سوال منطقی وجود دارد: «چرا نمی توان از چندین نوع اعلان به طور همزمان استفاده کرد؟»

برای حل این مشکل می‌توان با ایجاد زیر کلاس‌های ویژه که چندین روش اعلان را در یک کلاس ترکیب می‌کنند، آن مشکل را برطرف کرد. ولی با این حال، به سرعت آشکار می‌گردد که این رویکرد نه تنها کد کتابخانه بلکه کد مشتری را نیز به شدت مخدوش می‌کند.

Structure of the library after creating class combinations
انفجار ترکیبی زیر کلاس‌ها.

برای حل مشکل ایجاد شده شما باید راه دیگری برای ساختاربندی کلاس‌های اعلان پیدا کنید تا تعداد آنها به طور تصادفی بسیار زیاد نشوند.

زمانی که باید رفتار یک شی را تغییر دهید، گسترش یک کلاس اولین چیزی است که به ذهن خطور می‌کند. با این حال، مفهوم وراثت چندین اخطار جدی دارد که باید از آنها آگاه باشید.

یکی از راه‌های غلبه بر این اخطارها استفاده از Aggregation یا Composition به جای Inheritance است. هر دو گزینه تقریباً به یک شکل عمل می‌کنند: یک شی ارجاع به شی دیگر دارد و کاری را به آن واگذار می‌کند، در حالی که با وراثت، شی خود قادر به انجام آن کار است و رفتار را از ابرکلاس خود به ارث می‌برد. با این رویکرد جدید می‌توانید به راحتی شی «helper» مرتبط را با دیگری جایگزین کنید و رفتار کانتینر را در زمان اجرا تغییر دهید. یک شی می‌تواند از رفتار کلاس‌های مختلف استفاده کند، به چندین شیء ارجاع داشته باشد و انواع کارها را به آنها واگذار کند. تجمع/ترکیب، اصل کلیدی پشت بسیاری از الگوهای طراحی، از جمله الگوی Decorator است.

Inheritance vs. Aggregation
وراثت در مقابل تجمیع.

“Wrapper” نام مستعار جایگزین برای الگوی Decorator است که به وضوح ایده اصلی الگو را بیان می‌کند. Wrapper یک شی است که می‌تواند با یک شی هدف مرتبط شود. Wrapper شامل همان مجموعه‌ای از متدها به عنوان هدف است و تمام درخواست‌هایی را که دریافت می‌کند به آن تفویض می‌کند. با این حال، wrapper ممکن است با انجام کاری قبل یا بعد از ارسال درخواست به هدف، نتیجه را تغییر دهد.

چه زمانی یک پوشاننده(wrapper) ساده تبدیل به دکوراتور واقعی می‌شود؟ همانطور که اشاره کردم، wrapper همان رابط کاربری شی پوشیده شده را اجرا می‌کند. به همین دلیل است که از دیدگاه مشتری، این اشیاء یکسان هستند. کاری کنید که فیلد مرجع wrapper هر شیئی را که از آن اینترفیس پیروی می‌کند بپذیرد. این موضوع به شما امکان می‌دهد یک شی را در چند پوشش بپوشانید و رفتار ترکیبی همه پوشش‌ها را به آن اضافه کنید.

در مثال نوتیفیکیشن، بیایید رفتار اعلان ایمیل ساده را در کلاس Notifier پایه بگذاریم، اما سایر روش‌های اعلان را به دکوراتور تبدیل کنیم.

The solution with the Decorator pattern
متدهای مختلف اطلاع رسانی تبدیل به دکوراتور می‌شوند.

کد مشتری باید یک شی notifier اصلی را در مجموعه‌ای از دکوراتورها بپیچد که با ترجیحات مشتری مطابقت دارد. اشیاء به دست آمده به عنوان یک پشته ساختار خواهند داشت.

Apps might configure complex stacks of notification decorators
برنامه‌ها ممکن است پشته‌های پوشیده شده دکوراتور اعلان را پیکربندی کنند.

آخرین دکوراتور در پشته، شیئی است که مشتری در واقع با آن کار می‌کند. از آنجایی که همه دکوراتورها اینترفیس یکسانی را با اعلان‌کننده پایه پیاده‌سازی می‌کنند، بقیه کد کلاینت اهمیتی نمی‌دهد که با شی اعلان‌کننده «خالص» یا تزئین شده(decorate) کار کند. ما می‌توانیم همین رویکرد را برای متدهای دیگر مانند قالب‌بندی پیام‌ها یا تهیه فهرست گیرندگان اعمال کنیم. مشتری می‌تواند شی را با هر دکوراتور سفارشی تزئین کند، به شرطی که از رابط کاربری مشابه دیگران پیروی کند.

نمونه قابل قیاس در دنیای واقعی

پوشیدن لباس نمونه‌ای از استفاده از دکوراتورها است. وقتی سردمان می‌شود، خودمان را در یک پلیور می‌پیچیم. اگر با ژاکت هنوز سردتان است، می‌توانید یک ژاکت روی آن بپوشید. اگر باران می‌بارد، می‌توانید یک کت بارانی بپوشید. همه این لباس‌ها رفتارهای اصلی شما را «توسعه» می‌دهند، اما بخشی از شما نیستند، و می‌توانید هر زمان که به آن نیاز ندارید، به راحتی هر لباسی را در بیاورید.

Example of the Decorator pattern
با پوشیدن چند تکه لباس، جلوه‌ای ترکیبی به دست می‌آورید.

ساختار الگوی Decorator

در این بخش به بررسی ساختار الگوی دکوراتور(Decorator) می‌پردازیم و پیاده‌سازی آن را به صورت UML خواهیم دید. 

شبه کد(Pseudocode)

در این مثال، الگوی Decorator به شما امکان می‌دهد داده‌های حساس را مستقل از کدی که در واقع از این داده‌ها استفاده می‌کند فشرده و رمزگذاری کنید.

Structure of the Decorator pattern example
مثال دکوراتورهای رمزگذاری و فشرده‌سازی.

برنامه، شی منبع داده را با یک جفت دکوراتور می‌پوشاند. هر دو wrapper نحوه نوشتن و خواندن داده‌ها از دیسک را تغییر می‌دهند:

دکوراتورها و کلاس منبع داده، یک اینترفیس را پیاده‌سازی می‌کنند که همه آنها را در کد مشتری قابل تعویض می‌کند.

// The component interface defines operations that can be
// altered by decorators.
interface DataSource is
    method writeData(data)
    method readData():data

// Concrete components provide default implementations for the
// operations. There might be several variations of these
// classes in a program.
class FileDataSource implements DataSource is
    constructor FileDataSource(filename) { ... }

    method writeData(data) is
        // Write data to file.

    method readData():data is
        // Read data from file.

// The base decorator class follows the same interface as the
// other components. The primary purpose of this class is to
// define the wrapping interface for all concrete decorators.
// The default implementation of the wrapping code might include
// a field for storing a wrapped component and the means to
// initialize it.
class DataSourceDecorator implements DataSource is
    protected field wrappee: DataSource

    constructor DataSourceDecorator(source: DataSource) is
        wrappee = source

    // The base decorator simply delegates all work to the
    // wrapped component. Extra behaviors can be added in
    // concrete decorators.
    method writeData(data) is
        wrappee.writeData(data)

    // Concrete decorators may call the parent implementation of
    // the operation instead of calling the wrapped object
    // directly. This approach simplifies extension of decorator
    // classes.
    method readData():data is
        return wrappee.readData()

// Concrete decorators must call methods on the wrapped object,
// but may add something of their own to the result. Decorators
// can execute the added behavior either before or after the
// call to a wrapped object.
class EncryptionDecorator extends DataSourceDecorator is
    method writeData(data) is
        // 1. Encrypt passed data.
        // 2. Pass encrypted data to the wrappee's writeData
        // method.

    method readData():data is
        // 1. Get data from the wrappee's readData method.
        // 2. Try to decrypt it if it's encrypted.
        // 3. Return the result.

// You can wrap objects in several layers of decorators.
class CompressionDecorator extends DataSourceDecorator is
    method writeData(data) is
        // 1. Compress passed data.
        // 2. Pass compressed data to the wrappee's writeData
        // method.

    method readData():data is
        // 1. Get data from the wrappee's readData method.
        // 2. Try to decompress it if it's compressed.
        // 3. Return the result.


// Option 1. A simple example of a decorator assembly.
class Application is
    method dumbUsageExample() is
        source = new FileDataSource("somefile.dat")
        source.writeData(salaryRecords)
        // The target file has been written with plain data.

        source = new CompressionDecorator(source)
        source.writeData(salaryRecords)
        // The target file has been written with compressed
        // data.

        source = new EncryptionDecorator(source)
        // The source variable now contains this:
        // Encryption > Compression > FileDataSource
        source.writeData(salaryRecords)
        // The file has been written with compressed and
        // encrypted data.


// Option 2. Client code that uses an external data source.
// SalaryManager objects neither know nor care about data
// storage specifics. They work with a pre-configured data
// source received from the app configurator.
class SalaryManager is
    field source: DataSource

    constructor SalaryManager(source: DataSource) { ... }

    method load() is
        return source.readData()

    method save() is
        source.writeData(salaryRecords)
    // ...Other useful methods...


// The app can assemble different stacks of decorators at
// runtime, depending on the configuration or environment.
class ApplicationConfigurator is
    method configurationExample() is
        source = new FileDataSource("salary.dat")
        if (enabledEncryption)
            source = new EncryptionDecorator(source)
        if (enabledCompression)
            source = new CompressionDecorator(source)

        logger = new SalaryManager(source)
        salary = logger.load()
    // ...

قابلیت‌ها و کاربرد‌ها

نحوه پیاده‌سازی

  1. مطمئن شوید که دامنه کسب و کار شما می‌تواند به عنوان یک مؤلفه اصلی با چندین لایه اختیاری روی آن نمایش داده شود.
  2. دریابید که چه متدهایی برای کامپوننت اصلی و لایه‌های اختیاری مشترک است. یک اینترفیس کامپوننت ایجاد کنید و آن متدها را در آنجا تعریف کنید.
  3. یک کلاس جزء concrete ایجاد کنید و رفتار پایه را در آن تعریف کنید.
  4. یک کلاس دکوراتور پایه ایجاد کنید. باید یک فیلد برای ذخیره ارجاع به یک شی پیچیده داشته باشد. فیلد باید با نوع اینترفیس مؤلفه تعریف شود تا امکان اتصال به اجزای concrete و همچنین decorator وجود داشته باشد. دکوراتور پایه باید تمام کارها را به شی پوشیده شده واگذار کند.
  5. مطمئن شوید که همه کلاس‌ها اینترفیس کامپوننت را پیاده‌سازی می‌کنند.
  6. دکوراتورهای concrete را با گسترش آنها از دکوراتور پایه ایجاد کنید. یک دکوراتور concrete باید رفتار خود را قبل یا بعد از فراخوانی روش والد (که همیشه به شی پوشیده شده واگذار می‌کند) اجرا کند.
  7. کد مشتری باید مسئول ایجاد دکوراتورها و ترکیب آنها به روش مورد نیاز مشتری باشد.

مزایا و معایب الگوی Decorator

این الگوی طراحی دارای مزایا و معایبی به شرح زیر است:

– مزایا

– معایب

ارتباط با الگوهای دیگر

کنترل کننده‌های CoR می‌توانند عملیات دلخواه را مستقل از یکدیگر اجرا کنند. آنها همچنین می‌توانند ارسال درخواست را در هر نقطه‌ای متوقف کنند. از سوی دیگر، دکوراتورهای مختلف می‌توانند رفتار شی را گسترش دهند و در عین حال آن را با اینترفیس پایه سازگار نگه دارند. علاوه بر این، دکوراتورها مجاز به شکستن جریان درخواست نیستند.

الگوی Decorator مانند الگوی Composite است اما فقط یک جزء فرزند دارد. یک تفاوت مهم دیگر وجود دارد: دکوراتور مسئولیت‌های بیشتری را به شی پوشیده شده اضافه می‌کند، در حالی که Composite فقط نتایج فرزندان خود را “خلاصه” می‌کند. با این حال، الگوها همچنین می‌توانند همکاری کنند: می‌توانید از Decorator برای گسترش رفتار یک شی خاص در درخت ترکیبی استفاده کنید.

نمونه کد الگوی Decorator

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *