نمط المحول
جزء من قسم 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) لا تتوافق مع ما تحتاجه، أو عندما تحتاج إلى إنشاء فئة قابلة لإعادة الاستخدام تتعاون مع فئات ذات واجهات غير متوافقة.
التحدي
سهللنقم ببناء نظام محول مشغل الوسائط (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 startedstopPlayback()— تطبعLegacy: Playback stoppedadjustVolume(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: استعرض المحول أثناء العمل.اقرأ مدخلين:
- اسم ملف لتشغيله (string)
- مستوى الصوت (integer، 0-100)
أنشئ مثيلاً (instance) من
LegacyAudioLibومحولAudioAdapterيغلفه. ثم استخدم المحول من خلال واجهةMediaPlayer(قم بتخزينه كمؤشرMediaPlayer) للقيام بما يلي:- ضبط مستوى الصوت إلى المستوى المدخل
- تشغيل اسم الملف المدخل
- إيقاف التشغيل
على سبيل المثال، مع المدخلات 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;
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس Object Oriented Programming
1أساسيات الـ OOP
الملفات الخارجيةبناء وتجميع (Compilation) لغة C++ملفات الـ Header وملفات الـ Sourceالـ Namespaces والـ Scopeمقدمة في الـ OOP في C++الـ Classes مقابل الـ Objectsالمؤشر 'this'الـ Methods (الدوال الأعضاء)الـ Attributes (بيانات الأعضاء)أساسيات الـ Ctors والـ Dtorsمراجعة - آلة حاسبة بسيطة4خصائص الـ Class
أعضاء الـ Instance مقابل الـ Staticدوال الـ Getters والـ Settersدوال الأعضاء الثابتة (Const)الكلمة المفتاحية Mutableالدوال والمتغيرات الساكنة (Static)الدوال والفئات الصديقة (Friend)مراجعة - مدير الحساب البنكي7الوراثة
الوراثة الأساسيةمستويات الوصول في الوراثةترتيب استدعاء الـ Ctor والـ Dtorإعادة تعريف الدوال (Method Overriding)الدوال الافتراضية والـ VTableالوراثة المتعددةالوراثة الافتراضيةمراجعة - هيكلية الموظفين10نظرة عامة على STL
نظرة عامة وفلسفة STLحاويات STLالـ Iteratorsخوارزميات STLالـ Functors وتعبيرات Lambdaمراجعة - تكرار الكلمات13أنماط التصميم - الجزء الأول
مقدمة في أنماط التصميمنمط Singletonنمط Factory و Abstract Factoryنمط Builderنمط Observerنمط Strategy2إدارة الذاكرة
ذاكرة Stack مقابل Heapالمؤشرات والمراجعالذاكرة الديناميكية (new/delete)المؤشرات الذكية في C++RAII في C++مراجعة - مدير المصفوفات الديناميكية5التغليف (Encapsulation)
محددات الوصول في C++محددات الوصول بشكل متعمقإخفاء المعلوماتStruct مقابل Classالأصناف المتداخلة والداخليةمراجعة - نظام سجلات الطلاب8تعدد الأشكال (Polymorphism)
تعدد الأشكال: وقت التجميع مقابل وقت التشغيلالتحميل الزائد للدوال (Function Overloading)مراجعة الدوال الافتراضية (Virtual Functions)الدوال الافتراضية البحتة (Pure Virtual Functions)الأصناف المجردة (Abstract Classes)تصميم الواجهات (Interface) في C++التحويل الديناميكي (Dynamic Casting) و RTTIملخص - حاسبة الأشكال11مفاهيم OOP المتقدمة
التركيب مقابل الوراثةالـ Mixins عبر CRTPنمط Pimplمحو النوع (Type Erasure)أصناف Enum والأنواع القويةمعالجة الاستثناءات في OOPتسلسلات الاستثناءات المخصصة14أنماط التصميم - الجزء الثاني
نمط الأمرنمط المحولنمط المزيننمط قالب الطريقةنمط الحالةنمط التركيبRAII كنمط3المنشئات والموادم
المنشئ الافتراضيالمنشئ ذو المعاملاتمنشئ النسخمنشئ النقلقوائم تهيئة المنشئالمنشئات المفوضةتعمق في الموادمقاعدة الثلاثة / الخمسة / الصفرمراجعة - فئة String6تحميل العوامل (Operator Overloading)
مقدمة في تحميل العواملتحميل العوامل الحسابيةتحميل عوامل المقارنةعوامل Streamتحميل عامل التعيينتحميل العوامل [] و ()عوامل تحويل النوعمراجعة - Matrix Class9القوالب
قوالب الدوالقوالب الأصنافتخصيص القوالبالقوالب المتغيرةأساسيات SFINAE و Type Traitsمراجعة - الحاويات العامة