• 0
Mr.B

عمل واجهة Interface في C++ (تعدد الأشكال)

سؤال

السلام عليكم

سنتعلّم في هذا الموضوع عن كيفية إنشاء واجهة Interface في C++، تسمّى أيضاً تعدد الأشكال polymorphism، وكيف يمكنك الإستفادة منها وتطبيقها في برامجك.

قبل البدء أود أن أنبه على شيء. لاتخلط بين زيادة التحميل overloading وبين تعدد الأشكال. زيادة الحميل تعني أن يكون لديك أكثر من دالة أو وظيفة لها نفس الإسم وتختلف في نوع المعاملات الممررة لها أو نوع البيانات التي تعيدها. مثلاً:

#include <iostream>#include <string>using namespace std;class Foo {public:    char increase(char arg) {        return ++arg;    }    int increase(int arg) {        return ++arg;    }    double increase(double arg) {        return ++arg;    };};int main(int argc, char **argv){    Foo foo;    cout << foo.increase('A')  << endl; // B    cout << foo.increase(1)    << endl; // 2    cout << foo.increase(3.14) << endl; // 4.14    return 0;}

تعدد الأشكال شيء آخر بعيد عن إسمه، ولاعلاقة له بالأحياء |:.

أيضاً، تعدد الأشكال مبدأ من مبادئ البرمجة الكائنية، ربما أحد أهمها. المعلومات التي هنا يمكن تطبيقها في أي لغة تدعم هذا النمط من البرمجة، الا أن الإسلوب يختلف قليلاً. سواءً كنت مبرمج C++ أو تبرمج بلغة أخرى، فيمكنك المتابعة. سأستخدم أمثلة بسيطة جداً وسأذكر بعض الأمثلة في الأخير للغات مثل Python وJava و PHP.

لندخل في الموضوع، لو كنا سنكتب class للتعامل مع الصور مثل تحويل صيغ الصور من png إلى gif أو تغيير أبعاد الصورة أو تدويرها وغيرها من العمليات. بالطبع سيكون هناك أجزاء ووظائف مسؤولة عن هذه العمليات، لا تهمنا. لكن بالتأكيد سيكون هناك جزء مسؤول عن عملية قراءة وكتابة الصورة.

كيف سنضيف هذا الجزء؟ ربما سنضيف وظيفة مسؤولة عن تحميل الصورة ووظيفة لكتابتها في نفس الـclass مثل:

 

#include <iostream>#include <string>using namespace std;class Image {public:    void load(string filename) {        cout << "read (" << filename << ")" << endl;    }        void save(string filename) {        cout << "write (" << filename << ")" << endl;    }};int main(int argc, char **argv){    Image image;    image.load("image.png");    // ...    image.save("image2.png");    return 0;}

حل منطقي. ماذا لو لم أرغب بتحميل الصورة من ملف أو لاأريد كتابتها لملف؟ قد أكون حمّلت الصورة مسبقاً ولا أريد إعادة كتابتها في ملف لتقوم Image بتحميلها أو قد لاأريد كتابة الصورة لملف بل للذاكرة لأنني لم أنتهي منها بعد، مارأيك؟

لامشكلة، لنضيف وظيفة أخرى للقراءة والكتابة من الذاكرة، تأخذ العنوان والحجم:

 

 

#include <iostream>#include <string>using namespace std;class Image {public:    void load(string filename) {        cout << "read (" << filename << ")" << endl;    }        void load(void *buffer, size_t size) {        cout << "read (memory)" << endl;    }        void save(string filename) {        cout << "write (" << filename << ")" << endl;    }    void save(void *buffer, size_t size) {        cout << "write (memory)" << endl;    }};int main(int argc, char **argv){    Image image;    unsigned char buffer[1024];        image.load("image.png");    image.load(&buffer, sizeof(buffer));    // ...    image.save("image2.png");    image.save(&buffer, sizeof(buffer));    return 0;}

الأمور أفضل. بعد أسابيع أتاك شخص طالباً طريقة للكتابة لـsocket كي يمكّن المكتبة من إرسل الصورة عبر الشبكة مباشرة دون كتابتها لملف أو للذاكرة ... لحظة، أنا لا أعرف للشبكات ، وليست من مهامي، وهذا الطلب بداية إنحراف في المشروع. مالحل؟ لمثل هذه الحالات تُستخدم الواجهات ومبدأ تعدد الأشكال وتتم في C++ بإستخدام الوظائف التخيلية virtual functions/methods.

الفكرة أن تُنشيء class تعتبره الواجهة interface وتورّثه لعدّة class' أبناء مع وضعّ الوظائف virtual، إنظر هنا كيف ننشيء الواجهة (سأشرح بالتفصيل بعد تشغيل البرنامج):

 

 

class IOInterface {public:    virtual void read()  = 0;    virtual void write() = 0;};

ثمّ ننشيء class' ترث هذه الواجهة. مثلاً لننشيء class خاصّ بالكتابة للملفات ونسميه IOFile:

 

 

class IOFile : public IOInterface {    string filename;public:    IOFile(string filename) {        this->filename = filename;    }    void read() {        cout << "IOFile read (" << this->filename << ")" << endl;    }        void write() {        cout << "IOFile write (" << this->filename << ")" << endl;    }};

الآن إنظر للبرنامج ككل ،لاحظ التعديل على الوظيفتين load و save في Image:

 

 

#include <iostream>#include <string>using namespace std;class IOInterface {public:    virtual void read()  = 0;    virtual void write() = 0;};class IOFile : public IOInterface {    string filename;public:    IOFile(string filename) {        this->filename = filename;    }    void read() {        cout << "IOFile read (" << this->filename << ")" << endl;    }        void write() {        cout << "IOFile write (" << this->filename << ")" << endl;    }};class Image {public:    void load(IOInterface &io) {        io.read();    }        void save(IOInterface &io) {        io.write();    }};int main(int argc, char **argv){    Image image;    IOFile ioFile("image.png");    image.load(ioFile);    image.save(ioFile);    return 0;}

قم بتشغيله وتوقف قليلاً لإيستيعاب ماجرى.

لو شّغلت البرنامج فسيطبع:

 

 

IOFile read (image.png)IOFile write (image.png)

لنبدأ من عنّد load و save في Image. ستلاحظ أننا أخبرنا تلك الوظيفتين بأننا سنمرر كائن من نوع IOInterface:

 

 

    void load(IOInterface &io) {        io.read();    }        void save(IOInterface &io) {        io.write();    }
ولكننا مررنا لها كائن نوعه IOFile:

 

 

    IOFile ioFile("image.png");    image.load(ioFile);    image.save(ioFile);

في الـIOInterface كتبنا:

 

 

class IOInterface {public:    virtual void read()  = 0;    virtual void write() = 0;};

ماقمنا به هنا يسمّى "تجريد abstracting". قمنا بتعريف وظيفتين إسمهما read للقراءة و write للكتابة ولككننا لم نضفهما للـclass. يمكنك كتابتها أيضاً هكذا:

 

 

struct IOInterface {    virtual void read()  = 0;    virtual void write() = 0;};

بدل class تضغ struct ويمكنك التعامل معها تماماً كالسابق. في اللغات الأخرى مثل Java تقابل الأولى abstract class IOInterface والثانية interface.

الجزء "0 =" يخبر بأن تلك الوظيفتين "يجب" أن تضاف لأي class يرث من IOInterface. عدم إضافتهما سيعطي خطأ. وبالمناسبة يجب أن تستخدم "0 =" فهذا ليس إستناد صفر بل طريقت تعريف هذا النوع من الدوال، تسّمى pure virtual functions. فلو كتبت NULL فسيعطيك المصرف خطأ، لو كان فعلاً صفر كما قد تظن لن يعطيك خطأ لأن NULL تعني 0. في بعض اللغات يستخدمون كلمة خاصة بدل "0 =" كـabstract في Java و PHP.

هنا:

 

 

    void load(IOInterface &io) {        io.read();    }        void save(IOInterface &io) {        io.write();    }

سيسمح المصرّف فقط للـclass' التي ترّث IOInterface بأنّ تمرر. لنجرّب إضافة الجزء الخاص بالكتابة للذاكرة، سأسميه IOMemory:

 

 

#include <iostream>#include <string>using namespace std;class IOInterface {public:    virtual void read()  = 0;    virtual void write() = 0;};class IOFile : public IOInterface {    string filename;public:    IOFile(string filename) {        this->filename = filename;    }    void read() {        cout << "IOFile read (" << this->filename << ")" << endl;    }        void write() {        cout << "IOFile write (" << this->filename << ")" << endl;    }};class IOMemory : public IOInterface {public:    void read() {        cout << "IOMemory read (memory)" << endl;    }        void write() {        cout << "IOMemory write (memory)" << endl;    }};class Image {public:    void load(IOInterface &io) {        io.read();    }        void save(IOInterface &io) {        io.write();    }};int main(int argc, char **argv){    Image image;    IOFile ioFile("image.png");    IOMemory ioMemory;    image.load(ioFile);    image.save(ioFile);    image.load(ioMemory);    image.save(ioMemory);    return 0;}

سيطبع:

 

 

IOFile read (image.png)IOFile write (image.png)IOMemory read (memory)IOMemory write (memory)

ربما سيسأل شخص مالفائدة من هذا؟ هناك فائدة مهمة.

أهم فائدة "تقسيم وتنظيم وتسريع العمل". لو كان لديك ثلاثة أشخاص، يمكنك تسند عملية العمل على Image للأول و العمل على IOFile للثاني و IOMemory للثالث. يلتقون صباحاً ويتفقون على الواجهة IOInterface:

 

 

class IOInterface {public:    virtual void read()  = 0;    virtual void write() = 0;};

من يعمل على Image يعلم أن الآخرين سيمررون class يرث من IOInterface ويحتوي على وظيفة read و write والآخرين يمكنهم البدء مباشرة دون الحاجة للإنتهاء من Image أو مقاطعة من يعمل عليها أو حتى رؤيته.

أيضاً طريقة القراءة والكتابة تختلف بين طريقة الكتابة والقراءة من ملف أو ذاكرة أو غيرهما وليست جزء أصلي من Image، لذا لن يجبر من يعمل على Image على إنشاءها بل يمكنه التركيز على عمله الفعلي وتوكل مسؤلية العمل على جزء القراءة والكتابة لآخرين.

لو أتى صديقنا صاحب الشبكات، فيمكنه أن يضيف وظيفة الكتابة للـsocket بنفسه:

 

 

class IOSocket : public IOInterface {    string ip;    unsigned int port;public:    IOSocket(string ip, unsigned int port) {        this->ip   = ip;        this->port = port;    }    void read() {        cout << "IOSocket read (" << this->ip << ":" << this->port << ")" << endl;    }        void write() {        cout << "IOSocket write (" << this->ip << ":" << this->port << ")" << endl;    }};

ويبني البرنامج:

 

 

#include <iostream>#include <string>using namespace std;class IOInterface {public:    virtual void read()  = 0;    virtual void write() = 0;};class IOFile : public IOInterface {    string filename;public:    IOFile(string filename) {        this->filename = filename;    }    void read() {        cout << "IOFile read (" << this->filename << ")" << endl;    }        void write() {        cout << "IOFile write (" << this->filename << ")" << endl;    }};class IOMemory : public IOInterface {public:    void read() {        cout << "IOMemory read (memory)" << endl;    }        void write() {        cout << "IOMemory write (memory)" << endl;    }};class IOSocket : public IOInterface {    string ip;    unsigned int port;public:    IOSocket(string ip, unsigned int port) {        this->ip   = ip;        this->port = port;    }    void read() {        cout << "IOSocket read (" << this->ip << ":" << this->port << ")" << endl;    }        void write() {        cout << "IOSocket write (" << this->ip << ":" << this->port << ")" << endl;    }};class Image {public:    void load(IOInterface &io) {        io.read();    }        void save(IOInterface &io) {        io.write();    }};int main(int argc, char **argv){    Image image;    IOFile ioFile("image.png");    IOMemory ioMemory;    IOSocket ioSocket("127.0.0.1", 21);    image.load(ioFile);    image.save(ioFile);    image.load(ioMemory);    image.save(ioMemory);        image.load(ioSocket);    image.save(ioSocket);    return 0;}

سيطبع:

 

 

IOFile read (image.png)IOFile write (image.png)IOMemory read (memory)IOMemory write (memory)IOSocket read (127.0.0.1:21)IOSocket write (127.0.0.1:21)

بالنسبة لإصحاب اللغات الأخرى، أصحاب Java:

 

 

interface IOInterface {    public void read();    public void write();};/*OR:abstract class IOInterface {    abstract public void read();    abstract public void write();}WITH:class IOFile extends IOInterface { ...}class IOMemory extends IOInterface { ...}*/class IOFile implements IOInterface {    private String filename;    public IOFile(String filename) {        this.filename = filename;    }    public void read() {        System.out.println("read (" + this.filename + ")");    }    public void write() {        System.out.println("write (" + this.filename + ")");    }}class IOMemory implements IOInterface {    public void read() {        System.out.println("read memory");    }    public void write() {        System.out.println("write memory");    }}class Image {    public void load(IOInterface io) {        io.read();    }    public void save(IOInterface io) {        io.write();    }}class Example {    public static void main(String[] argv) {        Image image       = new Image();        IOFile ioFile     = new IOFile("image.png");        IOMemory ioMemory = new IOMemory();        image.load(ioFile);        image.save(ioFile);                image.load(ioMemory);        image.save(ioMemory);    }}

أصحاب PHP:

 

 

<?phpinterface IOInterface {    public function read();    public function write();};/**OR:abstract class IOInterface {    abstract public function read();    abstract public function write();}WITH:class IOFile extends IOInterface { ...}class IOMemory extends IOInterface { ...}**/class IOFile implements IOInterface {    private $filename;    public function __construct($filename) {        $this->filename = $filename;    }    public function read() {        echo 'IOFile read(' . $this->filename . ')' . "\n";    }    public function write() {        echo 'IOFile write(' . $this->filename . ')' . "\n";    }};class IOMemory implements IOInterface {    public function read() {        echo 'IOMemory read' . "\n";    }    public function write() {        echo 'IOMemory write' . "\n";    }};class Image {    public function load(IOInterface $io) {        $io->read();    }    public function save(IOInterface $io) {        $io->write();    }};header('Content-type: text/plain');$image    = new Image();$ioFile   = new IOFile('image.png');$ioMemory = new IOMemory();$image->load($ioFile);$image->save($ioFile);$image->load($ioMemory);$image->save($ioMemory);?>

أصحاب Python:

 

 

#!python# -*- coding: utf-8 -*-class IOInterface(object):    def read(self):        raise NotImplementedError    def write(self):        raise NotImplementedErrorclass IOFile(IOInterface):    def __init__(self, filename):        self._filename = filename    def read(self):        print('IOFile read(' + self._filename + ')')    def write(self):        print('IOFile write(' + self._filename + ')')class IOMemory(IOInterface):    def read(self):        print('IOMemory read')    def write(self):        print('IOMemory write')class Image(object):    def load(self, io):        io.read()    def save(self, io):        io.write()if __name__ == '__main__':    image    = Image()    ioFile   = IOFile('image.png')    ioMemory = IOMemory()    image.load(ioFile)    image.save(ioFile)    image.load(ioMemory)    image.save(ioMemory)

بالتوفيق.

3

شارك هذا الرد


رابط المشاركة
شارك الرد من خلال المواقع ادناه

4 إجابة على هذا السؤال .

  • 0

شكراً لك أخي الموضوع رائع جداً ومفيد، اسمح لي بأن أضيف شيئاً:

 

هناك فائدة أخرى رائعة للـpolymorphism وهي إمكانية اختيار النوع المراد استخدامه في وقت التنفيذ... ما المقصود من ذلك؟

المقصود... لنقل أننا نبرمج لعبة شبيهة بنمط Red Alert. فهناك عدة أنواع للجنود، الجندي العادي والطيار وحامل المدفع....

 

يمكننا عند إنشاء جندي عمل ما يلي:

 

Soldier* S;switch(choise){case NORMAL:	S= new S_Normal;case ROCKETEER:	S= new S_Rocketeer;case YURI:	S= new S_Yuri;//So on so forth...}

 

يمكن فيما بعد ببساطة كتابة:

 

S->Create();...S->Move(//Some location);...S->Attack(//Some enemy);

 

وسيتم تنفيذ التعليمات الخاصة بالنوع الذي تم اختياره سابقاً.

 

أكرر شكري لك على هذه الموضوع الرائع :)

تم تعديل بواسطه Abboodd
1

شارك هذا الرد


رابط المشاركة
شارك الرد من خلال المواقع ادناه
  • 0

ماقمت به نمط تصميم يسمى strategy pattern . أشكرك أخي على الإضافة.

1

شارك هذا الرد


رابط المشاركة
شارك الرد من خلال المواقع ادناه
  • 0

شكراً لك لم أكن أعرف اسمها :)

0

شارك هذا الرد


رابط المشاركة
شارك الرد من خلال المواقع ادناه
  • 0

فى الـ c++ يوجد ايضا مفهوم static polymorphism.

3

شارك هذا الرد


رابط المشاركة
شارك الرد من خلال المواقع ادناه

من فضلك سجل دخول لتتمكن من التعليق

ستتمكن من اضافه تعليقات بعد التسجيل



سجل دخولك الان

  • يستعرض القسم حالياً   0 members

    لا يوجد أعضاء مسجلين يشاهدون هذه الصفحة .