يبدأ كتاب الدكتور يان هونغ "JAVA والأنماط" بوصف نمط الزائر:
نمط الزائر هو نمط سلوك الكائنات. الغرض من نمط الزائر هو تغليف بعض العمليات التي يتم تطبيقها على عناصر معينة في بنية البيانات. بمجرد الحاجة إلى تعديل هذه العمليات، يمكن أن تظل بنية البيانات التي تقبل هذه العملية دون تغيير.
مفهوم الإرسالية
يسمى النوع عند الإعلان عن متغير بالنوع الثابت للمتغير (النوع الثابت)، ويطلق بعض الأشخاص على النوع الثابت النوع الظاهر (النوع الظاهري)؛ ويسمى النوع الحقيقي للكائن المشار إليه بواسطة المتغير أيضًا النوع الفعلي للمتغير (Actual Type). على سبيل المثال:
انسخ رمز الكود كما يلي:
قائمة القائمة = فارغة؛
list = new ArrayList();
يتم الإعلان عن قائمة المتغيرات، ونوعها الثابت (يسمى أيضًا النوع الواضح) هو القائمة، ونوعها الفعلي هو ArrayList.
يتم تقسيم اختيار الأساليب بناءً على نوع الكائن إلى نوعين، الإرسال الثابت والإرسال الديناميكي.
يحدث الإرسال الثابت في وقت الترجمة، ويحدث الإرسال بناءً على معلومات النوع الثابت. الإرسال الثابت ليس غريبًا علينا. طريقة التحميل الزائد هي إرسال ثابت.
يحدث الإرسال الديناميكي أثناء وقت التشغيل، ويحل الإرسال الديناميكي محل الطريقة ديناميكيًا.
إرسال ثابت
تدعم Java الإرسال الثابت من خلال التحميل الزائد للطريقة. باستخدام قصة موزي وهو يركب حصانًا كمثال، يمكن لموزي أن يركب حصانًا أبيض أو حصانًا أسود. الرسم التخطيطي لفصل Mozi والحصان الأبيض والحصان الأسود والحصان هو كما يلي:
في هذا النظام يتم تمثيل Mozi بفئة Mozi ويكون الكود كما يلي:
الطبقة العامة موزي {
ركوب الفراغ العام (الحصان ح) {
System.out.println("ركوب الخيل");
}
ركوب الفراغ العام (WhiteHorse wh) {
System.out.println("ركوب حصان أبيض");
}
ركوب الفراغ العام (BlackHorse bh) {
System.out.println("ركوب الحصان الاسود");
}
public static void main(String[] args) {
حصان wh = new WhiteHorse();
Horse bh = new BlackHorse();
Mozi mozi = new Mozi();
mozi.ride(wh);
mozi.ride(bh);
}
}
من الواضح أن طريقة الركوب () لفئة Mozi مثقلة بثلاث طرق. تقبل هذه الطرق الثلاثة معلمات Horse وWhiteHorse وBlackHorse والأنواع الأخرى على التوالي.
إذن ما هي النتائج التي سيطبعها البرنامج عند تشغيله؟ والنتيجة هي أن البرنامج يطبع نفس السطرين من "ظهور الخيل". بمعنى آخر، اكتشف موزي أن كل ما كان يركبه هو الخيول.
لماذا؟ يمر الاستدعاءان للتابع التابع () بمعلمات مختلفة، وهما wh وbh. على الرغم من أن لديهم أنواعًا حقيقية مختلفة، إلا أن أنواعهم الثابتة كلها متشابهة، وهي أنواع الخيول.
يعتمد إرسال الأساليب المحملة بشكل زائد على الأنواع الثابتة، وتكتمل عملية الإرسال هذه في وقت الترجمة.
إرسال ديناميكي
تدعم Java الإرسال الديناميكي من خلال تجاوز الطريقة. وباستخدام قصة الحصان الذي يأكل العشب كمثال، يكون الكود كما يلي:
انسخ رمز الكود كما يلي:
حصان الطبقة العامة {
أكل الفراغ العام () {
System.out.println("الحصان يأكل العشب");
}
}
انسخ رمز الكود كما يلي:
الطبقة العامة BlackHorse تمتد الحصان {
@تجاوز
أكل الفراغ العام () {
System.out.println("الحصان الاسود يأكل العشب");
}
}
انسخ رمز الكود كما يلي:
عميل الطبقة العامة {
public static void main(String[] args) {
الحصان h = new BlackHorse();
حرارة()؛
}
}
النوع الثابت للمتغير h هو Horse، والنوع الحقيقي هو BlackHorse. إذا كانت طريقة Eat () في السطر الأخير أعلاه تستدعي طريقة Eat () لفئة BlackHorse، فإن ما هو مطبوع أعلاه هو "Black Horse Eating Grass"، على العكس من ذلك، إذا كانت طريقة Eat () أعلاه تستدعي Eat (؛ ) طريقة فئة الحصان، ثم ما يطبع هو "الحصان يأكل العشب".
لذلك، جوهر المشكلة هو أن مترجم Java لا يعرف دائمًا الكود الذي سيتم تنفيذه أثناء الترجمة، لأن المترجم يعرف فقط النوع الثابت للكائن، لكنه لا يعرف النوع الحقيقي للكائن والطريقة؛ يعتمد الاستدعاء على الأنواع الحقيقية للكائن، وليس الأنواع الثابتة. بهذه الطريقة، تستدعي طريقة Eat() في السطر الأخير أعلاه طريقة Eat() لفئة BlackHorse، وتطبع "الحصان الأسود يأكل العشب".
نوع الإرسال
يسمى الكائن الذي تنتمي إليه الطريقة بمستقبل الطريقة. يُطلق على مستقبل الطريقة ومعلمات الطريقة بشكل جماعي حجم الطريقة. على سبيل المثال، رمز النسخ لفئة الاختبار في المثال أدناه هو كما يلي:
اختبار الطبقة العامة {
طباعة باطلة عامة (سلسلة str) {
System.out.println(str);
}
}
في الفئة المذكورة أعلاه، تنتمي طريقة الطباعة () إلى كائن الاختبار، لذا فإن جهاز الاستقبال الخاص بها هو أيضًا كائن الاختبار. تحتوي طريقة الطباعة () على معلمة تسمى str، ونوعها هو سلسلة.
اعتمادًا على عدد أنواع إرسال الكميات التي يمكن الاعتماد عليها، يمكن تقسيم اللغات الموجهة للكائنات إلى لغات إرسال واحدة (Uni-Dispatch) ولغات متعددة الإرسال (Multi-Dispatch). تحدد لغات الإرسال الفردي الأساليب بناءً على نوع مثيل واحد، بينما تحدد لغات الإرسال المتعدد الأساليب بناءً على نوع أكثر من مثيل واحد.
تعد كل من C++ وJava لغات إرسال واحدة، ومن الأمثلة على لغات الإرسال المتعددة CLOS وCecil. وفقًا لهذا التمييز، تعد Java لغة ديناميكية أحادية الإرسال، لأن الإرسال الديناميكي لهذه اللغة يأخذ في الاعتبار فقط نوع مستقبل الطريقة، وهي لغة ثابتة متعددة الإرسال، لأن هذه اللغة ترسل الأساليب المحملة بشكل زائد يتم أخذ نوع مستقبل الطريقة في الاعتبار وأنواع جميع معلمات الطريقة.
في اللغة التي تدعم الإرسال الفردي الديناميكي، هناك شرطان يحددان العملية التي سيستدعيها الطلب: الأول هو اسم الطلب، والنوع الحقيقي للمستلم. يحد الإرسال الفردي من عملية اختيار الطريقة بحيث يمكن أخذ مثيل واحد فقط في الاعتبار، وهو عادة متلقي الطريقة. في لغة Java، إذا تم تنفيذ عملية على كائن من نوع غير معروف، فسيتم إجراء اختبار النوع الحقيقي للكائن مرة واحدة فقط. هذه هي سمة الإرسال الفردي الديناميكي.
إرسال مزدوج
تقرر إحدى الطرق تنفيذ تعليمات برمجية مختلفة بناءً على نوع متغيرين. وهذا هو "الإرسال المزدوج". لا تدعم لغة Java الإرسال الديناميكي المتعدد، مما يعني أن Java لا تدعم الإرسال الديناميكي المزدوج. ولكن باستخدام أنماط التصميم، يمكن أيضًا تنفيذ الإرسال الديناميكي المزدوج بلغة Java.
في Java، يمكن تنفيذ إرساليتين من خلال استدعاءات الأسلوب. الرسم البياني للفئة هو كما يلي:
يوجد في الصورة جسمان، الجسم الموجود على اليسار يسمى الغرب، والجسم الموجود على اليمين يسمى الشرق. الآن يستدعي الكائن الغربي أولاً طريقة goEast() الخاصة بالكائن الشرقي، ويمرر نفسه. عندما يتم استدعاء الكائن الشرقي، فإنه يعرف على الفور من هو المتصل بناءً على المعلمات التي تم تمريرها، لذلك يتم استدعاء طريقة goWest () للكائن "المتصل" بدوره. من خلال مكالمتين، يتم تسليم التحكم في البرنامج إلى كائنين بدورهما، ويكون مخطط التسلسل كما يلي:
بهذه الطريقة، يتم تمرير عنصر تحكم البرنامج بين الكائنين. أولاً، يتم تمريره من الكائن الغربي إلى الكائن الشرقي، ثم يتم تمريره مرة أخرى إلى الكائن الغربي.
لكن مجرد إعادة الكرة لا يحل مشكلة التوزيع المزدوج. المفتاح هو كيفية استخدام هاتين المكالمتين ووظيفة الإرسال الديناميكي الفردي للغة Java لتشغيل إرسالين فرديين أثناء عملية المرور هذه.
يحدث الإرسال الفردي الديناميكي في لغة Java عندما تتجاوز فئة فرعية أسلوبًا للفئة الأصلية. بمعنى آخر، يجب وضع كل من الغرب والشرق في التسلسل الهرمي الخاص بهما، كما هو موضح أدناه:
كود المصدر
رمز نسخة الطبقة الغربية هو كما يلي:
فئة مجردة عامة الغرب {
الملخص العام void goWest1(SubEast1 east);
الملخص العام void goWest2(SubEast2 east);
}
رمز رمز نسخ فئة SubWest1 هو كما يلي:
الطبقة العامة SubWest1 تمتد غربًا {
@تجاوز
الفراغ العام goWest1 (SubEast1 east) {
System.out.println("SubWest1 + " + east.myName1());
}
@تجاوز
الفراغ العام goWest2 (SubEast2 east) {
System.out.println("SubWest1 + " + east.myName2());
}
}
الطبقة الفرعية الغربية 2
انسخ رمز الكود كما يلي:
الطبقة العامة SubWest2 تمتد غربًا {
@تجاوز
الفراغ العام goWest1 (SubEast1 east) {
System.out.println("SubWest2 + " + east.myName1());
}
@تجاوز
الفراغ العام goWest2 (SubEast2 east) {
System.out.println("SubWest2 + " + east.myName2());
}
}
رمز نسخة الطبقة الشرقية هو كما يلي:
فئة مجردة عامة الشرق {
الملخص العام void goEast(West west);
}
رمز رمز نسخ فئة SubEast1 هو كما يلي:
الطبقة العامة SubEast1 تمتد شرقًا {
@تجاوز
الفراغ العام يتجه شرقًا (الغرب الغربي) {
west.goWest1(this);
}
السلسلة العامة myName1(){
إرجاع "SubEast1" ؛
}
}
رمز رمز نسخ فئة SubEast2 هو كما يلي:
الطبقة العامة SubEast2 تمتد شرقًا {
@تجاوز
الفراغ العام يتجه شرقًا (الغرب الغربي) {
west.goWest2(this);
}
السلسلة العامة myName2 () {
إرجاع "SubEast2" ؛
}
}
رمز نسخة فئة العميل كما يلي:
عميل الطبقة العامة {
public static void main(String[] args) {
// الجمع 1
الشرق الشرقي = New SubEast1();
الغرب الغربي = New SubWest1();
east.goEast(west);
// الجمع 2
الشرق = جديد SubEast1();
الغرب = جديد SubWest2();
east.goEast(west);
}
}
نتائج التشغيل هي كما يلي. انسخ الكود.
SubWest1 + SubEast1
SubWest2 + SubEast1
عند تشغيل النظام، يتم إنشاء كائنات SubWest1 وSubEast1 أولاً، ثم يقوم العميل باستدعاء طريقة goEast() الخاصة بـ SubEast1 وتمرير كائن SubWest1. نظرًا لأن الكائن SubEast1 يتجاوز طريقة goEast() الخاصة بفئته الفائقة East، يحدث إرسال ديناميكي فردي في هذا الوقت. عندما يتلقى كائن SubEast1 المكالمة، فإنه سيحصل على كائن SubWest1 من المعلمة، لذلك يستدعي على الفور طريقة goWest1() لهذا الكائن ويمرر نفسه. نظرًا لأن كائن SubEast1 له الحق في اختيار الكائن الذي سيتم الاتصال به، فسيتم تنفيذ إرسال أسلوب ديناميكي آخر في هذا الوقت.
في هذا الوقت، حصل كائن SubWest1 على كائن SubEast1. من خلال استدعاء الأسلوب myName1() لهذا الكائن، يمكنك طباعة اسمك واسم كائن SubEast كما يلي:
وبما أن أحد هذين الاسمين يأتي من التسلسل الهرمي الشرقي والآخر يأتي من التسلسل الهرمي الغربي، فإن الجمع بينهما يتم تحديده ديناميكيًا. هذه هي آلية تنفيذ الإرسال المزدوج الديناميكي.
هيكل نمط الزائر
نمط الزائر مناسب للأنظمة ذات هياكل البيانات غير المحددة نسبيًا، فهو يفصل الاقتران بين بنية البيانات والعمليات التي تعمل على البنية، مما يسمح لمجموعة العمليات بالتطور بحرية نسبيًا. يظهر أدناه رسم تخطيطي مبسط لنمط الزائر:
يمكن لكل عقدة في بنية البيانات قبول مكالمة من زائر. تقوم هذه العقدة بتمرير كائن العقدة إلى كائن الزائر، ويقوم كائن الزائر بدوره بتنفيذ عمليات كائن العقدة. تسمى هذه العملية "الإرسال المزدوج". تستدعي العقدة الزائر، وتمرر نفسها، ويقوم الزائر بتنفيذ خوارزمية ضد هذه العقدة. يظهر أدناه رسم تخطيطي لفئة الزائر:
الأدوار المشاركة في وضع الزائر هي كما يلي:
● دور الزائر الملخص (الزائر) : يعلن عن عملية واحدة أو أكثر من عمليات الطريقة لتشكيل الواجهة التي يجب على جميع أدوار الزائر المحددة تنفيذها.
● دور الزائر الفعلي (ConcreteVisitor) : ينفذ الواجهة التي أعلنها الزائر المجرد، أي كل عملية وصول أعلنها الزائر المجرد.
● دور العقدة المجردة (العقدة) : يعلن عن عملية القبول ويقبل كائن الزائر كمعلمة.
● دور ConcreteNode : ينفذ عملية القبول المحددة بواسطة العقدة المجردة.
● دور كائن الهيكل (ObjectStructure) : لديه المسؤوليات التالية، ويمكنه اجتياز جميع العناصر الموجودة في الهيكل، إذا لزم الأمر، وتوفير واجهة عالية المستوى حتى تتمكن كائنات الزائر من الوصول إلى كل عنصر، إذا لزم الأمر، ويمكن تصميمها ككائن مركب أو مجموعة، مثل قائمة أو مجموعة.
كود المصدر
كما ترون، يقوم دور الزائر المجرد بإعداد عملية وصول لكل عقدة محددة. نظرًا لوجود عقدتين، هناك عمليتان وصول متطابقتان.
انسخ رمز الكود كما يلي:
زائر الواجهة العامة {
/**
* يتوافق مع عملية الوصول إلى NodeA
*/
زيارة باطلة عامة (عقدة NodeA) ؛
/**
* يتوافق مع عملية الوصول إلى NodeB
*/
زيارة باطلة عامة (عقدة NodeB) ؛
}
رمز نسخة فئة الزائر المحدد هو كما يلي:
الطبقة العامة VisitorA تنفذ الزائر {
/**
* يتوافق مع عملية الوصول إلى NodeA
*/
@تجاوز
زيارة باطلة عامة (عقدة NodeA) {
System.out.println(node.operationA());
}
/**
* يتوافق مع عملية الوصول إلى NodeB
*/
@تجاوز
زيارة باطلة عامة (عقدة NodeB) {
System.out.println(node.operationB());
}
}
رمز نسخة فئة الزائر المحدد للزائر هو كما يلي:
الطبقة العامة VisitorB تنفذ الزائر {
/**
* يتوافق مع عملية الوصول إلى NodeA
*/
@تجاوز
زيارة باطلة عامة (عقدة NodeA) {
System.out.println(node.operationA());
}
/**
* يتوافق مع عملية الوصول إلى NodeB
*/
@تجاوز
زيارة باطلة عامة (عقدة NodeB) {
System.out.println(node.operationB());
}
}
رمز نسخ فئة العقدة المجردة هو كما يلي:
عقدة فئة مجردة عامة {
/**
* قبول العملية
*/
قبول الفراغ الملخص العام (زائر زائر) ؛
}
فئة العقدة المحددة NodeA
انسخ رمز الكود كما يلي:
الطبقة العامة NodeA تمتد العقدة {
/**
* قبول العملية
*/
@تجاوز
قبول الفراغ العام (زائر زائر) {
Visitor.visit(this);
}
/**
*طريقة خاصة بالعقدة A
*/
عملية السلسلة العامةA(){
إرجاع "العقدة أ" ؛
}
}
فئة العقدة المحددة NodeB
انسخ رمز الكود كما يلي:
الطبقة العامة NodeB تمتد العقدة {
/**
* قبول الطريقة
*/
@تجاوز
قبول الفراغ العام (زائر زائر) {
Visitor.visit(this);
}
/**
* الأساليب الخاصة بـ NodeB
*/
عملية السلسلة العامةB () {
إرجاع "NodeB";
}
}
فئة دور الكائن الهيكلي يحمل دور الكائن الهيكلي مجموعة ويوفر طريقة add () للعالم الخارجي كعملية إدارة للمجموعة. من خلال استدعاء هذه الطريقة، يمكن إضافة عقدة جديدة ديناميكيًا.
انسخ رمز الكود كما يلي:
بنية الكائن العامة {
Private List<Node> العقد = new ArrayList<Node>();
/**
* تنفيذ عملية الطريقة
*/
إجراء باطل عام (زائر زائر) {
ل(عقدة العقدة: العقد)
{
العقدة.قبول(زائر);
}
}
/**
* إضافة عنصر جديد
*/
إضافة الفراغ العام (عقدة العقدة) {
nodes.add(node);
}
}
رمز نسخة فئة العميل كما يلي:
عميل الطبقة العامة {
public static void main(String[] args) {
// إنشاء كائن هيكلي
ObjectStructure os = new ObjectStructure();
// أضف عقدة إلى الهيكل
os.add(new NodeA());
// أضف عقدة إلى الهيكل
os.add(new NodeB());
//إنشاء زائر
زائر زائر = زائر جديدA();
os.action(visitor);
}
}
على الرغم من عدم ظهور بنية شجرة كائن معقدة ذات عقد فرعية متعددة في هذا التنفيذ التخطيطي، في الأنظمة الفعلية، يُستخدم نمط الزائر عادةً للتعامل مع هياكل شجرة الكائنات المعقدة، ويمكن استخدام نمط الزائر للتعامل مع مشكلات بنية الشجرة التي تمتد عبر تسلسلات هرمية متعددة . هذا هو المكان الذي يكون فيه نمط الزائر قويًا جدًا.
مخطط تسلسل عملية التحضير
أولاً، يقوم هذا العميل التوضيحي بإنشاء كائن بنية ثم تمرير كائن NodeA جديد وكائن NodeB جديد.
ثانيًا، يقوم العميل بإنشاء كائن VisitorA وتمرير هذا الكائن إلى كائن البنية.
ثم يقوم العميل باستدعاء أسلوب إدارة تجميع كائن البنية لإضافة العقد NodeA وNodeB إلى كائن البنية.
أخيرًا، يستدعي العميل طريقة الإجراء action() لكائن البنية لبدء عملية الوصول.
مخطط تسلسل عملية الوصول
سوف يجتاز كائن البنية جميع العقد في المجموعة التي يحفظها، والتي في هذا النظام هي العقد NodeA وNodeB. أولاً، سيتم الوصول إلى NodeA. يتكون هذا الوصول من العمليات التالية:
(1) يتم استدعاء طريقة قبول () لكائن NodeA وتمرير كائن VisitorA نفسه؛
(2) يستدعي كائن NodeA بدوره طريقة الوصول لكائن VisitorA ويمرر في كائن NodeA نفسه؛
(3) يستدعي كائن VisitorA الطريقة الفريدة OperationA() لكائن NodeA.
وبذلك تكتمل عملية الإرسال المزدوج، وسيتم الوصول إلى NodeB. وعملية الوصول هي نفس عملية الوصول إلى NodeA، والتي لن يتم وصفها هنا.
مزايا نمط الزائر
● يمكن أن تضيف القابلية للتوسعة الجيدة وظائف جديدة إلى العناصر الموجودة في بنية الكائن دون تعديل العناصر الموجودة في بنية الكائن.
● تتيح إمكانية إعادة الاستخدام الجيدة للزائرين تحديد الوظائف المشتركة في بنية الكائن بالكامل، وبالتالي تحسين درجة إمكانية إعادة الاستخدام.
● فصل السلوكيات غير ذات الصلة يمكنك استخدام الزائرين لفصل السلوكيات غير ذات الصلة، وتغليف السلوكيات ذات الصلة معًا لتكوين زائر، بحيث تكون وظيفة كل زائر واحدة نسبيًا.
عيوب نمط الزائر
● من الصعب تغيير بنية الكائن، وهو غير مناسب للمواقف التي تتغير فيها الفئات في بنية الكائن بشكل متكرر، نظرًا لأن بنية الكائن تتغير، يجب أن تتغير واجهة الزائر وتنفيذ الزائر وفقًا لذلك، وهو أمر مكلف للغاية.
● يتطلب كسر نمط الزائر المغلف عادةً أن تقوم بنية الكائن بفتح البيانات الداخلية للزائرين وObjectStructrue، الذي يكسر تغليف الكائن.