نمط المزخرِف
نمط المزخرِف هو نمط تصميم هيكلي يضيف سلوكيات جديدة إلى الكائنات بوضعها داخل كائنات تغليف خاصة تحتوي تلك السلوكيات.
المشكلة
ضع الصورة. يستطيع برنامج أن يستخدم فئة المنبه لإرسال إشعارات عن الأحداث المهمة إلى قائمة عناوين بريدية.
تخيل أنك تعمل على مكتبة إشعارات تسمح للبرامج الأخرى بتنبيه مستخدميها إلى الأحداث المهمة، وبنيت النسخة الأولية من المكتبة على فئة Notifier
بها بضعة حقول قليلة، ومنشئ (Constructor) وأسلوب Send
وحيد، ويستطيع الأسلوب قبول وسيط في هيئة رسالة من عميل ويرسلها إلى قائمة من عناوين البريد التي يمررها المنشئ إلى المنبه (Notifier).
ويفترض بتطبيق من طرف ثالث يتصرف كعميل أن ينشئ ويهيئ كائن المنبه مرة واحدة ثم يستخدمه في كل مرة يحدث شيء مهم، وستدرك في مرحلة ما أن مستخدمي المكتبة ينتظرون أكثر من مجرد إشعارات بريدية، فكثير منهم يريدون رسائل نصية قصيرة (SMS) عن المشاكل الحرجة التي لا تحتمل الانتظار، ويود غيرهم أن يُنبَّهوا على فيس بوك، وكذلك بالمثل للموظفين الذين يرغبون في تلقي رسائل على سلاك (Slack).
ضع الصورة. يُستخدم كل نوع تنبيه كفئة منبه فرعية.
لهذا توسِّع فئة Notifier
وتضع أساليب تنبيه إضافية في فئات فرعية، ويفترض بالعميل أن يعرف فئة التنبيه المطلوبة ويستخدمها لكل الإشعارات التالية، وستدرك أنك ستحتاج في الحالات الحرجة إلى إشعار المستخدم في كل القنوات التي هو فيها -فيس بوك، بريد، سلاك، ...- والطريقة المباشرة لفعل ذلك هي إنشاء فئات فرعية خاصة تجمع عدة أساليب إشعارات في فئة واحدة، لكن عيبها أن الشيفرة ستصبح ضخمة، سواء شيفرة المكتبة أو العميل، ونريد أن نجد طريقة أفضل لهيكلة فئات الإشعارات بحيث لا تكبر أعدادها بشكل زائد عن الحد.
ضع الصورة. انفجار الفئات الفرعية.
الحل
رغم أن توسعة الفئة هي أول ما يرد للذهن عند تغيير سلوك كائن ما إلا أن الاكتساب (Inheritance) له مؤشرات خطر يجب أن تدركها:
- الاكتساب ساكن، فلا يمكنك تغيير سلوك كائن موجود فعلًا في وقت التشغيل (run time) ولا يمكنك فعل شيء سوى استبدال الكائن بكامله بواحد أنشاته من فئة فرعية.
- لا يمكن أن يكون للفئات الفرعية سوى فئة أم وحيدة، ذلك أن الاكتساب لا يسمح في أغلب اللغات للفئة أن تكتسب سلوكيات من عدة فئات في نفس الوقت.
وإحدى الطرائق التي تتغلب بها على أوجه القصور تلك هي استخدام التركيب (Composition) بدلًا من الاكتساب، فالتركيب يجعل للكائن مرجعًا إلى كائن آخر يفوض إليه بعض المهام، في حين أنه مع الاكتساب يكون الكائن قادرًا على تنفيذ تلك المهام مكتسبًا السلوك من الفئة الأم. تستطيع كذلك استبدال كائن المساعد (Helper) بغيره، لتغير بذلك سلوك الحاوية أثناء وقت التشغيل، ويستطيع الكائن استخدام السلوك من فئات مختلفة ويحمل مراجع إلى عدة كائنات يفوض إليها المهام. ويُعد التركيب هو المبدأ الأساسي خلف عدة أنماط تصميم بما فيها المزخرِف.
ضع الصورة. الاكتساب مقابل التركيب.
ولنمط المزخرف اسم آخر يصف وظيفة النمط بوضوح، وهو "المغلِّف"، إذ أن المغلِّف هو كائن يمكن ربطه بكائن "هدف"، ويحتوي المغلِّف على نفس الأساليب التي في الكائن الهدف، ويفوِّض إليه كل الطلبات التي يستلمها، لكن قد يغير المغلِّف النتيجة بشيء ما قبل أو بعد تمريره الطلب إلى الهدف.
يستخدم المغلِّف نفس الواجهة كالكائن الذي يتم تغليفه (المغلَّف)، لهذا تبدو تلك الكائنات متطابقة من وجهة نظر العميل، ولكي تغطي كائنًا ما بعدة مغلِّفات مضيفًا سلوكًا مجمعًا من كل المغلِّفات إليه، يجب أن تجعل الحقل المرجعي للمغلِّف يقبل أي كائن يتبع تلك الواجهة. وفي مثال الإشعارات الذي تقدم سنترك سلوك إشعارات البريد البسيط داخل فئة Notifier
الأساسية، لكن سنحول كل أساليب الإشعار الأخرى إلى مزخرفات (Decorators).
ضع الصورة. عدة أساليب إشعار تصبح مزخرِفات.
ستحتاج شيفرة العميل أن تغلف كائن إشعار بسيط بمجموعة مزخرفات تطابق تفضيلات العميل، وستهيكَل الكائنات في هيئة مكدَّس (Stack).
ضع الصورة. قد تهيئ التطبيقات تكديسات معقدة من مزخرفات الإشعارات.
وسيكون آخر مزخرِف في المكدس هو الكائن الذي يعمل معه العميل، وبما أن كل المزخرِفات تستخدم نفس الواجهة التي يستخدمها المنبه الأساسي (Base Notifier)، فإن شيفرة العميل لن يهمها إن كانت تعمل مع كائن منبه "نقي" أو مزخرَف، ونستطيع تطبيق نفس المنظور على سلوكيات أخرى مثل تنسيق الرسائل أو تنظيم قائمة المستلمين، ويستطيع العميل زخرفة الكائن بأي مزخرِفات خاصة طالما أنها تتبع نفس الواجهة كالباقين.
مثال واقعي
ضع الصورة. تحصل على أثر مجمع من ارتداء الملابس.
ارتداء الملابس مثال على استخدام المزخرِفات، فأنت تغلف نفسك بكنزة مثلًا طلبًا للدفء في البرد اليسير، ثم تضيف فوقها معطفًا إن اشتد البرد، أما إن كانت تمطر فستلبس معطفًا خاصًا بالمطر، وكل تلك القطع توسع سلوكك الأساسي لكنها ليست جزءًا منك، وتستطيع خلع أي قطعة ملابس في أي وقت تشعر فيه أنك لم تحتاج إليها.
البُنية
ضع الصورة.
- يصرح الجزء (Component) عن واجهة مشتركة لكل من المغلِّفات والكائنات المغلَّفة.
- الجزء الحقيقي (Concrete Component) هو فئة الكائنات يتم تغليفها، وتعرِّف السلوك الأساسي الذي يمكن تغييره بواسطة المزخرِفات.
- فئة المزخرف الأساسي (Base Decorator) لها حقل للإشارة إلى كائن مغلَّف، ويجب أن يصرَّح عن نوع الحقل على أنه واجهة الجزء كي يستطيع احتواء كلًا من الأجزاء الحقيقية والمزخرِفات. ويفوِّض المزخرِف الأساسي العمليات كلها إلى الكائن المغلَّف.
- تعرِّف المزخرِفات الحقيقية (Concrete Decorators) سلوكيات إضافية يمكن أن تضاف إلى الأجزاء بديناميكية، وتتخطى المزخرِفات الحقيقية أساليب المزخرِف الأساسي وتنفذ أسلوبها قبل أو بعد استدعاء الأسلوب الأم (Parent Behavior).
- العميل (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. أعد النتيجة.
// خيار1. مثال بسيط لتجميع المزخرف.
class Application is
method dumbUsageExample() is
source = new FileDataSource("somefile.dat")
source.writeData(salaryRecords)
// كُتب الملف الهدف ببيانات غير مشفرة.
source = new CompressionDecorator(source)
source.writeData(salaryRecords)
// كُتب الملف الهدف ببيانات مشفرة.
source = new EncryptionDecorator(source)
// يحتوي متغير المصدر الآن على هذا:
// Encryption > Compression > FileDataSource
source.writeData(salaryRecords)
// كُتب الملف الهدف ببيانات مشفرة ومضغوطة.
// خيار 2. شيفرة العميل التي تستخدم مصدر بيانات خارجي.
// أو تهتم بتفاصيل تخزين البيانات، إذ تعمل مع مصدر بيانات معد مسبقًا SalaryManager لا تعرف كائنات
// (app configurator) تم استقباله من قبل مهيئ التطبيق.
class SalaryManager is
field source: DataSource
constructor SalaryManager(source: DataSource) { ... }
method load() is
return source.readData()
method save() is
source.writeData(salaryRecords)
// ...أساليب أخرى مفيدة...
// يستطيع التطبيق أن يجمع تكديسات مختلفة من المزخرِفات أثناء وقت التشغيل اعتمادًا على الإعدادات أو البيئة.
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()
// ...
قابلية الاستخدام
استخدم نمط المزخرف عند الحاجة لتعيين سلوكيات إضافية إلى الكائنات في وقت التشغيل دون تعطيل الشيفرة التي تستخدم تلك الكائنات.
يسمح لك نمط المزخرف بهيكلة منطق تجارتك (Business Logic) في هيئة طبقات، فأنشئ مزخرِفًا لكل طبقة ونظم الكائنات مع تجميعات مختلفة من هذا المنطق في وقت التشغيل. تستطيع شيفرة العميل أن تعامل كل تلك الكائنات بنفس الطريقة بما أنها جميعًا تتبع نفس الواجهة المشتركة.
استخدم نمط المزخرِف عند عدم إمكانية توسعة سلوك الكائن باستخدام الاكتساب (Inheritance).
ستجد كلمة final المفتاحية في أغلب لغات البرمجة، وتستخدم لمنع التوسعات المستقبلية لفئة ما، والطريقة الوحيدة لإعادة استخدام السلوك الموجود فعليًا في فئة نهائية (final) هو بتغليف الفئة بمُغلِّفك الخاص باستخدام نمط المزخرِف.
كيفية الاستخدام
- تأكد أن نطاقك التجاري يمكن تمثيله كجزء أساسي مع عدة طبقات اختيارية فوقه.
- اكتشف أي الأساليب التي تشترك فيها الطبقات الخيارية مع الجزء الأساسي، ومن ثم أنشئ واجهة للجزء وصرح عن تلك الأساليب هناك.
- أنشئ فئة جزء حقيقي وعرَّف السلوك الأساسي فيها.
- أنشئ فئة مزخرِف أساسي، وينبغي أن يكون فيها حقل لتخزين مرجع إلى الكائن المُغلَّف، ويجب أن يُصرَّح عن الحقل مع نوع واجهة الجزء (Component Interface Type) ليسمح بالربط مع الأجزاء الحقيقية والمزخرِفات، ويجب أن يفوض المزخرِف الأساسي كل المهام إلى الكائن المغلَّف.
- تأكد أن كل الفئات تستخدم واجهة الجزء.
- أنشئ مزخرَفات حقيقية بتوسيعها من المزخرِف الأساسي، ويجب أن ينفذ المزخرف الحقيقي سلوكه قبل أو بعد الاستدعاء إلى الأسلوب الأم (Parent Method)، والذي يفوض بدوره المهام إلى الكائن المغلَّف.
- يجب أن تكون شيفرة العميل مسؤولة عن إنشاء المزخرِفات وترتيبها بالطريقة التي يحتاجها العميل.
المزايا والعيوب
المزايا
- توسيع سلوك الكائن دون إنشاء فئة فرعية جديدة.
- إضافة أو حذف مسؤوليات من الكائن أثناء التشغيل.
- جمع عدة سلوكيات من خلال تغليف كائن بعدة مزخرِفات.
- مبدأ المسؤولية الواحدة. يمكنك تقسيم فئة أحادية تستخدم عدة صور محتملة من السلوكيات إلى عدة فئات أصغر منها.
العيوب
- صعوبة حذف مغلِّف ما من المكدِّسات (Stacks).
- صعوبة استخدام مزخرِف بحيث لا يعتمد سلوكه على الترتيب داخل مكدِّس المزخرِفات.
- قد تبدو شيفرة الإعدادات الأولية قبيحة في البداية.
العلاقات مع الأنماط الأخرى
- يغير نمط المحوِّل واجهة كائن موجود فعلًا بينما يحسِّن المزخرِفُ الكائنَ دون تغيير واجهته، وإضافة لهذا فإن المزخرف يدعم التركيب التكراري (Recursive Composition) الذي يستحيل عند استخدام المحول.
- يقدم المحول واجهة مختلفة للكائن المغلَّف، ويقدم نمط الوكيل (Proxy) نفس الواجهة له، أما المزخرِف فيقدم واجهة محسَّنة.
- يتشابه نمطيْ المزخرِف وسلسلة المسؤوليات في هيكلة الفئات إذ يعتمد كلا النمطين على التركيب التكراري لتمرير التنفيذ خلال سلسلة كائنات، لكن بأي حال هناك فروق كبيرة أخرى بينهما، فسلسلة المسؤوليات تستطيع تنفيذ عمليات عشوائية بشكل مستقل عن بعضها، كما تستطيع إيقاف تمرير الطلب عند أي نقطة لما بعدها، ومن الناحية الأخرى تستطيع مزخرفات عديدة توسيع سلوك الكائن مع الحفاظ عليه متناسقًا مع الواجهة الأساسية، وإضافة لهذا فلا يُسمح للمزخرِفات أن تعطل سير الطلب.
- للمركَّب والمزخرِف مخططات هيكلية متشابهة بما أن كليهما يعتمدان على التركيب التكراري لتنظيم عدد مفتوح النهاية من الكائنات. ويشبه المزخرِفُ المركَّبَ لكنه لا يملك إلا عنصرًا فرعيًا واحدًا، كما أن المزخرف يضيف مسؤوليات إضافية إلى الكائن المغلَّف في حين أن المركب لا يزيد على جمع النتائج من فروعه. لكن هذين النمطين قد يتعاونان فيما بينهما إذ يمكنك استخدام المزخرف لتوسيع سلوك كائن بعينه في شجرة المركَّب.
- التصميمات التي تستخدم المركب والمزخرف بكثرة تستفيد من استخدام النموذج الأولي، ذلك أن استخدامه يسمح لك بنسخ هياكل معقدة بدلًا من إعادة إنشائهم من الصفر.
- يسمح المزخرف بتغيير مظهر كائن ما، بينما يسمح لك نمط الخطة بتغيير جوهره.
- رغم تشابه المزخرف والوكيل في الهيكلة إلا أنهما يختلفان تمامًا في الهدف منهما، فالنمطين كلاهما قد بنيا على مبدأ التركيب حيث يفترض بكائن ما أن يفوض بعضًا من مهامه إلى غيره، لكن الفرق هو أن الوكيل عادة ما يدير دورة حياة كائن الخدمة الخاصة به بنفسه، بينما يتحكم العميل دومًا في تركيب المزخرفات.
الاستخدام في لغة جافا
المستوى: ★ ★ ☆
الانتشار: ★ ★ ☆
أمثلة الاستخدام: يكثر استخدام نمط المزخرِف في شيفرة جافا، خاصة في الشيفرات المتعلقة بالمتدفقات (streams). إليك بعض الأمثلة على المزخرِف في مكتبات جافا:
- كل الفئات الفرعية لـ java.io.InputStream ، OutputStream ، Reader ، Writer لديها منشئات تقبل كائنات من نفس نوعها.
- أساليب java.util.Collections و ()checkedXXX و ()synchronizedXXX و ()unmodifiableXXX
- javax.servlet.http.HttpServletRequestWrapper و
HttpServletResponseWrapper
.
يمكن ملاحظة نمط المزخرف من خلال الأساليب الإنشائية أو المنشئات التي تقبل كائنات من نفس النوع/الواجهة الحالية.
مثال: مزخرفات التشفير والضغط
يوضح المثال كيفية تعديل سلوك كائن ما دون تغيير شيفرته، ففي البداية كان المنطق التجاري (Business Logic) يستطيع قراءة وكتابة البيانات في صورة نص بسيط (Plain text)
الاستخدام في لغة #C
المستوى: ★ ★ ☆
الانتشار: ★ ★ ☆
أمثلة الاستخدام: يكثر استخدام نمط المزخرِف في شيفرة جافا، خاصة في الشيفرات المتعلقة بالمتدفقات (streams).
يمكن ملاحظة نمط المزخرف من خلال الأساليب الإنشائية أو المنشئات التي تقبل كائنات من نفس النوع/الواجهة الحالية.
مثال تصوري
يوضح هذا المثال بنية نمط المزخرِف، ويركز على إجابة الأسئلة التالية:
- ما الفئات التي يتكون منها؟
- ما الأدوار التي تلعبها هذه الفئات؟
- كيف ترتبط عناصر النمط ببعضها؟
Program.cs: مثال تصوري
الاستخدام في لغة PHP
المستوى: ★ ★ ☆
الانتشار: ★ ★ ☆
أمثلة الاستخدام: يكثر استخدام نمط المزخرِف في شيفرة جافا، خاصة في الشيفرات المتعلقة بالمتدفقات (streams).
مثال تصوري
يوضح هذا المثال بنية نمط المزخرِف، ويركز على إجابة الأسئلة التالية:
- ما الفئات التي يتكون منها؟
- ما الأدوار التي تلعبها هذه الفئات؟
- كيف ترتبط عناصر النمط ببعضها؟
الاستخدام في لغة بايثون
المستوى: ★ ★ ☆
الانتشار: ★ ★ ☆
أمثلة الاستخدام: يكثر استخدام نمط المزخرِف في شيفرة جافا، خاصة في الشيفرات المتعلقة بالمتدفقات (streams).
يمكن ملاحظة نمط المزخرف من خلال الأساليب الإنشائية أو المنشئات التي تقبل كائنات من نفس النوع/الواجهة الحالية.
مثال تصوري
يوضح هذا المثال بنية نمط المزخرِف، ويركز على إجابة الأسئلة التالية:
- ما الفئات التي يتكون منها؟
- ما الأدوار التي تلعبها هذه الفئات؟
- كيف ترتبط عناصر النمط ببعضها؟
الاستخدام في لغة Swift
المستوى: ★ ★ ☆
الانتشار: ★ ★ ☆
أمثلة الاستخدام: يكثر استخدام نمط المزخرِف في شيفرة جافا، خاصة في الشيفرات المتعلقة بالمتدفقات (streams).
يمكن ملاحظة نمط المزخرف من خلال الأساليب الإنشائية أو المنشئات التي تقبل كائنات من نفس النوع/الواجهة الحالية.
مثال تصوري
يوضح هذا المثال بنية نمط المزخرِف، ويركز على إجابة الأسئلة التالية:
- ما الفئات التي يتكون منها؟
- ما الأدوار التي تلعبها هذه الفئات؟
- كيف ترتبط عناصر النمط ببعضها؟
الاستخدام في لغة TypeScript
المستوى: ★ ★ ☆
الانتشار: ★ ★ ☆
أمثلة الاستخدام: يكثر استخدام نمط المزخرِف في شيفرة جافا، خاصة في الشيفرات المتعلقة بالمتدفقات (streams).
يمكن ملاحظة نمط المزخرف من خلال الأساليب الإنشائية أو المنشئات التي تقبل كائنات من نفس النوع/الواجهة الحالية.
مثال تصوري
يوضح هذا المثال بنية نمط المزخرِف، ويركز على إجابة الأسئلة التالية:
- ما الفئات التي يتكون منها؟
- ما الأدوار التي تلعبها هذه الفئات؟
- كيف ترتبط عناصر النمط ببعضها؟
انظر أيضًا
مصادر
توثيق نمط المزخرِف في موقع refactoring.guru.