أنماط التصميم في جافا

نمط Decorator

15 دقيقة الدرس 7 من 13

نمط Decorator

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

لماذا لا نلجأ إلى الوراثة فحسب؟

لنفترض أن لديك محرر نصوص TextEditor وتريد دعم التدقيق الإملائي والحفظ التلقائي وتمييز الكود — بأي مجموعة ممكنة. إنشاء فصيل فرعي لكل مجموعة يُعطيك سبعة أصناف لثلاث ميزات. أضف ميزة رابعة ويتضاعف العدد مجددًا. يتجاوز Decorator هذا كليًا: كل ميزة تعيش في صنف واحد ويُغلّف أي TextEditor آخر.

مبدأ Open/Closed في التطبيق العملي: Decorator هو التطبيق الكتبي لمبدأ Open/Closed — المكوّنات مفتوحة للتوسعة (غلّفها) ومغلقة للتعديل (لا تلمسها أبدًا).

البنية

  • Component — الواجهة (أو الصنف المجرّد) المشتركة بين الكائن الحقيقي وكل مزخرف.
  • ConcreteComponent — التنفيذ الأساسي الذي تريد توسعته.
  • BaseDecorator — يحمل مرجعًا لـ Component ويفوّض جميع الاستدعاءات؛ الأصناف الفرعية تُجاوز الأساليب التي تحتاج إلى تعزيزها فقط.
  • ConcreteDecorators — تُضيف السلوك الفعلي قبل التفويض و/أو بعده.

مثال تحويل النصوص

ابدأ بواجهة مكوّن بسيطة وتنفيذ عادي:

// Component public interface TextTransformer { String transform(String input); } // ConcreteComponent public class PlainText implements TextTransformer { @Override public String transform(String input) { return input; } }

يخزّن المزخرف الأساسي المكوّن المُغلَّف ويُعيد توجيه كل استدعاء إليه:

public abstract class TextDecorator implements TextTransformer { private final TextTransformer wrapped; protected TextDecorator(TextTransformer wrapped) { this.wrapped = wrapped; } @Override public String transform(String input) { return wrapped.transform(input); // تفويض نقي بشكل افتراضي } }

تُضيف المزخرفات الملموسة السلوك حول ذلك التفويض:

// يُزيل المسافات البيضاء قبل التفويض public class TrimDecorator extends TextDecorator { public TrimDecorator(TextTransformer wrapped) { super(wrapped); } @Override public String transform(String input) { return super.transform(input.strip()); } } // يُحوّل النتيجة إلى أحرف كبيرة بعد التفويض public class UpperCaseDecorator extends TextDecorator { public UpperCaseDecorator(TextTransformer wrapped) { super(wrapped); } @Override public String transform(String input) { return super.transform(input).toUpperCase(); } } // يستبدل كل tab بأربع مسافات public class TabExpandDecorator extends TextDecorator { public TabExpandDecorator(TextTransformer wrapped) { super(wrapped); } @Override public String transform(String input) { return super.transform(input.replace("\t", " ")); } }

ركّبها بأي ترتيب عند موقع الاستدعاء:

TextTransformer pipeline = new UpperCaseDecorator( new TrimDecorator( new TabExpandDecorator( new PlainText() ) ) ); String result = pipeline.transform(" \thello world "); // → " HELLO WORLD" (توسيع tab، ثم حذف المسافات، ثم تحويل للأحرف الكبيرة)
اقرأ التداخل من الداخل إلى الخارج: المُنشئ الأعمق يُنفَّذ أولًا. في المثال أعلاه ترتيب التنفيذ هو TabExpand ← Trim ← UpperCase. اختر ترتيب التغليف الأكثر وضوحًا لنطاق عملك.

java.io — مثال الحياة الواقعية الكلاسيكي لـ Decorator

تقوم تراتبية تدفقات java.io بأكملها على Decorator. فـ InputStream هو المكوّن؛ وFileInputStream وByteArrayInputStream وغيرهما هي المكوّنات الملموسة؛ وَFilterInputStream هو المزخرف الأساسي. كل صنف تُطبّقه فوق ذلك — BufferedInputStream، DataInputStream، GZIPInputStream — هو مزخرف ملموس.

import java.io.*; import java.util.zip.GZIPInputStream; // قراءة ملف مضغوط بـ gzip مع التخزين المؤقت، سطرًا بسطر try (var reader = new BufferedReader( new InputStreamReader( new GZIPInputStream( new FileInputStream("data.gz") ), java.nio.charset.StandardCharsets.UTF_8 ) )) { String line; while ((line = reader.readLine()) != null) { System.out.println(line); } }

يُضيف كل غلاف قدرة واحدة بالضبط — التخزين المؤقت، فك ترميز المحارف، ضغط gzip، قراءة الملف — دون أن يحتاج أي من هذه الأصناف إلى معرفة الأصناف الأخرى. يمكنك استبدال FileInputStream بـ ByteArrayInputStream (للاختبارات) دون المساس بأي طبقة أخرى.

المقايضات ومتى تستخدم النمط

  • فضّله على الوراثة عندما تكون الميزات متعامدة حقًا وقابلة للتركيب.
  • فضّله على الصنف الضخم الواحد الذي يُبدّل السلوك بالأعلام — تجعل المزخرفات كل اهتمام قابلًا للاختبار بمعزل.
  • احذر من الحساسية للترتيب: حذف المسافات ثم التحويل للأحرف الكبيرة ليس كالتحويل للأحرف الكبيرة ثم حذف المسافات عند تعلّق الأمر بحالات الحروف المرتبطة بالمحلّية.
  • تنكسر المساواة والهوية: decorated.equals(original) تكون false دائمًا تقريبًا. لا تعتمد على هوية الكائن حين تكون المزخرفات في اللعبة.
  • الأكوام العميقة يصعب تصحيحها: سلسلة من عشرة مزخرفات قد تُعتم مصدر نتيجة غير متوقعة. أبقِ كل مزخرف صغيرًا ووثّق الترتيب المتوقع.
Decorator مقابل Proxy: كلاهما يُغلّف كائنًا، لكن بنية مختلفة. يتحكم Proxy في الوصول (التحميل الكسول، التخزين المؤقت، فحص الأمان). يُضيف Decorator السلوك. إذا وجدت نفسك تتحقق من الصلاحيات أو تُهيئ كسولًا داخل "مزخرف"، فأنت على الأرجح تريد Proxy.

Decorator مع الواجهات الوظيفية في Java الحديثة (بديل حديث)

للتحويلات البسيطة ذات الأسلوب الواحد، يمنحك Function::andThen وFunction::compose نفس التركيب دون أي تراتبية صفية:

import java.util.function.UnaryOperator; UnaryOperator<String> trim = String::strip; UnaryOperator<String> upper = String::toUpperCase; UnaryOperator<String> pipeline = trim.andThen(upper); String result = pipeline.apply(" hello "); // "HELLO"

يظل نمط Decorator المبني على الأصناف هو الأداة الصحيحة حين تحتوي الواجهة على أساليب متعددة، أو حين تحمل المزخرفات حالة (كمزخرف العدّ)، أو حين تحتاج إلى تمرير المزخرف عبر كود يتوقع نوع المكوّن.

الخلاصة

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