نمط المحوِّل Adapter
نمط المحوِّل هو نمط تصميم هيكلي يسمح للكائنات غير المتوافقة واجهاتها بالتعاون.
المشكلة
تخيل أنك تنشئ تطبيقًا لمراقبة سوق البورصة، ويحمل التطبيق بيانات البورصة من مصادر متعددة بصيغة XML ثم يعرض للمستخدم مخططات ورسومًا بيانية. ولنفرض أنك قررت في مرحلة ما أن تطور التطبيق بإدخال مكتبة تحليلات من طرف ثالث عليه، لكن تلك المكتبة لا تعمل إلا مع بيانات بصيغة JSON.
ضع الصورة. لا يمكنك استخدام مكتبة التحليلات كما هي لأنها تريد بيانات بصيغة لا تتوافق مع تطبيقك.
يمكنك تعديل المكتبة لتعمل مع xml، لكن ذلك قد يعطل بعض الشيفرات المعتمدة عليها، أو قد لا يكون لديك صلاحية وصول إلى شيفرة المكتبة المصدرية من الأساس فحينها يكون حتى تعديل المكتبة مستحيلًا.
الحل
يكون الحل هنا هو إنشاء محوِّل (Adapter)، وهو كائن خاص (Special Object) يحول واجهة أحد الكائنات كي يستطيع الكائن الآخر فهمها. ويغلف المحول الكائن ليخفي تعقيد التحويل الحادث وراء الستار، ولا يكون الكائن المحوَّلة واجهته مدركًا للمحوِّل، فيمكنك مثلًا تغليف كائن يعمل بالنظام المتري -مثل الكيلو متر والمتر- بمحوِّل يحوِّل كل البيانات إلى الوحدات الامبريالية مثل -القدم والأميال-.
إضافة إلى ما تقدم من تحويل البيانات إلى صيغ مختلفة، فإن المحولات تيسر أيضًا من عمل الكائنات التي تختلف واجهاتها عن بعضها، إليك طريقة عملها:
- يحصل المحول على واجهة تتوافق مع أحد الكائنات الموجودة مسبقًا.
- يمكن للكائن باستخدام تلك الواجهة استدعاء أساليب المحول.
- عند استلام الاستدعاء، يمرِّر المحولُ الطلبَ إلى الكائن الثاني لكن بالصيغة والترتيب اللذين يتوقعهما ذلك الكائن.
من الممكن أحيانًا إنشاء محول ثنائي يحول الاستدعاءات في كلا الاتجاهين ذهابًا واستلامًا.
ضع الصورة.
بالعودة إلى مثال تطبيق البورصة، فمن أجل حل معضلة الصيغ غير المتوافقة يمكنك إنشاء محولات من XML إلى JSON لكل فئة من مكتبة التحليلات التي تعمل شيفرتك معها تلقائيًا، ثم يمكنك تعديل الشيفرة لتتواصل مع المكتبة من خلال تلك المحولات فقط.
وحين يستقبل محولٌ استدعاءً فإنه يترجم بيانات XML المعطاة إلى هيكل JSON ويمرر الاستدعاء إلى الأساليب المناسبة لكائن تحليلات مغلف.
مثال واقعي
حين تسافر من الولايات المتحدة إلى الاتحاد الأوروبي لأول مرة فستقابلك مشكلة حين تحاول شحن حاسوبك، ذلك أن مقابس الكهرباء المستخدمة مختلفة في أوربا عن أمريكا عن غيرهما، لهذا واجهت مشكلة في شحن حاسوبك، وحل تلك المشكلة هو استخدام محوِّل صغير من هذا المقبس إلى ذاك.
البُنية
محول الكائن
هذا التطبيق يستخدم مبدأ التركيب (compostition)، إذ يستخدم المحوِّل واجهة أحد الكائنات ويغلف واجهة الكائن الآخر، يمكن استخدام هذا المبدأ في كل لغات البرمجة الشائعة.
ضع الصورة.
- فئة العميل (Client) هي فئة تحتوي على المنطق التجاري الحالي للبرنامج.
- تصف واجهة العميل بروتوكول يجب أن تتبعه الفئات الأخرى لتستطيع التفاهم مع شيفرة العميل.
- فئة الخدمة (Service) هي فئة مفيدة (عادة ما تكون طرفًا ثالثًا أو إصدارًا قديمًا). لا يستطيع العميل استخدام هذه الفئة مباشرة بسبب أن واجهتها لا تتوافق معه.
- فئة المحول (Adapter) هي فئة تستطيع العمل مع كلا من العميل والخدمة، إذ تستخدم واجهة العميل بينما تغلف كائن الخدمة. ويستقبل المحول استدعاءات من العميل من خلال واجهة المحول ويترجمها إلى استدعاءات إلى كائن الخدمة المغلَّف في صيغة يستطيع الأخير فهمها.
- لا ترتبط شيفرة العميل بفئة المحول الحقيقية طالما كانت تعمل مع المحول من خلال واجهة العميل، ويمكنك بفضل ذلك إدخال أنواع محولات جديدة إلى البرنامج دون تعطيل الشيفرة الحالية. هذا مفيد حين تتغير أو تُستبدل الواجهة الخاصة بفئة الخدمة، فيمكنك حينها إنشاء فئة محول جديدة دون تغيير شيفرة العميل.
محول الفئة
هذا التطبيق يستخدم أسلوب الاكتساب (inheritance)، إذ يكتسب المحول واجهات من كلا الكائنين في نفس الوقت، لاحظ أن هذا المنظور يمكن استخدامه فقط في لغات تدعم الاكتساب المتعدد مثل ++C.
ضع الصورة.
- لا يحتاج محول الفئة (Class Adapter) إلى تغليف أي كائن بسبب أنه يكتسب سلوكه من كلًا من العميل والخدمة على حد سواء، ويحدث الاكتساب داخل الأساليب التي تم تخطيها. والمحول الناتج يمكن استخدامه مكان فئة عميل موجودة مسبقًا.
مثال توضيحي
بُني المثال التالي الذي يوضح نمط المحول على المعضلة الكلاسيكية بين الوتد المربع والثقب الدائري.
ضع الصورة. تحويل الأوتاد المربعة لتناسب الثقوب الدائرية.
يدعي المحول أنه وتد دائري بنصف قطر يساوي نصف قطر المربع (بمعنى آخر، قطر أصغر دائرة تستوعب الوتد المربع).
// لنقل أن لديك فئتين بواجهات متوافقة:
// RoundHole و RoundPeg.
class RoundHole is
constructor RoundHole(radius) { ... }
method getRadius() is
// أَعِدْ نصف قطر الثقب.
method fits(peg: RoundPeg) is
return this.getRadius() >= peg.radius()
class RoundPeg is
constructor RoundPeg(radius) { ... }
method getRadius() is
// أعد نصف قطر الوتد.
// لكن هناك فئة غير متوافقة: SquarePeg.
class SquarePeg is
constructor SquarePeg(width) { ... }
method getWidth() is
// أعد عرض الوتد المربع.
// تسمح فئة المحول بإدخال الأوتاد المربعة في الثقوب الدائرية.
// لتسمح لكائنات المحول بالتصرف كأوتاد دائرية RoundPeg إذ توسع فئة.
class SquarePegAdapter extends RoundPeg is
// SquarePeg في الواقع، يحتوب المحول على نسخة من فئة
private field peg: SquarePeg
constructor SquarePegAdapter(peg: SquarePeg) is
this.peg = peg
method getRadius() is
// يدعي المحول أنه وتد دائري بنصف قطر يسمح بإدخال الوتد المربع
// الذي يغلفه المحول في الحقيقية.
return peg.getWidth() * Math.sqrt(2) / 2
// في مكان ما في شيفرة العميل.
hole = new RoundHole(5)
rpeg = new RoundPeg(5)
hole.fits(rpeg) // true
small_sqpeg = new SquarePeg(2)
large_sqpeg = new SquarePeg(5)
hole.fits(small_sqpeg) // لن يُجمّع هذا بسبب أنها أنواع غير متوافقة.
small_sqpeg_adapter = new SquarePegAdapter(small_sqpeg)
large_sqpeg_adapter = new SquarePegAdapter(large_sqpeg)
hole.fits(small_sqpeg_adapter) // true
hole.fits(large_sqpeg_adapter) // false
قابلية الاستخدام
- استخدم فئة المحول حين تريد أن تستخدم بعض الفئات الموجودة مسبقًا، لكن واجهتها غير متوافقة مع بقية شيفرتك.
يسمح لك نمط المحول بإنشاء فئة وسيطة تعمل كمترجم بين شيفرتك وبين الفئة القديمة أو فئة الطرف الثالث أو أي فئة أخرى بواجهة غريبة.
- استخدم النمط حين تريد إعادة استخدام فئات موجودة مسبقًا تفتقر إلى بعض الوظائف التي لا يمكن إضافتها إلى الفئة الأم.
يمكنك توسيع كل فئة فرعية ووضع الوظيفة المفقودة في فئات فرعية جديدة، لكنك ستحتاج إلى تكرار الشيفرة في كل تلك الفئات الجديدة، وهذا أمر يطول.
والحل الأفضل لهذه الحالة هو وضع الوظائف المفقودة في فئة محول، وتأكد أن فئة الكائن الذي يغلفه المحول تطابق الفئة الأساسية للفئات الفرعية التي تريد إضافة الوظائف إليها. وهذا يسمح للمحول أن يغلف أي كائن من هرمية تلك الفئة. وستبدو الشيفرة النهائية قريبة من نمط الزائر.
كيفية الاستخدام
- تأكد أن لديك فئتين على الأقل بواجهات غير متوافقة:
- فئة خدمة (service) مفيدة لا يمكنك تغييرها (عادة ما تكون من طرف ثالث أو قديمة أو اعتماديات كثيرة موجودة مسبقًا).
- فئة client واحدة أو أكثر ستستفيد من استخدام فئة الخدمة.
- صرِّح عن واجهة العميل وصِفْ كيف سيتواصل العملاء مع هذه الخدمة.
- أنشئ فئة محول واجعلها تتبع واجهة العميل، واترك كل الأساليب فارغة للآن.
- أضف حقلًا لفئة المحول لتخزين مرجع لكائن الخدمة. السيناريو الشائع هنا هو بدء هذا الحقل بمنشئ (constructor)، لكن قد يكون من المناسب أحيانًا تمريره إلى المحول عند استدعاء أساليبه.
- استخدم كل أساليب واجهة العمل في فئة المحول واحدًا تلو الآخر. ينبغي أن يفوِّض المحول أغلب العمل الحقيقي إلى كائن الخدمة، ويعالج هو الواجهة أو تحويل صيغة البيانات.
- يجب أن يستخدم العملاء المحول من خلال واجهة العميل، سيسمح هذا لك بتغيير أو توسيع المحولات دون التأثير على شيفرة العميل.
المزايا والعيوب
المزايا
- مبدأ المسؤولية الواحدة. يمكنك فصل الواجهة أو شيفرة تحويل البيانات من المنطق التجاري الأساسي للبرنامج.
- مبدأ مفتوح/مغلق. يمكنك إدخال أنواع جديدة من المحولات في البرنامج دون تعطيل الشيفرة الحالية للبرنامج طالما أنها تعمل مع المحولات من خلال واجهة العميل.
العيوب
- زيادة التعقيد الكلي للشيفرة بسبب أنك تحتاج إلى إدخال مجموعة من الواجهات والفئات الجديدة، فأحيانًا يكون من السهل تغيير فئة الخدمة كي تطابق بقية شيفرتك.
العلاقات مع الأنماط الأخرى
يُصمم نمط الجسر (Bridge) عادة من البداية ليسمح لك بتطوير أجزاء من التطبيق بشكل مستقل عن بعضها، أما نمط المحول من الناحية الأخرى يُستخدم بكثرة مع التطبيق الموجود فعلًا ليجعل بعض الفئات غير المتوافقة تعمل مع بعضها بشكل متناغم.
يغير نمط المحول واجهة كائن موجود فعلًا، بينما يحسّن نمط المزخرِف من الكائن دون تغيير واجهته. أيضًا، يدعن المزخرف التركيب العكسي، وذلك غير متاح حين تستخدم المحول.
يوفر المحول واجهة مختلفة للكائن المُغلَّف، بينما يوفر نمط الوكيل (proxy) نفس الواجهة، أما نمط المزخرف فيوفر للكائن واجهة محسَّنة.
يعرّف نمط الواجهة (Facade) واجهة جديدة للكائنات الموجودة مسبقًا، في حين يحاول المحول جعل الواجهات الحالية قابلة للاستخدام. كذلك يغلف المحول كائنًا واحدًا في العادة، بينما يعمل نمط الواجهة مع نظام فرعي كامل لمجموعة كائنات.
تتشابه هيكلة أنماط الجسر والحالة والخطة (وأحيانًا نمط المحول) بشكل كبير، صحيح أن تلك الأنماط مبنية على التركيب- وهو تفويض المهام إلى كائنات أخرى- لكنها تحل مشاكل مختلفة. ذلك أن النمط ليس وصفة لهيكلة شيفرتك بشكل معين وحسب، فهو يبلغ المشكلة التي يحلها كذلك إلى المطورين الآخرين.
الاستخدام في لغة جافا
المستوى: ★ ☆ ☆
الانتشار: ★ ★ ★
أمثلة الاستخدام: يشيع استخدام نمط المحول في شيفرة جافا، ويستخدم عادة في الأنظمة المبنية على شيفرات قديمة، ففي تلك الحالة تكون وظيفة نمط المحول هي جعل الشيفرة القديمة تعمل مع الفئات الجديدة. إليك بعض المحولات القياسية في مكتبات جافا:
- java.util.Arrays#asList()
- java.util.Collections#list()
- java.util.Collections#enumeration()
- (java.io.InputStreamReader(InputStream (تعيد كائن
Reader
) - (java.io.OutputStreamWriter(OutputStream (تعيد كائن
Writer
) - ()javax.xml.bind.annotation.adapters.XmlAdapter#marshal و
()unmarshal#
يمكن ملاحظة نمط المحول من خلال المنشئ (constructor) الذي يأخذ نسخة من