Menu
Coddy logo textTech
flag Ar iconالعربيةdown icon

نمط المحول

جزء من قسم Object Oriented Programming في رحلة C++ على Coddy — الدرس 97 من 104.

يسمح نمط المحول (Adapter pattern) للواجهات غير المتوافقة بالعمل معاً. فهو يعمل كجسر بين فئتين، حيث يقوم بتحويل واجهة فئة واحدة إلى واجهة يتوقعها العملاء. وهذا مفيد بشكل خاص عند دمج الكود القديم (legacy code) أو مكتبات الطرف الثالث التي لا تتوافق مع واجهة نظامك.

تخيل أن لديك فئة (class) موجودة بالفعل تقوم بما تحتاجه، ولكن واجهتها (interface) لا تتطابق مع ما يتوقعه الكود الخاص بك. بدلاً من تعديل أي من الطرفين، تقوم بإنشاء محول (adapter) يغلف الفئة الموجودة ويقوم بترجمة الاستدعاءات:

#include <iostream>
#include <string>

// Existing class with incompatible interface
// فئة موجودة بواجهة غير متوافقة
class LegacyPrinter {
public:
    void printMessage(const std::string& msg) {
        std::cout << "Legacy: " << msg << "\n";
    }
};

// Target interface that client expects
// الواجهة المستهدفة التي يتوقعها العميل
class Printer {
public:
    virtual void print(const std::string& text) = 0;
    virtual ~Printer() = default;
};

// Adapter wraps LegacyPrinter and implements Printer interface
// المحول (Adapter) يغلف LegacyPrinter وينفذ واجهة Printer
class PrinterAdapter : public Printer {
    LegacyPrinter& legacy;
public:
    PrinterAdapter(LegacyPrinter& lp) : legacy(lp) {}
    
    void print(const std::string& text) override {
        legacy.printMessage(text);  // Translate the call / ترجمة الاستدعاء
    }
};

int main() {
    LegacyPrinter oldPrinter;
    PrinterAdapter adapter(oldPrinter);
    
    // Client uses the expected interface
    // العميل يستخدم الواجهة المتوقعة
    adapter.print("Hello World");
}

يحتفظ المحول (adapter) بمرجع إلى LegacyPrinter ويقوم بتنفيذ واجهة Printer. عندما يتم استدعاء print()، فإنه يقوم بتفويض المهمة إلى printMessage(). يعمل كود العميل مع واجهة Printer دون معرفة التنفيذ القديم (legacy implementation) الموجود في الخلفية.

استخدم Adapter عندما تريد استخدام فئة (class) موجودة ولكن واجهتها (interface) لا تتوافق مع ما تحتاجه، أو عندما تحتاج إلى إنشاء فئة قابلة لإعادة الاستخدام تتعاون مع فئات ذات واجهات غير متوافقة.

challenge icon

التحدي

سهل

لنقم ببناء نظام محول مشغل الوسائط (Media Player Adapter) الذي يربط بين واجهات مكتبات الصوت المختلفة. تخيل أنك تقوم بدمج مكتبة صوتية قديمة في تطبيق مشغل وسائط حديث - المكتبة القديمة تعمل بشكل مثالي، لكن واجهتها لا تتطابق مع ما يتوقعه مشغلك. هذا هو بالضبط المكان الذي يتألق فيه نمط المحول (Adapter pattern).

ستقوم بتنظيم الكود الخاص بك عبر أربعة ملفات:

  • MediaPlayer.h: حدد الواجهة المستهدفة التي يتوقعها مشغل الوسائط الحديث الخاص بك.

    أنشئ فئة مجردة (abstract class) باسم MediaPlayer مع هذه الدوال الافتراضية البحتة (pure virtual methods):

    • play(const std::string& filename) — لتشغيل الملف المحدد
    • stop() — لإيقاف التشغيل
    • setVolume(int level) — لضبط مستوى الصوت (0-100)

    قم بتضمين مدمر افتراضي (virtual destructor).

  • LegacyAudioLib.h: أنشئ المكتبة الصوتية "القديمة" الموجودة حالياً بواجهة غير متوافقة.

    تمثل فئة LegacyAudioLib مكتبة قديمة تعمل بشكل مختلف عما يتوقعه مشغلك. يجب أن تحتوي على:

    • loadAudio(const std::string& path) — تطبع Legacy: Loading [path]
    • startPlayback() — تطبع Legacy: Playback started
    • stopPlayback() — تطبع Legacy: Playback stopped
    • adjustVolume(double percentage) — تأخذ قيمة عشرية (0.0 إلى 1.0) وتطبع Legacy: Volume set to [percentage] (أظهر القيمة العشرية)
  • AudioAdapter.h: قم ببناء المحول الذي يربط بين الواجهتين.

    يجب أن ترث فئة AudioAdapter من MediaPlayer وتحتفظ بمرجع لكائن LegacyAudioLib. يقوم المحول بترجمة الاستدعاءات من الواجهة الحديثة إلى الواجهة القديمة:

    • play() يجب أن تستدعي كلاً من loadAudio() و startPlayback() في المكتبة القديمة
    • stop() يجب أن تفوض المهمة إلى stopPlayback()
    • setVolume() تستقبل عدداً صحيحاً (0-100) ولكن المكتبة القديمة تتوقع قيمة عشرية (0.0-1.0)، لذا ستحتاج إلى تحويلها
  • main.cpp: استعرض المحول أثناء العمل.

    اقرأ مدخلين:

    1. اسم ملف لتشغيله (string)
    2. مستوى الصوت (integer، 0-100)

    أنشئ مثيلاً (instance) من LegacyAudioLib ومحول AudioAdapter يغلفه. ثم استخدم المحول من خلال واجهة MediaPlayer (قم بتخزينه كمؤشر MediaPlayer) للقيام بما يلي:

    1. ضبط مستوى الصوت إلى المستوى المدخل
    2. تشغيل اسم الملف المدخل
    3. إيقاف التشغيل

على سبيل المثال، مع المدخلات song.mp3 و 75:

Legacy: Volume set to 0.75
Legacy: Loading song.mp3
Legacy: Playback started
Legacy: Playback stopped

مع المدخلات podcast.wav و 50:

Legacy: Volume set to 0.5
Legacy: Loading podcast.wav
Legacy: Playback started
Legacy: Playback stopped

لاحظ كيف يعمل كود العميل بالكامل مع واجهة MediaPlayer — ليس لديه أي فكرة عن أن مكتبة قديمة هي التي تقوم بالعمل الفعلي في الخلفية. يتعامل المحول مع كل عمليات الترجمة، بما في ذلك تحويل مستوى الصوت من عدد صحيح إلى قيمة عشرية. هذا هو جوهر نمط المحول: جعل الواجهات غير المتوافقة تعمل معاً دون تعديل أي من الطرفين.

ورقة مرجعية

يسمح نمط المحول (Adapter pattern) للواجهات غير المتوافقة بالعمل معاً من خلال العمل كجسر بين فئتين. فهو يقوم بتحويل واجهة فئة معينة إلى واجهة يتوقعها العملاء.

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

الهيكل

يتكون النمط من ثلاثة مكونات رئيسية:

  • الواجهة المستهدفة (Target Interface): الواجهة التي يتوقعها العميل
  • المُحول إليه (Adaptee): الفئة الموجودة ذات الواجهة غير المتوافقة
  • المحول (Adapter): يغلف المُحول إليه وينفذ الواجهة المستهدفة، ويقوم بترجمة الاستدعاءات بينهما

مثال

#include <iostream>
#include <string>

// الفئة الموجودة ذات الواجهة غير المتوافقة (Adaptee)
class LegacyPrinter {
public:
    void printMessage(const std::string& msg) {
        std::cout << "Legacy: " << msg << "\n";
    }
};

// الواجهة المستهدفة التي يتوقعها العميل
class Printer {
public:
    virtual void print(const std::string& text) = 0;
    virtual ~Printer() = default;
};

// يقوم المحول بتغليف LegacyPrinter وينفذ واجهة Printer
class PrinterAdapter : public Printer {
    LegacyPrinter& legacy;
public:
    PrinterAdapter(LegacyPrinter& lp) : legacy(lp) {}
    
    void print(const std::string& text) override {
        legacy.printMessage(text);  // ترجمة الاستدعاء
    }
};

int main() {
    LegacyPrinter oldPrinter;
    PrinterAdapter adapter(oldPrinter);
    
    // يستخدم العميل الواجهة المتوقعة
    adapter.print("Hello World");
}

يحتفظ المحول بمرجع للمُحول إليه وينفذ الواجهة المستهدفة. عندما يتم استدعاء الأساليب (methods) في المحول، فإنه يفوضها إلى أساليب المُحول إليه، مع ترجمة الاستدعاءات حسب الحاجة. يعمل كود العميل مع الواجهة المستهدفة دون معرفة التنفيذ القديم.

جرّب بنفسك

#include <iostream>
#include <string>
#include "MediaPlayer.h"
#include "LegacyAudioLib.h"
#include "AudioAdapter.h"

using namespace std;

int main() {
    // قراءة المدخلات
    string filename;
    int volumeLevel;
    cin >> filename;
    cin >> volumeLevel;
    
    // TODO: إنشاء مثيل (instance) من LegacyAudioLib
    
    // TODO: إنشاء AudioAdapter يغلف المكتبة القديمة (legacy library)
    
    // TODO: تخزين الـ adapter كمؤشر (pointer) من نوع MediaPlayer
    
    // TODO: استخدم واجهة MediaPlayer لـ:
    // 1. ضبط مستوى الصوت إلى المستوى المدخل
    // 2. تشغيل اسم الملف المدخل
    // 3. إيقاف التشغيل
    
    return 0;
}
quiz iconاختبر نفسك

يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.

جميع دروس Object Oriented Programming