في عالم البرمجة بشكل عام عندما يتم ذكر المصطلح Adapter فهذا يعني أن لدينا implementation ما ونريد التعديل قليلا على هذا الimplementation سواء بمنع مجموعه من الدوال أو باضافه دوال جديدة ، وهنا سنقدم للمستخدم كلاس جديد Wrapper Class بمجموعه من الدوال والمستخدم لن يعلم شيئا عن الUnderlying implementation ، وهنا تكمن الفائده في أمكانية تغيير هذه الlower data structure بدون تغيير اي سطر في كود المستخدم .
أشهر استخدامات نمط الAdapter في سي++ هو في الSTL ، حيث نجد أن هناك الAdapter Container مثل الstack و الqueue والتي تستخدم كل منها (list or deque ) في الوضع الإفتراضي ، ولكن طبيعة مكتبة الSTL المرنة تمكنك من تغيير الunderlying Container بكل سهوله كما تريد ، وذلك بفضل الtemplate ، يمكنك مراجعه مقال الأخ خالد الشايع ( نظرة حول مكتبات الـ Data Structre في ++C ) وهو مقال رائع بلا أدنى شك.
وكما ان هناك Adapter للكلاسات في اللغه ، فهناك Adapter للدوال ، ولكنها تختلف قليلا في الفكرة حيث تكمن مهمه الFunctor Adapter في ربط عدة دوال مع بعضهم ليتم تحقيق هدف معين . وقبل أن نتناول هذه الAdapter سوف نتناول بعضا من الدوال في الSTL ثم نشرح المشكلة التي تحلها هذه الAdapter حتى ندرك تماما متى نستخدمها وهل يمكن أن نتجاهلها أم لأ.
أحد أهم الخوارزميات في الSTL هي خوارزميات البحث find و find_if واللتان تبحثان عن المفتاح في الContainer وترجع مؤشر iterator لأول عنصر كان مشابه للمفتاح ،، المثال التالي يوضح ذلك
// find and find_if exmaple #include <iostream> #include <vector> #include <algorithm> #include <numeric> using namespace std; bool isPass (int degree) { return (degree >= 50 ); } int main(int arcg , char* argv[]) { vector<int> v1; v1.push_back(10); v1.push_back(56); v1.push_back(23); v1.push_back(12); // print vector cout << "content: "; copy(v1.begin(),v1.end(),ostream_iterator<int>(cout," ") ); cout << endl; // search for key using find int key = 23; vector<int>::iterator itr = find(v1.begin(), v1.end(), key); if ( itr != v1.end() ) cout << "find key at position: " << (itr - v1.begin()) << endl; else cout << "cannot find key ." << endl; // search for first passing degree ( >= 50 ) itr = find_if(v1.begin(),v1.end(), isPass); if ( itr != v1.end() ) cout << "find passing degree at position: " << (itr - v1.begin()) << endl; else cout << "cannot find passing degree ." << endl; return (0); }
بعد النظر للمثال فقد يبدوا هذا السطر محيرا قليلا :
cout << "content: "; copy(v1.begin(),v1.end(),ostream_iterator<int>(cout," ") ); cout << endl;
الجمله التي في الوسط هي عباره عن استدعاء للدالة copy في STL تقوم هذه الدالة بأخذ عنصر عنصر من الContainer وهو في هذه الحالة v1 الى الDestination وهو عباره عن iterator يأخذ العنصر ويقوم بطباعته ثم طباعه مسافه بعد ذلك ،، سوف نستخدم هذا السطر لطباعه اي Sequential Container ، ويمكن اذا لم يعجبك أن تطبع بالطريقة التي تريد ، كما يلي :
for ( vector<int>::iterator itr=v1.begin(); itr != v1.end(); ++itr) cout << *itr << " ";
أو :
// function defnition void myPrint (int x) { cout << x << " "; } // in main function for_each ( v1.begin() , v1.end() , &myPrint);
لدالة for_each أيضا هي من دوال الSTL تأخذ عنصر عنصر الى الCallback function والتي تقوم بطباعه العنصر ،،
الى هذه اللحظه سوف نجد أن هناك الكثير من الدوال في الSTL تتعامل مع أي دوال Callback Function ويمكنك أن تصنع دوال هكذه عن طريق وضع مؤشر للدالة كما في for_each أو أن تعيد تعريف المعامل ( ) في الكائن الذي تريده أن يعمل كCallback .
نرجع للمثال السابق (الأول) ، بعد سطر الطباعه سنجد أننا بحثنا عن المفتاح أولا باستخدام find ، وهي ترجع مؤشر يساوي الخانه الأخيره من الcontainer (تذكر أن الخانه الأخيره هي one-past أي أنها لا تؤشر لشيئ ) .. بعد ذلك استخدمنا الدالة الثانية find_if وسنجد أن هذه الدالة (وكل دوال STL التي تنتهي بif) سوف تتطلب Callback funcation يرجع قيمه منطقية boolean فاذا كان أحد العناصر مساويا لذلك ، أرجعت الدالة true وهنا سوف تتوقف الدالة find_if وترجع العنصر الصحيح . (الدالتين find و find_if ترجع قيمه واحده فقط ، اذا أردت يمكن أن تكتب دالة مشابه ترجع جميع العناصر وجعلها STL-Compitable بحيث تستخدمها مع أي container ) .
الى هنا سنجد أن الCallback يعمل جيدا ، ولكن ،، هناك Functor جاهز في سي++ للقيام بالعديد من المهام ، فلماذا لا نستخدمها بدلا من اعاده العجلة من جديد ؟
الFunctor الموجود في سي++ هو للعمليات الحسابية جمع وطرح وقسمه … ، ولعمليات المقارنه (أكبر أصغر يساوي ..) وأيضا للعمليات المنطقية and or not .
المثال التالي يبين استخدام هذه الكائنات:
// Pre-defined Functor //Arithmetics Functor: //--------------------- //Binary operator : plus , minus , multiplies , divides , modules //Unary operator : negate //comparsion Functor: //Binary operator : less, equal_to , not_equal_to, greater, less_equal, greater_equal //Logical Functor : //logical_not, logical_and , logical_or #include <iostream> #include <functional> using namespace std; #define print(x) cout<<#x << " : " << x << endl; int main (int argc , char* argv[]) { plus<int> p; int sum = p(2,45); print(sum); multiplies<int> m; int x = m(sum,3); print(x); less<int> le; bool rslt = le(900,3); print(rslt); greater<int> gr; rslt = gr(900,3); print(rslt); return (0); }
ال#define في المثال اعلاه هي تقوم بطباعه اسم المتغير وقيمته وهي مفيده في البرامج الصغيره ،، تسمى Stringizing وليس لها دخل طبعا بSTL فهي موجودة من أيام لغه السي .. المثال تقريبا واضح ، وبالرغم من ذلك فإن استخدام هذه الكائنات بهذا الشكل أمر لا فائده مرجوه منه ، ولكن في بعض الأحيان قد تضطر الى استخدامها فمثلا في الpriority_queue معامل المقارنة الإفتراضي هو less فاذا أردت تغيير ذلك فيمكنك أن تستخدم مثلا greater ، وهنا سوف تكون طريقة الإنشاء بهذا الشكل :
priority_queue<int, vector<int>, greater<int> > myQueue
أعلاه اضطررنا أن نحدد الContainer المستخدم بسبب أنه في حال أردنا تغيير الdefault argument فيجب أن نغير كل ما قبله اذا كان هناك .
بعد كل هذه المقدمة الطويلة نعود لمثالنا find_if ، وهنا هذه المرة نريد استخدام الCallback الموجوده مثل greater أو less ، فاذا كتبت العباره :
vector<int>:: iterator itr = find_if(v1.begin(),v1.end(), greater<int>());
فسوف تحصل على رسالة خطأ مشفره خلاصتها أن الكائن greater يحتاج لمعاملين ، وأن الدالة find_if ترسل عنصر واحد في كل مرة ،، اذا ما الحل ؟ هل هناك طريقة تمكننا من جعل الكائن graeter يستقبل عنصرين بدون التأثير على find_if ؟؟
نعم وهذه هي وظيفة الAdapter أو الFunctional Compsition ، ومرحبا بك في عالم التعقيد ..
لنبدأ بأول Adapter وهو bind2nd ، وسنستخدمه لحل المشكلة السابقة ، وبالتالي برنامجنا سوف يصبح :
// find_if exmaple #include <iostream> #include <vector> #include <algorithm> #include <numeric> using namespace std; int main(int arcg , char* argv[]) { vector<int> v1; v1.push_back(10); v1.push_back(56); v1.push_back(23); v1.push_back(12); // print vector cout << "content: "; copy(v1.begin(),v1.end(),ostream_iterator<int>(cout," ") ); cout << endl; // search for first passing degree ( >= 50 ) vector<int>:: iterator itr = find_if(v1.begin(),v1.end(), bind2nd(greater_equal<int>(),50)); if ( itr != v1.end() ) cout << "find passing degree at position: " << (itr - v1.begin()) << endl; else cout << "cannot find passing degree ." << endl; return (0); }
وظيفة الbind2nd هي اضافة الرقم 50 للمعامل الثاني لgreater_equal وهكذا سيعمل المثال ويتم ارجاع القيمة الصحيحه ، هناك bind1nd وهي اضافة الرقم الفلاني للمعامل الأول (لاحظ 1 في اسم الدالة للإشاره الى المعامل الأول ) .
الAdapter الأخر وهو not1 وهو لنفي النتيجه بالكامل ، فاذا أردنا في المثال السابق أن نرجع أول عنصر هو ليس بأكبر من أو يساوي 50 ، فيكون ذلك كما يلي (لن نضيف المثال بالكامل ، فقط سطر الfind_if ) :
vector<int>:: iterator itr = find_if(v1.begin(),v1.end(), not1(bind2nd(greater_equal<int>(),50)) );
هكذا سيعود بأول نتيجه أقل من 50 ، بالطبع يمكن استخدام الكائن less بدلا من ذلك :
vector<int>:: iterator itr = find_if(v1.begin(),v1.end(), bind2nd(less<int>(),50));
مثلا ، اذا أردت جمع 100 لكل عنصر :
transform(v1.begin(), v1.end(), v1.begin(),bind2nd(plus<int>(), 100));
مثلا أردت حصر قيمه أي عنصر بين 0 الى 100 ، يمكنك أن تستخدم :
replace_if(myVector.begin(), myVector.end(), bind2nd(less<int>(), 0), 0); replace_if(myVector.begin(), myVector.end(), bind2nd(greater<int>(), 100),100);
الى هنا كان الAdapter يتعامل مع الكائنات الجاهزة Functor مثل less أو greater ، لكن ماذا اذا أردنا أن نستخدم هذه الAdapter مع الدوال العاديه أو حتى الكائنات التي تعرف () ؟ هذا ما سنتناوله الأن ..
استخدام الAdapter مع الدوال العادية :
—————————————–
أولا هذه الAdapter تتطلب بعض من الtypedef المعرفة في الكائن الذي تعمل عليه ، لذلك اذا أردنا أن نستخدم هذه الAdapter مع الدوال العاديه يجب أن نستخدم ptr_func والذي مهمته تحويل مؤشر الدالة الى طريقة يمكن أن تستخدمه الدوال bind2nd و not1 ، لنشاهد المثال :
مثلا لدينا string ونريد معرفه هل هذا النص هو عدد أم لأ :
// isNumber.cpp #include <iostream> #include <cctype> // for isdigit #include <algorithm> #include <functional> using namespace std; bool isNumber (const string& s) { string::const_iterator itr= find_if(s.begin(),s.end(),::isdigit); if ( itr == s.end() ) return false; else return true; } int main (int argc, char* argv[]){ string s = "42566"; string s1= "ahemd"; cout << isNumber(s) << endl; cout << isNumber(s1) << endl; return (0); }
المثال جيد ، ولكن لنفرض أننا نريد أول حرف ليس رقم ، بالتالي سنحتاج نفي العباره بnot1 ، وكما ذكرنا أن الدوال العادية لا يمكن استخدامها مع هذه الAdapter الا عند اضافتها ل ptr_fun ، وهنا سوف يكون التغيير في الدالة isNumber كما يلي :
bool isNumber (const string& s) { string::const_iterator itr= find_if(s.begin(),s.end(), not1( ptr_fun(::isdigit) ) ); if ( itr == s.end() ) return true; else return false; }
استخدام الAdapter مع الكائنات التي تعرف () :
—————————————————
أيضا اذا أردنا أن نستخدم الكائنات التي تعيد تعريف () مع هذه الAdapter ، فإنه يجب تعريف بضعه typedef أو الطريق الأسهل وهو الوراثه من أحدى الكائنين unary_function أو binary_function على حسب الدالة هل تأخذ وسيط أو اثنين ، كما يبين ذلك المثال :
// isNumber.cpp #include <iostream> #include <cctype> // for isdigit #include <algorithm> #include <functional> using namespace std; class MyIsDigit : public unary_function<char,bool> { public: bool operator() (char c) const { return (::isdigit(c) ); } }; bool isNumber (const string& s) { string::const_iterator itr= find_if(s.begin(),s.end(), not1( MyIsDigit() ) ); if ( itr == s.end() ) return true; else return false; } int main (int argc, char* argv[]){ string s = "42566"; string s1= "ahemd"; cout << isNumber(s) << endl; cout << isNumber(s1) << endl; return (0); }
قبل أن تنتهي لنتعرف على Adapter أخر وظيفته جعل دالة داخل الكلاس تعمل كCallback ، وهنا في هذه الحالة سوف نستخدم mem_fun_ref اذا كان الcontainer من كائنات عادية ، أما اذا كان الContainer من مؤشرات فسوف نستخدم mem_fun .
مثلا لدينا vector of string ونريد حذف أي string فارغ من هذا الvector ، نشاهد المثال :
// remove empty string #include <iostream> #include <vector> // vector #include <algorithm> // for find #include <numeric> // ostream_iterator using namespace std; void removeStrings (vector<string>& strings); int main (int argc , char* argv[]){ vector<string> strings; strings.push_back("ahmed");strings.push_back("");strings.push_back("");strings.push_back("ali"); cout << "Before Remove : " << endl; copy(strings.begin(),strings.end(),ostream_iterator<string>(cout,"\n")); removeStrings(strings); cout << endl << "After Remove : " << endl; copy(strings.begin(),strings.end(),ostream_iterator<string>(cout,"\n")); return (0); } void removeStrings (vector<string>& strings) { vector<string>::iterator itr=remove_if(strings.begin(),strings.end(),mem_fun_ref( &string::empty ) ); strings.erase(itr,strings.end()); // erase removing elements }
لاحظ تم استخدام mem_fun_ref وهي بمثابة جعل الدالة empty تعمل كcallback للدالة remove_if .
أخيرا وليس أخر ، فإن boost قدمت المكتبة bind للقيام بنفس مهمه الadapter في STL ، ولكنها أسهل وأيسر استخداما ، فكما لاحظنا أنه لجعل الدوال داخل الكلاس تعمل ك callback قد نستخدم mem_fun_ref أو mem_fun (وذلك حسب محتوي الcontainer هل يتعامل مع الvalue أو الpointer) ولكن في boost.bind فهي تتعامل مع الحالتين من خلال داله واحده boost::bind .
أرجوا أن يفيد الموضوع من قرر استخدام الadapter في برامجه .