نمط المحوِّل Adapter

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

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

المشكلة

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

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

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

الحل

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

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

  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.Structural
{
    // / <summary>
 / EN: Adapter Design Pattern
 /
 / Converts the interface of
    // a class into the interface clients expect.
 / Adapter lets classes work
    // together where they otherwise couldn't, due to incompatible interfaces.
    // / 
 / AR: نمط تصميم المحول
 / 
 / يحول واجهة
    // فئة ما إلى واجهة يتوقعها العملاء.
 / يسمح المحول للفئات بالعمل معًا
    // في المواضع التي لا تستطيع فيها ذلك بسبب الواجهات غير المتوافقة.
 / </summary>


    // / <summary>
 / EN: ITarget defines interface expected by the client.
 /
    // / AR: الواجهة التي يتوقعها العميل ITarget يحدد.
 /
    // </summary>
    interface ITarget
    {
        string GetRequest();
    }

    // / <summary>
 / EN: The Adaptee contains some useful behaviour, but its
    // interface is incompatible
 / with the existing client code.The Adaptee
    // needs some adaptation before the client code can use it.
 / 
 / AR: على بعض السلوك المفيد، Adaptee تحتوي فئة 
    // لكن واجهتها لا تتوافق  مع الشيفرة الحالية للعميل
 /إلى بعض التهيئة قبل أن تتمكن شيفرة العميل من استخدامها. Adaptee تحتاج فئة 
 / </summary>
    class Adaptee
    {
        public string GetSpecificRequest()
        {
            return "Specific request.";
        }
    }

    // / <summary>
 / EN: The Adapter makes the Adaptee's interface compatible
    // with the ITarget interface. 
 / 
 / RU: Адаптер позволяет привести
    // интерфейс Adaptee к ожидаемому клиентом интерфейсу ITarget.
 / </summary>
    class Adapter : ITarget
    {
        private readonly Adaptee _adaptee;

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

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

    class Client
    {
        public void Main()
        {
            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());
        }
    }

    class Program
    {
        static void Main()
        {
            new Client().Main();
        }
    }
}
 Output.txt: المخرجات
مع العميل Adaptee لا تتوافق واجهة،
لكن يستطيع العميل بواسطة المحول استدعاء أسلوبها.

الاستخدام في لغة 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.

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

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

 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!'.

انظر أيضًا

مصادر