ConstructiCat Logo
CodeBust.
Browse section ▾

الزائر والإرسال المزدوج.

دعونا نلقي نظرة على التسلسل الهرمي التالي لفئات الأشكال الهندسية (انتبه: الشيفرة هي شبه كود):

interface Graphic is
    method draw()

class Shape implements Graphic is
    field id
    method draw()
    // ...

class Dot extends Shape is
    field x, y
    method draw()
    // ...

class Circle extends Dot is
    field radius
    method draw()
    // ...

class Rectangle extends Shape is
    field width, height
    method draw()
    // ...

class CompoundGraphic implements Graphic is
    field children: array of Graphic
    method draw()
    // ...

تعمل الشيفرة بشكل جيد والتطبيق في مرحلة الإنتاج. لكن في يوم ما قررت إضافة ميزة تصدير. ستبدو شيفرة التصدير غريبة إذا وُضعت في هذه الفئات. لذا عوضًا عن إضافة ميزة التصدير إلى جميع فئات هذا التسلسل الهرمي، قررت إنشاء فئة جديدة خارج التسلسل الهرمي وتضمين كل منطق التصدير فيها. ستحتوي الفئة على توابع لتصدير الحالة العامة لكل كائن إلى سلاسل XML:

class Exporter is
    method export(s: Shape) is
        print("Exporting shape")
    method export(d: Dot)
        print("Exporting dot")
    method export(c: Circle)
        print("Exporting circle")
    method export(r: Rectangle)
        print("Exporting rectangle")
    method export(cs: CompoundGraphic)
        print("Exporting compound")

تبدو الشيفرة جيدة، لكن دعونا نجرّبها:

class App() is
    method export(shape: Shape) is
        Exporter exporter = new Exporter()
        exporter.export(shape);

app.export(new Circle());
// للأسف، سيُخرج هذا "Exporting shape".

انتظر! لماذا؟!

التفكير كمترجم

ملاحظة: المعلومات التالية صحيحة لمعظم لغات البرمجة كائنية التوجه الحديثة (Java، وC#، وPHP، وغيرها).

الربط المتأخر/الديناميكي

تخيّل أنك مترجم. عليك أن تقرر كيف تترجم الشيفرة التالية:

method drawShape(shape: Shape) is
    shape.draw();

دعنا نرى... التابع draw معرَّف في الفئة Shape. انتظر لحظة، لكن هناك أيضًا أربع فئات فرعية تُعيد تعريف هذا التابع. هل يمكننا تحديد أي تنفيذ سيُستدعى هنا بأمان؟ لا يبدو ذلك. الطريقة الوحيدة للتأكد هي تشغيل البرنامج والتحقق من فئة الكائن الممرّر إلى التابع. الشيء الوحيد الذي نعرفه على وجه اليقين هو أن الكائن سيمتلك تنفيذًا للتابع draw.

وبالتالي، ستتحقق الشيفرة الآلية الناتجة من فئة الكائن الممرّر إلى المعامل shape وتختار تنفيذ draw من الفئة المناسبة.

يُسمّى هذا الفحص الديناميكي للنوع الربطَ المتأخر (أو الديناميكي):

  • متأخر، لأننا نربط الكائن بتنفيذه بعد التصريف، في وقت التشغيل.
  • ديناميكي، لأن كل كائن جديد قد يحتاج إلى الارتباط بتنفيذ مختلف.

الربط المبكر/الساكن

الآن، دعونا "نصرّف" الشيفرة التالية:

method exportShape(shape: Shape) is
    Exporter exporter = new Exporter()
    exporter.export(shape);

كل شيء واضح في السطر الثاني: الفئة Exporter لا تملك بانيًا مخصصًا، لذا نقوم فقط بإنشاء كائن. ماذا عن استدعاء export؟ تمتلك Exporter خمسة توابع بالاسم ذاته تختلف في أنواع المعاملات. أيها يُستدعى؟ يبدو أننا سنحتاج إلى ربط ديناميكي هنا أيضًا.

لكن ثمة مشكلة أخرى. ماذا لو كانت هناك فئة شكل لا تملك تابع export مناسب في الفئة Exporter؟ كأن يكون الكائن من نوع Ellipse مثلًا. لا يستطيع المترجم ضمان وجود التابع المحمّل المناسب خلافًا للتوابع المُعاد تعريفها. ينشأ موقف غامض لا يمكن للمترجم السماح به.

لذا، يلجأ مطورو المترجمات إلى المسار الآمن ويستخدمون الربط المبكر (أو الساكن) للتوابع المحمّلة:

  • مبكر لأنه يحدث في وقت التصريف، قبل تشغيل البرنامج.
  • ساكن لأنه لا يمكن تغييره في وقت التشغيل.

دعونا نعود إلى مثالنا. نحن متأكدون من أن الوسيطة الواردة ستكون من التسلسل الهرمي Shape: إما الفئة Shape أو إحدى فئاتها الفرعية. كما نعرف أن الفئة Exporter تمتلك تنفيذًا أساسيًا للتصدير يدعم الفئة Shape: export(s: Shape).

هذا هو التنفيذ الوحيد الذي يمكن ربطه بأمان بالشيفرة المعطاة دون إحداث غموض. لهذا السبب، حتى لو مررنا كائن Rectangle إلى exportShape، سيظل المُصدِّر يستدعي التابع export(s: Shape).

الإرسال المزدوج

الإرسال المزدوج هو حيلة تتيح استخدام الربط الديناميكي جنبًا إلى جنب مع التوابع المحمّلة. إليك الطريقة:

class Visitor is
    method visit(s: Shape) is
        print("Visited shape")
    method visit(d: Dot)
        print("Visited dot")

interface Graphic is
    method accept(v: Visitor)

class Shape implements Graphic is
    method accept(v: Visitor)
        // يعلم المترجم بيقين أن 'this' هو 'Shape'.
        // مما يعني أن 'visit(s: Shape)' يمكن استدعاؤه بأمان.
        v.visit(this)

class Dot extends Shape is
    method accept(v: Visitor)
        // يعلم المترجم أن 'this' هو 'Dot'.
        // مما يعني أن 'visit(s: Dot)' يمكن استدعاؤه بأمان.
        v.visit(this)


Visitor v = new Visitor();
Graphic g = new Dot();

// التابع 'accept' مُعاد تعريفه، وليس محمّلًا. يربطه المترجم
// ديناميكيًا. لذا سيُنفَّذ 'accept' على الفئة التي
// تتوافق مع الكائن الذي يستدعي التابع (في مثالنا، الفئة 'Dot').
g.accept(v);

// الناتج: "Visited dot"

خاتمة

على الرغم من أن نمط الزائر مبني على مبدأ الإرسال المزدوج، إلا أن ذلك ليس غرضه الأساسي. يُتيح الزائر إضافة عمليات "خارجية" إلى تسلسل هرمي كامل من الفئات دون تغيير الشيفرة الموجودة لهذه الفئات.