نمط المحول Adapter

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

نمط المحوِّل هو نمط تصميم هيكلي يسمح للكائنات ذات الواجهات غير المتوافقة بالتعاون.

المشكلة

لا يمكنك استخدام مكتبة التحليلات كما هي لأنها تريد بيانات بصيغة لا تتوافق مع تطبيقك.

تخيل أنك تنشئ تطبيقًا لمراقبة سوق البورصة، ويحمل التطبيق بيانات البورصة من مصادر متعددة بصيغة XML ثم يعرض للمستخدم مخططات ورسومًا بيانية. ولنفرض أنك قررت في مرحلة ما أن تطور التطبيق بإدخال مكتبة تحليلات من طرف ثالث عليه، لكن تلك المكتبة لا تعمل إلا مع بيانات بصيغة JSON.

يمكنك تعديل المكتبة لتعمل مع xml، لكن ذلك قد يعطل بعض الشيفرات المعتمدة عليها، أو قد لا يكون لديك صلاحية وصول إلى شيفرة المكتبة المصدرية من الأساس فحينها يكون حتى تعديل المكتبة مستحيلًا.

الحل

يكون الحل هنا هو إنشاء محوِّل (Adapter)، وهو كائن خاص (Special Object) يحول واجهة أحد الكائنات كي يستطيع الكائن الآخر فهمها. ويغلف المحول الكائن ليخفي تعقيد التحويل الحادث وراء الستار، ولا يكون الكائن المحوَّلة واجهته مدركًا للمحوِّل، فيمكنك مثلًا تغليف كائن يعمل بالنظام المتري -مثل الكيلو متر والمتر- بمحوِّل يحوِّل كل البيانات إلى الوحدات الامبريالية مثل -القدم والأميال-.

(ش.2)

إضافة إلى ما تقدم من تحويل البيانات إلى صيغ مختلفة، فإن المحولات تيسر أيضًا من عمل الكائنات التي تختلف واجهاتها عن بعضها، إليك طريقة عملها:

  1. يحصل المحول على واجهة تتوافق مع أحد الكائنات الموجودة مسبقًا.
  2. يمكن للكائن باستخدام تلك الواجهة استدعاء أساليب المحول.
  3. عند استلام الاستدعاء، يمرِّر المحولُ الطلبَ إلى الكائن الثاني لكن بالصيغة والترتيب اللذين يتوقعهما ذلك الكائن.

من الممكن أحيانًا إنشاء محول ثنائي يحول الاستدعاءات في كلا الاتجاهين ذهابًا واستلامًا.

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

وحين يستقبل محولٌ استدعاءً فإنه يترجم بيانات XML المعطاة إلى هيكل JSON ويمرر الاستدعاء إلى الأساليب المناسبة لكائن تحليلات مغلف.

مثال واقعي

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

البُنية

محول الكائن

هذا التطبيق يستخدم مبدأ التركيب (compostition)، إذ يستخدم المحوِّل واجهة أحد الكائنات ويغلف واجهة الكائن الآخر، يمكن استخدام هذا المبدأ في كل لغات البرمجة الشائعة.

  1. فئة العميل (Client) هي فئة تحتوي على منطق العمل الحالي للبرنامج.
  2. تصف واجهة العميل بروتوكول يجب أن تتبعه الفئات الأخرى لتستطيع التفاهم مع شيفرة العميل.
  3. فئة الخدمة (Service) هي فئة مفيدة (عادة ما تكون طرفًا ثالثًا أو إصدارًا قديمًا). لا يستطيع العميل استخدام هذه الفئة مباشرة بسبب أن واجهتها لا تتوافق معه.
  4. فئة المحول (Adapter) هي فئة تستطيع العمل مع كلا من العميل والخدمة، إذ تستخدم واجهة العميل بينما تغلف كائن الخدمة. ويستقبل المحول استدعاءات من العميل من خلال واجهة المحول ويترجمها إلى استدعاءات إلى كائن الخدمة المغلَّف في صيغة يستطيع الأخير فهمها.
  5. لا ترتبط شيفرة العميل بفئة المحول الحقيقية طالما كانت تعمل مع المحول من خلال واجهة العميل، ويمكنك بفضل ذلك إدخال أنواع محولات جديدة إلى البرنامج دون تعطيل الشيفرة الحالية. هذا مفيد حين تتغير أو تُستبدل الواجهة الخاصة بفئة الخدمة، فيمكنك حينها إنشاء فئة محول جديدة دون تغيير شيفرة العميل.

محول الفئة

هذا التطبيق يستخدم أسلوب الاكتساب (inheritance)، إذ يكتسب المحول واجهات من كلا الكائنين في نفس الوقت، لاحظ أن هذا المنظور يمكن استخدامه فقط في لغات تدعم الاكتساب المتعدد مثل ++C.

ضع الصورة.

  1. لا يحتاج محول الفئة (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

قابلية الاستخدام

  • استخدم فئة المحول حين تريد أن تستخدم بعض الفئات الموجودة مسبقًا، لكن واجهتها غير متوافقة مع بقية شيفرتك.

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

  • استخدم النمط حين تريد إعادة استخدام فئات موجودة مسبقًا تفتقر إلى بعض الوظائف التي لا يمكن إضافتها إلى الفئة الأم.

يمكنك توسيع كل فئة فرعية ووضع الوظيفة المفقودة في فئات فرعية جديدة، لكنك ستحتاج إلى تكرار الشيفرة في كل تلك الفئات الجديدة، وهذا أمر يطول.

والحل الأفضل لهذه الحالة هو وضع الوظائف المفقودة في فئة محول، وتأكد أن فئة الكائن الذي يغلفه المحول تطابق الفئة الأساسية للفئات الفرعية التي تريد إضافة الوظائف إليها. وهذا يسمح للمحول أن يغلف أي كائن من هرمية تلك الفئة. وستبدو الشيفرة النهائية قريبة من نمط الزائر.

كيفية الاستخدام

  1. تأكد أن لديك فئتين على الأقل بواجهات غير متوافقة:
    • فئة خدمة (service) مفيدة لا يمكنك تغييرها (عادة ما تكون من طرف ثالث أو قديمة أو اعتماديات كثيرة موجودة مسبقًا).
    • فئة client واحدة أو أكثر ستستفيد من استخدام فئة الخدمة.
  2. صرِّح عن واجهة العميل وصِفْ كيف سيتواصل العملاء مع هذه الخدمة.
  3. أنشئ فئة محول واجعلها تتبع واجهة العميل، واترك كل الأساليب فارغة للآن.
  4. أضف حقلًا لفئة المحول لتخزين مرجع لكائن الخدمة. السيناريو الشائع هنا هو تهيئة هذا الحقل بمنشئ (constructor)، لكن قد يكون من المناسب أحيانًا تمريره إلى المحول عند استدعاء أساليبه.
  5. استخدم كل أساليب واجهة العمل في فئة المحول واحدًا تلو الآخر. ينبغي أن يفوِّض المحول أغلب العمل الحقيقي إلى كائن الخدمة، ويعالج هو الواجهة أو تحويل صيغة البيانات.
  6. يجب أن يستخدم العملاء المحول من خلال واجهة العميل، سيسمح هذا لك بتغيير أو توسيع المحولات دون التأثير على شيفرة العميل.

المزايا والعيوب

المزايا

  • مبدأ المسؤولية الواحدة. يمكنك فصل الواجهة أو شيفرة تحويل البيانات من منطق العمل الأساسي للبرنامج.
  • مبدأ مفتوح/مغلق. يمكنك إدخال أنواع جديدة من المحولات في البرنامج دون تعطيل الشيفرة الحالية للبرنامج طالما أنها تعمل مع المحولات من خلال واجهة العميل.

العيوب

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

العلاقات مع الأنماط الأخرى

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

يغير نمط المحول واجهة كائن موجود فعلًا، بينما يحسّن نمط المزخرِف من الكائن دون تغيير واجهته. أيضًا، يدعن المزخرف التركيب العكسي، وذلك غير متاح حين تستخدم المحول.

يوفر المحول واجهة مختلفة للكائن المُغلَّف، بينما يوفر نمط الوكيل (proxy) نفس الواجهة، أما نمط المزخرف فيوفر للكائن واجهة محسَّنة.

يعرّف نمط الواجهة (Facade) واجهة جديدة للكائنات الموجودة مسبقًا، في حين يحاول المحول جعل الواجهات الحالية قابلة للاستخدام. كذلك يغلف المحول كائنًا واحدًا في العادة، بينما يعمل نمط الواجهة مع نظام فرعي كامل لمجموعة كائنات.

تتشابه هيكلة أنماط الجسر والحالة والخطة (وأحيانًا نمط المحول) بشكل كبير، صحيح أن تلك الأنماط مبنية على التركيب- وهو تفويض المهام إلى كائنات أخرى- لكنها تحل مشاكل مختلفة. ذلك أن النمط ليس وصفة لهيكلة شيفرتك بشكل معين وحسب، فهو يبلغ المشكلة التي يحلها كذلك إلى المطورين الآخرين.

الاستخدام في لغة جافا

المستوى: ★ ☆ ☆

الانتشار:  ★ ★ ★

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

يمكن ملاحظة نمط المحول من خلال المنشئ (constructor) الذي يأخذ نسخة من نوع مجرد/واجهة مختلف، فحين يستقبل المحول استدعاءً إلى أي من أساليبه فإنه يترجم المعامِلات (parameters) إلى الصيغة المناسبة ويعيد توجيه الاستدعاء إلى أسلوب أو أكثر من أساليب الكائن المٌغلَّف.

مثال: إدخال أوتاد مربعة في ثقوب دائرية

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

دائري

 round/RoundHole.java: ثقوب دائرية
package refactoring_guru.adapter.example.round;

/**
 * RoundHoles متوافقة مع RoundPegs.
 */
public class RoundHole {
    private double radius;

    public RoundHole(double radius) {
        this.radius = radius;
    }

    public double getRadius() {
        return radius;
    }

    public boolean fits(RoundPeg peg) {
        boolean result;
        result = (this.getRadius() >= peg.getRadius());
        return result;
    }
}
 round/RoundPeg.java: أوتاد دائرية
package refactoring_guru.adapter.example.round;

/**
 * RoundPegs متوافقة مع RoundHoles.
 */
public class RoundPeg {
    private double radius;

    public RoundPeg() {}

    public RoundPeg(double radius) {
        this.radius = radius;
    }

    public double getRadius() {
        return radius;
    }
}

مربع

 square/SquarePeg.java: أوتاد مربعة
package refactoring_guru.adapter.example.square;

/**
 * SquarePegs لا تتوافق مع RoundHoles
 *إذ تم استخدامهما من قبل فريق تطوير سابق، لكننا مضطرون لاستخدامهما في
 * برنامجنا.
 */
public class SquarePeg {
    private double width;

    public SquarePeg(double width) {
        this.width = width;
    }

    public double getWidth() {
        return width;
    }

    public double getSquare() {
        double result;
        result = Math.pow(this.width, 2);
        return result;
    }
}

المحولات

 adapters/SquarePegAdapter.java: محول الأوتاد المربعة إلى الثقوب الدائرية
package refactoring_guru.adapter.example.adapters;

import refactoring_guru.adapter.example.round.RoundPeg;
import refactoring_guru.adapter.example.square.SquarePeg;

/**
 * يسمح المحول بإدخال أوتاد مربعة داخل ثقوب دائرية.
 */
public class SquarePegAdapter extends RoundPeg {
    private SquarePeg peg;

    public SquarePegAdapter(SquarePeg peg) {
        this.peg = peg;
    }

    @Override
    public double getRadius() {
        double result;
        // احسب أقل قطر دائرة يمكن أن تحتوي هذا الوتد.
        result = (Math.sqrt(Math.pow((peg.getWidth() / 2), 2) * 2));
        return result;
    }
}
 Demo.java: شيفرة العميل
package refactoring_guru.adapter.example;

import refactoring_guru.adapter.example.adapters.SquarePegAdapter;
import refactoring_guru.adapter.example.round.RoundHole;
import refactoring_guru.adapter.example.round.RoundPeg;
import refactoring_guru.adapter.example.square.SquarePeg;

/**
 * في مكان ما في شيفرة العميل.
 */
public class Demo {
    public static void main(String[] args) {
        // الدائري يوافق الدائري.
        RoundHole hole = new RoundHole(5);
        RoundPeg rpeg = new RoundPeg(5);
        if (hole.fits(rpeg)) {
            System.out.println("Round peg r5 fits round hole r5.");
        }

        SquarePeg smallSqPeg = new SquarePeg(2);
        SquarePeg largeSqPeg = new SquarePeg(20);
        // hole.fits(smallSqPeg); // لن تُجمَّع.

        // يحل المحول المشكلة.
        SquarePegAdapter smallSqPegAdapter = new SquarePegAdapter(smallSqPeg);
        SquarePegAdapter largeSqPegAdapter = new SquarePegAdapter(largeSqPeg);
        if (hole.fits(smallSqPegAdapter)) {
            System.out.println("Square peg w2 fits round hole r5.");
        }
        if (!hole.fits(largeSqPegAdapter)) {
            System.out.println("Square peg w20 does not fit into round hole r5.");
        }
    }
}
 OutputDemo.txt: نتائج التنفيذ
Round peg r5 fits round hole r5.
Square peg w2 fits round hole r5.
Square peg w20 does not fit into round hole r5.

الاستخدام في لغة #C

المستوى: ★ ☆ ☆

الانتشار:  ★ ★ ★

أمثلة الاستخدام: يشيع استخدام نمط المحول في شيفرة #C، ويستخدم عادة في الأنظمة المبنية على شيفرات قديمة، ففي تلك الحالة تكون وظيفة نمط المحول هي جعل الشيفرة القديمة تعمل مع الفئات الجديدة.

يمكن ملاحظة نمط المحول من خلال المنشئ (constructor) الذي يأخذ نسخة من نوع مجرد/واجهة مختلف، فحين يستقبل المحول استدعاءً إلى أي من أساليبه فإنه يترجم المعامِلات (parameters) إلى الصيغة المناسبة ويعيد توجيه الاستدعاء إلى أسلوب أو أكثر من أساليب الكائن المٌغلَّف.

مثال: بنية النمط

يوضح هذا المثال بنية نمط المحول، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

 Program.cs: مثال هيكلي

using System;

namespace RefactoringGuru.DesignPatterns.Adapter.Conceptual
{
    // domain-specific interface يحدد الهدف الواجهة المخصصة للنطاق
    // التي تستخدمها شيفرة العميل.
    public interface ITarget
    {
        string GetRequest();
    }
    // على بعض السلوك المفيد، لكن واجهتها Adaptee تحتوي واجهة 
    // لا تتوافق مع شيفرة العميل الحالية.
    // بعض التعديل قبل أن تستطيع شيفرة Adaptee تحتاج فئة 
    // العميل استخدامها.
    class Adaptee
    {
        public string GetSpecificRequest()
        {
            return "Specific request.";
        }
    }

    // متوافقة مع واجهة الهدف Adaptee يجعل المحول واجهة.
    class Adapter : ITarget
    {
        private readonly Adaptee _adaptee;

        public Adapter(Adaptee adaptee)
        {
            this._adaptee = adaptee;
        }

        public string GetRequest()
        {
            return $"This is '{this._adaptee.GetSpecificRequest()}'";
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Adaptee adaptee = new Adaptee();
            ITarget target = new Adapter(adaptee);

            Console.WriteLine("Adaptee interface is incompatible with the client.");
            Console.WriteLine("But with adapter client can call it's method.");

            Console.WriteLine(target.GetRequest());
        }
    }
}
 Output.txt: المخرجات
Adaptee interface is incompatible with the client.
But with adapter client can call it's method.
This is 'Specific request.'

الاستخدام في لغة PHP

المستوى: ★ ☆ ☆

الانتشار:  ★ ★ ★

أمثلة الاستخدام: يشيع استخدام نمط المحول في شيفرة PHP، ويستخدم عادة في الأنظمة المبنية على شيفرات قديمة، ففي تلك الحالة تكون وظيفة نمط المحول هي جعل الشيفرة القديمة تعمل مع الفئات الجديدة.

مثال: بنية النمط

يوضح هذا المثال بنية نمط المحول، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

سيكون من السهل عليك بعد تعلم بنية النمط أن تستوعب المثال التالي المبني على حالة استخدام واقعية.

 AdapterStructural.php: مثال هيكلي

<?php

namespace RefactoringGuru\Adapter\Structural;

/**
 * يحدد الهدف الواجهة المخصصة للنطاق، والتي تستخدمها شيفرة العميل.
 */
class Target
{
    public function request(): string
    {
        return "Target: The default target's behavior.";
    }
}

/**
 * على بعض السلوك المفيد Adaptee تحتوي فئة
 * لكن واجهتها لا تتوافق مع شيفرة العميل الحالية، 
 * وتحتاج بعض التهيئة قبل أن تستخدمها شيفرة العميل
 */
class Adaptee
{
    public function specificRequest(): string
    {
        return ".eetpadA eht fo roivaheb laicepS";
    }
}

/**
 * متوافقة مع Adaptee يجعل المحول واجهة فئة 
 * واجهة الهدف.
 */
class Adapter extends Target
{
    private $adaptee;

    public function __construct(Adaptee $adaptee)
    {
        $this->adaptee = $adaptee;
    }

    public function request(): string
    {
        return "Adapter: (TRANSLATED) ".strrev($this->adaptee->specificRequest());
    }
}

/**
 * تدعم شيفرة العميل كل الفئات التي تتبع واجهة الهدف.
 */
function clientCode(Target $target)
{
    echo $target->request();
}

echo "Client: I can work just fine with the Target objects:\n";
$target = new Target;
clientCode($target);
echo "\n\n";

$adaptee = new Adaptee;
echo "Client: The Adaptee class has a weird interface. See, I don't understand it:\n";
echo "Adaptee: " . $adaptee->specificRequest();
echo "\n\n";

echo "Client: But I can work with it via the Adapter:\n";
$adapter = new Adapter($adaptee);
clientCode($adapter);
 Output.txt: المخرجات
Client: I can work just fine with the Target objects:
Target: The default target's behavior.

Client: The Adaptee class has a weird interface. See, I don't understand it:
Adaptee: .eetpadA eht fo roivaheb laicepS

Client: But I can work with it via the Adapter:
Adapter: (TRANSLATED) Special behavior of the Adaptee.

مثال: حالة حقيقية

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

 AdapterRealWorld.php: حالة حقيقية

<?php

namespace RefactoringGuru\Adapter\RealWorld;

/**
 * نمط تصميم المحول
 * 
 * الهدف: تحويل واجهة فئة ما إلى واجهة يتوقعها العملاء.
 * يسمح المحول للفئات أن تعمل معًا في المواطن التي لا تستطيع
 * ذلك بدونه بسبب الواجهات غير المتوافقة.
 *
 * مثال: يسمح نمط المحول لك باستخدام فئات من طرف ثالث أو
 * قديمة حتى لو كانت غير متوافقة مع أغلب  شيفرتك، فمثلًا بدلًا 
 * من إعادة كتابة واجهة إشعارات لتطبيقك كي تدعم 
 * خدمة من طرف ثالث مثل سلاك أو فيس بوك أو الرسائل القصيرة
 * (special wrappers) يمكنك بدلًا من ذلك أن تنشئ مجموعة من المُغلِّفات الخاصة 
 *  تكيّف الاستدعاءات القادمة من تطبيقك إلى واجهة ما، وتهيئ المطلوب من كل فئة من طرف ثالث.  
 */

/**
 * تمثل واجهة الهدف الواجهة التي تتبعها فئات تطبيقك.
 */
interface Notification
{
    public function send(string $title, string $message);
}

/**
 * إليك مثالًا عن فئة موجودة فعلًا وتتبع واجهة الهدف
 *
 * لا تكون هذه الواجهة معرَّفة بوضوح في العديد من التطبيقات 
 * الحقيقية، وإن كانت تلك حالتك فأفضل خيار لك سيكون توسيع
 * المحول من أحد فئات تطبيقك الموجودة فعلًا.
 * SlackNotification وقد يبدو ذلك غريبًا أحيانًا، ففئة
 * EmailNotification لن يكون مناسبًا وضعها كفئة فرعية لفئة
 * وفي تلك الحالة يجب أن تكون خطوتك الأولى هي استخراج واجهة.
 */
class EmailNotification implements Notification
{
    private $adminEmail;

    public function __construct(string $adminEmail)
    {
        $this->adminEmail = $adminEmail;
    }

    public function send(string $title, string $message): void
    {
        mail($this->adminEmail, $title, $message);
        echo "Sent email with title '$title' to '{$this->adminEmail}' that says '$message'.";
    }
}

/**
 * مفيدة ولكنها غير متوافقة مع واجهة الهدف Adaptee فئة
 * ولا يمكنك تغيير شيفرة الفئة لتتبع واجهة الهدف ببساطة، بما
 * أن الشيفرة قد تكون آتية من مكتبة من طرف ثالث.
 */
class SlackApi
{
    private $login;
    private $apiKey;

    public function __construct(string $login, string $apiKey)
    {
        $this->login = $login;
        $this->apiKey = $apiKey;
    }

    public function logIn(): void
    {
        // Slack web أرسل طلب توثيق إلى خدمة
        echo "Logged in to a slack account '{$this->login}'.\n";
    }

    public function sendMessage(string $chatId, string $message): void
    {
        // Slack web أرسل رسالة بعد الطلب إلى خدمة.
        echo "Posted following message into the '$chatId' chat: '$message'.\n";
    }
}

/**
 * وواجهة الهدف Adaptee المحول هو فئة يربط بين فئة
 * في تلك الحالة فإنه يسمح للتطبيق أن يرسل إشعارات باستخدام
 * الخاصة بسلاك API واجهة.
 */
class SlackNotification implements Notification
{
    private $slack;
    private $chatId;

    public function __construct(SlackApi $slack, string $chatId)
    {
        $this->slack = $slack;
        $this->chatId = $chatId;
    }

    /**
     * إضافة لتكييف الواجهات معًا، فإن المحول يحول البيانات
     * Adaptee القادمة إلى الصيغة المطلوبة من فئة
     */
    public function send(string $title, string $message): void
    {
        $slackMessage = "#" . $title . "# " . strip_tags($message);
        $this->slack->logIn();
        $this->slack->sendMessage($this->chatId, $slackMessage);
    }
}

/**
 *تعمل شيفرة العميل مع أي فئة تتبع واجهة الهدف.
 */
function clientCode(Notification $notification)
{
    // ...

    echo $notification->send("Website is down!",
        "<strong style='color:red;font-size: 50px;'>Alert!</strong> " .
        "Our website is not responding. Call admins and bring it up!");

    // ...
}

echo "Client code is designed correctly and works with email notifications:\n";
$notification = new EmailNotification("developers@example.com");
clientCode($notification);
echo "\n\n";


echo "The same client code can work with other classes via adapter:\n";
$slackApi = new SlackApi("example.com", "XXXXXXXX");
$notification = new SlackNotification($slackApi, "Example.com Developers");
clientCode($notification);
 Output.txt: المخرجات
Client code is designed correctly and works with email notifications:
Sent email with title 'Website is down!' to 'developers@example.com' that says '<strong style='color:red;font-size: 50px;'>Alert!</strong> Our website is not responding. Call admins and bring it up!'.

The same client code can work with other classes via adapter:
Logged in to a slack account 'example.com'.
Posted following message into the 'Example.com Developers' chat: '#Website is down!# Alert! Our website is not responding. Call admins and bring it up!'.

الاستخدام في لغة بايثون

المستوى: ★ ☆ ☆

الانتشار:  ★ ★ ★

أمثلة الاستخدام: يشيع استخدام نمط المحول في شيفرة بايثون، ويستخدم عادة في الأنظمة المبنية على شيفرات قديمة، ففي تلك الحالة تكون وظيفة نمط المحول هي جعل الشيفرة القديمة تعمل مع الفئات الجديدة.

يمكن ملاحظة نمط المحول من خلال المنشئ (constructor) الذي يأخذ نسخة من نوع مجرد/واجهة مختلف، فحين يستقبل المحول استدعاءً إلى أي من أساليبه فإنه يترجم المعامِلات (parameters) إلى الصيغة المناسبة ويعيد توجيه الاستدعاء إلى أسلوب أو أكثر من أساليب الكائن المٌغلَّف.

مثال: بنية النمط

يوضح هذا المثال بنية نمط المحول، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

 main.py: مثال تصوري

class Target():
    """
    يحدد الهدف الواجهة المخصصة للنطاق، والتي تستخدمها شيفرة العميل.
    """

    def request(self) -> str:
        return "Target: The default target's behavior."


class Adaptee:
    """
    على بعض السلوك المفيد Adaptee تحتوي فئة
    لكن واجهتها لا تتوافق مع شيفرة العميل الحالية، 
    وتحتاج بعض التهيئة قبل أن تستخدمها شيفرة العميل
    """

    def specific_request(self) -> str:
        return ".eetpadA eht fo roivaheb laicepS"


class Adapter(Target):
    """
    متوافقة مع Adaptee يجعل المحول واجهة فئة
    واجهة الهدف.
    """

    def __init__(self, adaptee: Adaptee) -> None:
        self.adaptee = adaptee

    def request(self) -> str:
        return f"Adapter: (TRANSLATED) {self.adaptee.specific_request()[::-1]}"


def client_code(target: Target) -> None:
    """
    تدعم شيفرة العميل كل الفئات التي تتبع واجهة الهدف.
    """

    print(target.request(), end="")


if __name__ == "__main__":
    print("Client: I can work just fine with the Target objects:")
    target = Target()
    client_code(target)
    print("\n")

    adaptee = Adaptee()
    print("Client: The Adaptee class has a weird interface. See, I don't understand it:")
    print(f"Adaptee: {adaptee.specific_request()}", end="\n\n")

    print("Client: But I can work with it via the Adapter:")
    adapter = Adapter(adaptee)
    client_code(adapter)

 Output.txt: نتائج التنفيذ

Client: I can work just fine with the Target objects:
Target: The default target's behavior.

Client: The Adaptee class has a weird interface. See, I don't understand it:
Adaptee: .eetpadA eht fo roivaheb laicepS

Client: But I can work with it via the Adapter:
Adapter: (TRANSLATED) Special behavior of the Adaptee.

الاستخدام في لغة روبي

المستوى: ★ ☆ ☆

الانتشار:  ★ ★ ★

أمثلة الاستخدام: يشيع استخدام نمط المحول في شيفرة روبي، ويستخدم عادة في الأنظمة المبنية على شيفرات قديمة، ففي تلك الحالة تكون وظيفة نمط المحول هي جعل الشيفرة القديمة تعمل مع الفئات الجديدة.

يمكن ملاحظة نمط المحول من خلال المنشئ (constructor) الذي يأخذ نسخة من نوع مجرد/واجهة مختلف، فحين يستقبل المحول استدعاءً إلى أي من أساليبه فإنه يترجم المعامِلات (parameters) إلى الصيغة المناسبة ويعيد توجيه الاستدعاء إلى أسلوب أو أكثر من أساليب الكائن المٌغلَّف.

مثال: بنية النمط

يوضح هذا المثال بنية نمط المحول، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

 main.rb: مثال تصوري

# يحدد الهدف الواجهة المخصصة للنطاق، والتي تستخدمها شيفرة العميل.
class Target
  # @return [String]
  def request
    'Target: The default target\'s behavior.'
  end
end

# على بعض السلوك المفيد Adaptee تحتوي فئة
# لكن واجهتها لا تتوافق مع شيفرة العميل الحالية،
# وتحتاج بعض التهيئة قبل أن تستخدمها شيفرة العميل
class Adaptee
  # @return [String]
  def specific_request
    '.eetpadA eht fo roivaheb laicepS'
  end
end

# متوافقة مع Adaptee يجعل المحول واجهة فئة
# واجهة الهدف.
class Adapter < Target
  # @param [Adaptee] adaptee
  def initialize(adaptee)
    @adaptee = adaptee
  end

  def request
    "Adapter: (TRANSLATED) #{@adaptee.specific_request.reverse!}"
  end
end

# تدعم شيفرة العميل كل الفئات التي تتبع واجهة الهدف.
def client_code(target)
  print target.request
end

puts 'Client: I can work just fine with the Target objects:'
target = Target.new
client_code(target)
puts "\n\n"

adaptee = Adaptee.new
puts 'Client: The Adaptee class has a weird interface. See, I don\'t understand it:'
puts "Adaptee: #{adaptee.specific_request}"
puts "\n"

puts 'Client: But I can work with it via the Adapter:'
adapter = Adapter.new(adaptee)
client_code(adapter)

 output.txt: نتائج التنفيذ

Client: I can work just fine with the Target objects:
Target: The default target's behavior.

Client: The Adaptee class has a weird interface. See, I don't understand it:
Adaptee: .eetpadA eht fo roivaheb laicepS

Client: But I can work with it via the Adapter:
Adapter: (TRANSLATED) Special behavior of the Adaptee.

الاستخدام في لغة Swift

المستوى: ★ ☆ ☆

الانتشار:  ★ ★ ★

أمثلة الاستخدام: يشيع استخدام نمط المحول في شيفرة Swift، ويستخدم عادة في الأنظمة المبنية على شيفرات قديمة، ففي تلك الحالة تكون وظيفة نمط المحول هي جعل الشيفرة القديمة تعمل مع الفئات الجديدة.

يمكن ملاحظة نمط المحول من خلال المنشئ (constructor) الذي يأخذ نسخة من نوع مجرد/واجهة مختلف، فحين يستقبل المحول استدعاءً إلى أي من أساليبه فإنه يترجم المعامِلات (parameters) إلى الصيغة المناسبة ويعيد توجيه الاستدعاء إلى أسلوب أو أكثر من أساليب الكائن المٌغلَّف.

مثال: بنية النمط

يوضح هذا المثال بنية نمط المحول، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

بعد تعلم بنية النمط سيكون من السهل عليك استيعاب المثال التالي المبني على حالة واقعية في لغة Swift.

 Example.swift: مثال تصوري

import XCTest

/// يحدد الهدف الواجهة المخصصة للنطاق، والتي تستخدمها شيفرة العميل.
class Target {

    func request() -> String {
        return "Target: The default target's behavior."
    }
}

/// على بعض السلوك المفيد Adaptee تحتوي فئة
/// لكن واجهتها لا تتوافق مع شيفرة العميل الحالية،
/// وتحتاج بعض التهيئة قبل أن تستخدمها شيفرة العميل
class Adaptee {

    public func specificRequest() -> String {
        return ".eetpadA eht fo roivaheb laicepS"
    }
}

/// متوافقة مع Adaptee يجعل المحول واجهة فئة
/// واجهة الهدف.
class Adapter: Target {

    private var adaptee: Adaptee

    init(_ adaptee: Adaptee) {
        self.adaptee = adaptee
    }

    override func request() -> String {
        return "Adapter: (TRANSLATED) " + adaptee.specificRequest().reversed()
    }
}

/// تدعم شيفرة العميل كل الفئات التي تتبع واجهة الهدف.
class Client {
    // ...
    static func someClientCode(target: Target) {
        print(target.request())
    }
    // ...
}

/// لنرى الآن كيف سيعمل كل ذلك...
class AdapterConceptual: XCTestCase {

    func testAdapterConceptual() {
        print("Client: I can work just fine with the Target objects:")
        Client.someClientCode(target: Target())

        let adaptee = Adaptee()
        print("Client: The Adaptee class has a weird interface. See, I don't understand it:")
        print("Adaptee: " + adaptee.specificRequest())

        print("Client: But I can work with it via the Adapter:")
        Client.someClientCode(target: Adapter(adaptee))
    }
}

 Output.txt: نتائج التنفيذ

Client: I can work just fine with the Target objects:
Target: The default target's behavior.
Client: The Adaptee class has a weird interface. See, I don't understand it:
Adaptee: .eetpadA eht fo roivaheb laicepS
Client: But I can work with it via the Adapter:
Adapter: (TRANSLATED) Special behavior of the Adaptee.

مثال واقعي

 Example.swift: مثال واقعي

import XCTest
import UIKit

/// نمط تصميم المحول
///
/// الهدف: تحويل واجهة فئة ما إلى واجهة يتوقعها العملاء.
/// يسمح المحول للفئات أن تعمل معًا في المواطن التي لا تستطيع
/// ذلك بدونه بسبب الواجهات غير المتوافقة.

class AdapterRealWorld: XCTestCase {

    /// مثال: لنقل أن برنامجنا يعمل بكفاءة مع استيثاق فيس بوك، لكن
    /// يطلب المستخدمون منك إضافة تسجيل الدخول عبر تويتر.
    /// 
    /// الخاصة بتويتر لديها أسلوب استيثاق مختلف SDK لكن حزمة التطوير.
    /// 
    /// وتُدخل أسلوب الاستيثاق AuthService يجب أن تنشئ أولًا بروتوكولًا جديدًا اسمه
    /// من فيس بوك SDK الخاص بحزمة.
    /// 
    /// AuthService واستخدم أساليب بروتوكول Twitter SDK ثانيًا، اكتب إضافةً لحزمة 
    /// كإعادة توجيه بسيطة.
    /// 
    /// يجب ألا تكتب أي شيفرة عند هذه المرحلة ، Facebook SDK ثالثًا، اكتب إضافةً لحزمة
    /// قد استخدمت الأساليب بالفعل Facebook SDK إذ أن حزمة.
    /// 
    /// لديهما نفس الواجهة SDK فهي تخبر المجمِّع أن حزمتي الـ.

    func testAdapterRealWorld() {

        print("Starting an authorization via Facebook")
        startAuthorization(with: FacebookAuthSDK())

        print("Starting an authorization via Twitter.")
        startAuthorization(with: TwitterAuthSDK())
    }

    func startAuthorization(with service: AuthService) {

        /// الحالي للبرنامج top view controller  متحكِّم الرؤية العلوية.
        let topViewController = UIViewController()

        service.presentAuthFlow(from: topViewController)
    }
}

protocol AuthService {

    func presentAuthFlow(from viewController: UIViewController)
}

class FacebookAuthSDK {

    func presentAuthFlow(from viewController: UIViewController) {
        /// Call SDK methods and pass a view controller
        print("Facebook WebView has been shown.")
    }
}

class TwitterAuthSDK {

    func startAuthorization(with viewController: UIViewController) {
        /// view controller ومرر متحكِّم رؤية SDK استدع أسلوب
        print("Twitter WebView has been shown. Users will be happy :)")
    }
}

extension TwitterAuthSDK: AuthService {

    /// هذا محوّل.
    ///
    /// نحن نستطيع توسيع الفئة الحالية فقط دون الحاجة
    /// لإنشاء واحدة جديدة.

    func presentAuthFlow(from viewController: UIViewController) {
        print("The Adapter is called! Redirecting to the original method...")
        self.startAuthorization(with: viewController)
    }
}

extension FacebookAuthSDK: AuthService {
    /// لديهما نفس الواجهة SDK تخبر هذه الإضافةُ المجمِّعَ أن كلا من حزمتي.
}

 Output.txt: نتائج التنفيذ

Starting an authorization via Facebook
Facebook WebView has been shown
///
Starting an authorization via Twitter
The Adapter is called! Redirecting to the original method...
Twitter WebView has been shown. Users will be happy :)

الاستخدام في لغة TypeScript

المستوى: ★ ☆ ☆

الانتشار:  ★ ★ ★

أمثلة الاستخدام: يشيع استخدام نمط المحول في شيفرة TypeScript، ويستخدم عادة في الأنظمة المبنية على شيفرات قديمة، ففي تلك الحالة تكون وظيفة نمط المحول هي جعل الشيفرة القديمة تعمل مع الفئات الجديدة.

يمكن ملاحظة نمط المحول من خلال المنشئ (constructor) الذي يأخذ نسخة من نوع مجرد/واجهة مختلف، فحين يستقبل المحول استدعاءً إلى أي من أساليبه فإنه يترجم المعامِلات (parameters) إلى الصيغة المناسبة ويعيد توجيه الاستدعاء إلى أسلوب أو أكثر من أساليب الكائن المٌغلَّف.

مثال: بنية النمط

يوضح هذا المثال بنية نمط المحول، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

 index.ts: مثال تصوري

/**
 * يحدد الهدف الواجهة المخصصة للنطاق، والتي تستخدمها شيفرة العميل.
 */
class Target {
    public request(): string {
        return 'Target: The default target\'s behavior.';
    }
}

/**
 * على بعض السلوك المفيد Adaptee تحتوي فئة
 * لكن واجهتها لا تتوافق مع شيفرة العميل الحالية،
 * وتحتاج بعض التهيئة قبل أن تستخدمها شيفرة العميل
 */
class Adaptee {
    public specificRequest(): string {
        return '.eetpadA eht fo roivaheb laicepS';
    }
}

/**
 * متوافقة مع Adaptee يجعل المحول واجهة فئة
 * واجهة الهدف.
 */
class Adapter extends Target {
    private adaptee: Adaptee;

    constructor(adaptee: Adaptee) {
        super();
        this.adaptee = adaptee;
    }

    public request(): string {
        const result = this.adaptee.specificRequest().split('').reverse().join('');
        return `Adapter: (TRANSLATED) ${result}`;
    }
}

/**
 * تدعم شيفرة العميل كل الفئات التي تتبع واجهة الهدف.
 */
function clientCode(target: Target) {
    console.log(target.request());
}

console.log('Client: I can work just fine with the Target objects:');
const target = new Target();
clientCode(target);

console.log('');

const adaptee = new Adaptee();
console.log('Client: The Adaptee class has a weird interface. See, I don\'t understand it:');
console.log(`Adaptee: ${adaptee.specificRequest()}`);

console.log('');

console.log('Client: But I can work with it via the Adapter:');
const adapter = new Adapter(adaptee);
clientCode(adapter);

 Output.txt: نتائج التنفيذ

Client: I can work just fine with the Target objects:
Target: The default target's behavior.

Client: The Adaptee class has a weird interface. See, I don't understand it:
Adaptee: .eetpadA eht fo roivaheb laicepS

Client: But I can work with it via the Adapter:
Adapter: (TRANSLATED) Special behavior of the Adaptee.

انظر أيضًا

مصادر