نمط المزخرِف

من موسوعة حسوب

نمط المزخرِف هو نمط تصميم هيكلي يضيف سلوكيات جديدة إلى الكائنات بوضعها داخل كائنات تغليف خاصة تحتوي تلك السلوكيات.

المشكلة

ضع الصورة. يستطيع برنامج أن يستخدم فئة المنبه لإرسال إشعارات عن الأحداث المهمة إلى قائمة عناوين بريدية.

تخيل أنك تعمل على مكتبة إشعارات تسمح للبرامج الأخرى بتنبيه مستخدميها إلى الأحداث المهمة، وبنيت النسخة الأولية من المكتبة على فئة Notifier بها بضعة حقول قليلة، ومنشئ (Constructor) وأسلوب Send وحيد، ويستطيع الأسلوب قبول وسيط في هيئة رسالة من عميل ويرسلها إلى قائمة من عناوين البريد التي يمررها المنشئ إلى المنبه (Notifier).

ويفترض بتطبيق من طرف ثالث يتصرف كعميل أن ينشئ ويهيئ كائن المنبه مرة واحدة ثم يستخدمه في كل مرة يحدث شيء مهم، وستدرك في مرحلة ما أن مستخدمي المكتبة ينتظرون أكثر من مجرد إشعارات بريدية، فكثير منهم يريدون رسائل نصية قصيرة (SMS) عن المشاكل الحرجة التي لا تحتمل الانتظار، ويود غيرهم أن يُنبَّهوا على فيس بوك، وكذلك بالمثل للموظفين الذين يرغبون في تلقي رسائل على سلاك (Slack).

ضع الصورة. يُستخدم كل نوع تنبيه كفئة منبه فرعية.

لهذا توسِّع فئة Notifier وتضع أساليب تنبيه إضافية في فئات فرعية، ويفترض بالعميل أن يعرف فئة التنبيه المطلوبة ويستخدمها لكل الإشعارات التالية، وستدرك أنك ستحتاج في الحالات الحرجة إلى إشعار المستخدم في كل القنوات التي هو فيها -فيس بوك، بريد، سلاك، ...- والطريقة المباشرة لفعل ذلك هي إنشاء فئات فرعية خاصة تجمع عدة أساليب إشعارات في فئة واحدة، لكن عيبها أن الشيفرة ستصبح ضخمة، سواء شيفرة المكتبة أو العميل، ونريد أن نجد طريقة أفضل لهيكلة فئات الإشعارات بحيث لا تكبر أعدادها بشكل زائد عن الحد.

ضع الصورة. انفجار الفئات الفرعية.

الحل

رغم أن توسعة الفئة هي أول ما يرد للذهن عند تغيير سلوك كائن ما إلا أن الاكتساب (Inheritance) له مؤشرات خطر يجب أن تدركها:

  • الاكتساب ساكن، فلا يمكنك تغيير سلوك كائن موجود فعلًا في وقت التشغيل (run time) ولا يمكنك فعل شيء سوى استبدال الكائن بكامله بواحد أنشاته من فئة فرعية.
  • لا يمكن أن يكون للفئات الفرعية سوى فئة أم وحيدة، ذلك أن الاكتساب لا يسمح في أغلب اللغات للفئة أن تكتسب سلوكيات من عدة فئات في نفس الوقت.

وإحدى الطرائق التي تتغلب بها على أوجه القصور تلك هي استخدام التركيب (Composition) بدلًا من الاكتساب، فالتركيب يجعل للكائن مرجعًا إلى كائن آخر يفوض إليه بعض المهام، في حين أنه مع الاكتساب يكون الكائن قادرًا على تنفيذ تلك المهام مكتسبًا السلوك من الفئة الأم. تستطيع كذلك استبدال كائن المساعد (Helper) بغيره، لتغير بذلك سلوك الحاوية أثناء وقت التشغيل، ويستطيع الكائن استخدام السلوك من فئات مختلفة ويحمل مراجع إلى عدة كائنات يفوض إليها المهام. ويُعد التركيب هو المبدأ الأساسي خلف عدة أنماط تصميم بما فيها المزخرِف.

ضع الصورة. الاكتساب مقابل التركيب.

ولنمط المزخرف اسم آخر يصف وظيفة النمط بوضوح، وهو "المغلِّف"، إذ أن المغلِّف هو كائن يمكن ربطه بكائن "هدف"، ويحتوي المغلِّف على نفس الأساليب التي في الكائن الهدف، ويفوِّض إليه كل الطلبات التي يستلمها، لكن قد يغير المغلِّف النتيجة بشيء ما قبل أو بعد تمريره الطلب إلى الهدف.

يستخدم المغلِّف نفس الواجهة كالكائن الذي يتم تغليفه (المغلَّف)، لهذا تبدو تلك الكائنات متطابقة من وجهة نظر العميل، ولكي تغطي كائنًا ما بعدة مغلِّفات مضيفًا سلوكًا مجمعًا من كل المغلِّفات إليه، يجب أن تجعل الحقل المرجعي للمغلِّف يقبل أي كائن يتبع تلك الواجهة. وفي مثال الإشعارات الذي تقدم سنترك سلوك إشعارات البريد البسيط داخل فئة Notifier الأساسية، لكن سنحول كل أساليب الإشعار الأخرى إلى مزخرفات (Decorators).

ضع الصورة. عدة أساليب إشعار تصبح مزخرِفات.

ستحتاج شيفرة العميل أن تغلف كائن إشعار بسيط بمجموعة مزخرفات تطابق تفضيلات العميل، وستهيكَل الكائنات في هيئة مكدَّس (Stack).

ضع الصورة. قد تهيئ التطبيقات تكديسات معقدة من مزخرفات الإشعارات.

وسيكون آخر مزخرِف في المكدس هو الكائن الذي يعمل معه العميل، وبما أن كل المزخرِفات تستخدم نفس الواجهة التي يستخدمها المنبه الأساسي (Base Notifier)، فإن شيفرة العميل لن يهمها إن كانت تعمل مع كائن منبه "نقي" أو مزخرَف، ونستطيع تطبيق نفس المنظور على سلوكيات أخرى مثل تنسيق الرسائل أو تنظيم قائمة المستلمين، ويستطيع العميل زخرفة الكائن بأي مزخرِفات خاصة طالما أنها تتبع نفس الواجهة كالباقين.

مثال واقعي

ضع الصورة. تحصل على أثر مجمع من ارتداء الملابس.

ارتداء الملابس مثال على استخدام المزخرِفات، فأنت تغلف نفسك بكنزة مثلًا طلبًا للدفء في البرد اليسير، ثم تضيف فوقها معطفًا إن اشتد البرد، أما إن كانت تمطر فستلبس معطفًا خاصًا بالمطر، وكل تلك القطع توسع سلوكك الأساسي لكنها ليست جزءًا منك، وتستطيع خلع أي قطعة ملابس في أي وقت تشعر فيه أنك لم تحتاج إليها.

البُنية

ضع الصورة.

  1. يصرح الجزء (Component) عن واجهة مشتركة لكل من المغلِّفات والكائنات المغلَّفة.
  2. الجزء الحقيقي (Concrete Component) هو فئة الكائنات يتم تغليفها، وتعرِّف السلوك الأساسي الذي يمكن تغييره بواسطة المزخرِفات.
  3. فئة المزخرف الأساسي (Base Decorator) لها حقل للإشارة إلى كائن مغلَّف، ويجب أن يصرَّح عن نوع الحقل على أنه واجهة الجزء كي يستطيع احتواء كلًا من الأجزاء الحقيقية والمزخرِفات. ويفوِّض المزخرِف الأساسي العمليات كلها إلى الكائن المغلَّف.
  4. تعرِّف المزخرِفات الحقيقية (Concrete Decorators) سلوكيات إضافية يمكن أن تضاف إلى الأجزاء بديناميكية، وتتخطى المزخرِفات الحقيقية أساليب المزخرِف الأساسي وتنفذ أسلوبها قبل أو بعد استدعاء الأسلوب الأم (Parent Behavior).
  5. العميل (Client) يستطيع تغليف الأجزاء في طبقات متعددة من المزخرِفات طالما أنها تعمل مع كل الكائنات من خلال واجهة الجزء (Component Interface).

مثال توضيحي

في هذا المثال يسمح لك نمط المزخرِف بضغط وتشفير البيانات الحساسة بشكل مستقل عن الشيفرة التي تستخدم تلك البيانات.

ضع الصورة. مثال مزخرِف التشفير والضغط.

يغلِّف التطبيق كائن مصدرِ البيانات بزوج من المزخرِفات، وكلا المزخرفين يغيران الطريقة التي تُكتب البيانات بها وتقرأ من القرص:

  • تشفر المزخرِفات البيانات وتضغطها قبل كتابتها على القرص مباشرة، وتكتب الفئةُ الأساسيةُ البيانات المحمية والمشفرة إلى الملف دون العلم بالتغيير.
  • تمر البيانات بعد قراءتها من القرص مباشرة على نفس المزخرِفات التي ستفك ضغطها وتفك تشفيرها.

تستخدم المزخرِفات وفئةُ مصدرِ البيانات نفس الواجهة التي تجعلها كلها قابلة للتعديل بشكل متبادل (interchangeable) في شيفرة العميل.

// تعرِّف واجهة الجزء العملياتَ التي يمكن تغييرها بواسطة المزخرفات.
interface DataSource is
    method writeData(data)
    method readData():data

// تضيف الأجزاء الحقيقية استخدامات افتراضية للعمليات، قد تكون هناك صور مختلفة لهذه الفئات
// في برنامج ما
class FileDataSource implements DataSource is
    constructor FileDataSource(filename) { ... }

    method writeData(data) is
        // اكتب البيانات إلى الملف.

    method readData():data is
        // اقرأ البيانات من الملف.

// تتبع فئة المزخرِف الأساسي نفس الواجهة كالأجزاء الأخرى، والقرص الأساسي لهذه الفئة هو تعريف واجهة التغليف
// لكل المزخرِفات الحقيقية. قد يشمل الاستخدام الافتراضي لشيفرة التغليف حقلًا لتخزين الجزء المغلَّف
// ووسائل بدئها.

class DataSourceDecorator implements DataSource is
    protected field wrappee: DataSource

    constructor DataSourceDecorator(source: DataSource) is
        wrappee = source

    // يفوِّض المزخرِف الأساسي كل المهام إلى الجزء المغلَّف، ويمكن إضافة السلوكيات الإضافية من المزخرِفات
    // الحقيقية.
    method writeData(data) is
        wrappee.writeData(data)

    // قد تستدعي المزخرِفات الاستخدام الأم للعملية بدلًا من استدعاء
    // الكائن المغلَّف مباشرة، وهذا المنظور يبسط توسعة فئات المزخرِف.
    method readData():data is
        return wrappee.readData()

// يجب أن تستدعي المزخرِفات الحقيقيةُ الأساليبَ على الكائن المغلَّف، 
// لكن قد تضيف من عندها شيئًا إلى النتيجة
// تستطيع المزخرِفات تنفيذ السلوك المضاف قبل أو بعد استدعاء الكائن
// المغلَّف.
class EncryptionDecorator extends DataSourceDecorator is
    method writeData(data) is
        // 1. شفِّر البيانات المُمَرَّرة.
        // 2. للكائن المغلَّف writeData مرر البيانات المشفرة إلى أسلوب 

    method readData():data is
        // 1. للكائن المغلَّف readData اجلب البيانات من أسلوب.
        // 2. حاول فك تشفيرها إن كانت مشفرة.
        // 3. أعد النتيجة.

// يمكنك تغليف الكائنات في عدة طبقات من المزخرِفات.
class CompressionDecorator extends DataSourceDecorator is
    method writeData(data) is
        // 1. اضغط البيانات المُمَرَّرة.
        // 2. للكائن writeData مرر البيانات المضغوطة إلى أسلوب
        // المغلَّف.

    method readData():data is
        // 1. للكائن المغلَّف readData اجلب البيانات من أسلوب.
        // 2. حاول فك ضغطها إن كانت مضغوطة.
        // 3. أعد النتيجة.


// 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()
    // ...