لماذا تحتاج إلى نائب؟
يساعد هذا النمط في حل المشكلات المرتبطة بالوصول المتحكم فيه إلى كائن ما. قد يكون لديك سؤال: "لماذا نحتاج إلى مثل هذا الوصول الخاضع للرقابة؟" دعونا نلقي نظرة على بعض المواقف التي ستساعدك على معرفة ما هو.مثال 1
لنتخيل أن لدينا مشروعًا كبيرًا يحتوي على مجموعة من الأكواد القديمة، حيث يوجد فئة مسؤولة عن تنزيل التقارير من قاعدة البيانات. تعمل الفئة بشكل متزامن، أي أن النظام بأكمله يكون خاملاً أثناء معالجة قاعدة البيانات للطلب. في المتوسط، يتم إنشاء التقرير خلال 30 دقيقة. وبسبب هذه الميزة، يبدأ تحميله في الساعة 00:30، وتتلقى الإدارة هذا التقرير في الصباح. أثناء التحليل تبين أنه من الضروري استلام التقرير مباشرة بعد إنشائه، أي خلال يوم واحد. من المستحيل إعادة جدولة وقت البدء، لأن النظام سينتظر الرد من قاعدة البيانات. الحل هو تغيير مبدأ التشغيل عن طريق بدء التحميل وإنشاء التقرير في موضوع منفصل. سيسمح هذا الحل للنظام بالعمل كالمعتاد، وستتلقى الإدارة تقارير جديدة. ومع ذلك، هناك مشكلة: لا يمكن إعادة كتابة الكود الحالي، حيث يتم استخدام وظائفه بواسطة أجزاء أخرى من النظام. في هذه الحالة، يمكنك تقديم فئة وكيل وسيطة باستخدام نمط النائب، والتي ستتلقى طلبًا لتحميل تقرير وتسجيل وقت البدء وإطلاق سلسلة رسائل منفصلة. عندما يتم إنشاء التقرير، سيكمل الموضوع عمله وسيكون الجميع سعداء.مثال 2
يقوم فريق التطوير بإنشاء موقع ويب للملصقات. للحصول على بيانات حول الأحداث الجديدة، يلجأون إلى خدمة جهة خارجية، ويتم التفاعل معها من خلال مكتبة مغلقة خاصة. أثناء التطوير، نشأت مشكلة: يقوم نظام تابع لجهة خارجية بتحديث البيانات مرة واحدة يوميًا، ويحدث طلب لها في كل مرة يقوم فيها المستخدم بتحديث الصفحة. يؤدي هذا إلى إنشاء عدد كبير من الطلبات وتتوقف الخدمة عن الاستجابة. الحل هو تخزين استجابة الخدمة مؤقتًا وتزويد الزائرين بالنتيجة المحفوظة عند كل عملية إعادة تشغيل، وتحديث ذاكرة التخزين المؤقت هذه حسب الحاجة. في هذه الحالة، يعد استخدام نمط النائب حلاً ممتازًا دون تغيير الوظيفة النهائية.كيف يعمل النمط
لتنفيذ هذا النمط، تحتاج إلى إنشاء فئة وكيل. وهي تنفذ واجهة فئة الخدمة، ومحاكاة سلوكها لرمز العميل. وبالتالي، بدلاً من الكائن الحقيقي، يتفاعل العميل مع الوكيل الخاص به. عادةً، يتم تمرير كافة الطلبات إلى فئة الخدمة، ولكن مع إجراءات إضافية قبل أو بعد استدعاءها. ببساطة، كائن الوكيل هذا عبارة عن طبقة بين رمز العميل والكائن الهدف. دعونا نلقي نظرة على مثال للتخزين المؤقت لطلب من قرص قديم بطيء جدًا. فليكن جدولًا زمنيًا للقطار الكهربائي في بعض التطبيقات القديمة، والذي لا يمكن تغيير مبدأ تشغيله. يتم إدخال القرص بالجدول المحدث كل يوم في وقت محدد. اذا لدينا:- واجهه المستخدم
TimetableTrains
. - الفئة
TimetableElectricTrains
التي تنفذ هذه الواجهة. - من خلال هذه الفئة يتفاعل رمز العميل مع نظام ملفات القرص.
- فئة العميل
DisplayTimetable
. تستخدم طريقتهاprintTimetable()
أساليبTimetableElectricTrains
.
printTimetable()
الفصل TimetableElectricTrains
إلى القرص، ويفرغ البيانات ويقدمها للعميل. يعمل هذا النظام بشكل جيد، ولكنه بطيء جدًا. ولذلك تقرر زيادة أداء النظام عن طريق إضافة آلية التخزين المؤقت. يمكن القيام بذلك باستخدام نمط الوكيل: بهذه الطريقة لن يلاحظ الفصل أنه يتفاعل مع الفصل وليس مع الفصل السابق. يقوم التطبيق الجديد بتحميل الجدول مرة واحدة يوميًا، وبناءً على الطلبات المتكررة، يقوم بإرجاع الكائن الذي تم تحميله بالفعل من الذاكرة. DisplayTimetable
TimetableElectricTrainsProxy
ما هي المهام الأفضل لاستخدام الوكيل؟
فيما يلي بعض المواقف التي سيكون فيها هذا النمط مفيدًا بالتأكيد:- التخزين المؤقت.
- يُعرف التنفيذ البطيء أيضًا بالتنفيذ البطيء. لماذا تقوم بتحميل كائن مرة واحدة بينما يمكنك تحميله حسب الحاجة؟
- طلبات التسجيل.
- البيانات المؤقتة والتحقق من الوصول.
- إطلاق خيوط المعالجة المتوازية.
- تسجيل أو حساب تاريخ المكالمة.
المميزات والعيوب
- + يمكنك التحكم في الوصول إلى كائن الخدمة كما يحلو لك؛
- + قدرات إضافية لإدارة دورة حياة كائن الخدمة؛
- + يعمل بدون كائن خدمة؛
- + تحسين أداء التعليمات البرمجية والأمن.
- - هناك خطر تدهور الأداء بسبب العلاجات الإضافية.
- - يعقد هيكل فئات البرنامج.
نمط بديل في الممارسة العملية
لننفذ معك نظامًا يقرأ جداول القطارات من القرص:public interface TimetableTrains {
String[] getTimetable();
String getTrainDepartureTime();
}
فئة تنفذ الواجهة الرئيسية:
public class TimetableElectricTrains implements TimetableTrains {
@Override
public String[] getTimetable() {
ArrayList<String> list = new ArrayList<>();
try {
Scanner scanner = new Scanner(new FileReader(new File("/tmp/electric_trains.csv")));
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
list.add(line);
}
} catch (IOException e) {
System.err.println("Error: " + e);
}
return list.toArray(new String[list.size()]);
}
@Override
public String getTrainDepartureTime(String trainId) {
String[] timetable = getTimetable();
for(int i = 0; i<timetable.length; i++) {
if(timetable[i].startsWith(trainId+";")) return timetable[i];
}
return "";
}
}
في كل مرة تحاول فيها الحصول على الجدول الزمني لجميع القطارات، يقرأ البرنامج الملف من القرص. ولكن هذه لا تزال الزهور. تتم قراءة الملف أيضًا في كل مرة تحتاج فيها إلى الحصول على الجدول الزمني لقطار واحد فقط! من الجيد أن هذا الرمز موجود فقط في الأمثلة السيئة :) فئة العميل:
public class DisplayTimetable {
private TimetableTrains timetableTrains = new TimetableElectricTrains();
public void printTimetable() {
String[] timetable = timetableTrains.getTimetable();
String[] tmpArr;
System.out.println("Поезд\tОткуда\tКуда\t\tВремя отправления\tВремя прибытия\tВремя в пути");
for(int i = 0; i < timetable.length; i++) {
tmpArr = timetable[i].split(";");
System.out.printf("%s\t%s\t%s\t\t%s\t\t\t\t%s\t\t\t%s\n", tmpArr[0], tmpArr[1], tmpArr[2], tmpArr[3], tmpArr[4], tmpArr[5]);
}
}
}
ملف المثال:
9B-6854;Лондон;Прага;13:43;21:15;07:32
BA-1404;Париж;Грац;14:25;21:25;07:00
9B-8710;Прага;Вена;04:48;08:49;04:01;
9B-8122;Прага;Грац;04:48;08:49;04:01
دعونا اختبار:
public static void main(String[] args) {
DisplayTimetable displayTimetable = new DisplayTimetable();
displayTimetable.printTimetable();
}
خاتمة:
Поезд Откуда Куда Время отправления Время прибытия Время в пути
9B-6854 Лондон Прага 13:43 21:15 07:32
BA-1404 Париж Грац 14:25 21:25 07:00
9B-8710 Прага Вена 04:48 08:49 04:01
9B-8122 Прага Грац 04:48 08:49 04:01
الآن دعونا نستعرض خطوات تنفيذ النمط الخاص بنا:
-
حدد واجهة تسمح لك باستخدام وكيل جديد بدلاً من الكائن الأصلي. في مثالنا هو
TimetableTrains
. -
إنشاء فئة الوكيل. يجب أن يحتوي على مرجع لكائن خدمة (إنشاء في فئة أو تمرير في مُنشئ)؛
هنا فئة الوكيل لدينا:
public class TimetableElectricTrainsProxy implements TimetableTrains { // Ссылка на оригинальный an object private TimetableTrains timetableTrains = new TimetableElectricTrains(); private String[] timetableCache = null @Override public String[] getTimetable() { return timetableTrains.getTimetable(); } @Override public String getTrainDepartureTime(String trainId) { return timetableTrains.getTrainDepartureTime(trainId); } public void clearCache() { timetableTrains = null; } }
في هذه المرحلة، نقوم ببساطة بإنشاء فئة بمرجع إلى الكائن الأصلي وتمرير كافة الاستدعاءات إليه.
-
نحن ننفذ منطق فئة الوكيل. في الأساس، يتم دائمًا إعادة توجيه المكالمة إلى الكائن الأصلي.
public class TimetableElectricTrainsProxy implements TimetableTrains { // Ссылка на оригинальный an object private TimetableTrains timetableTrains = new TimetableElectricTrains(); private String[] timetableCache = null @Override public String[] getTimetable() { if(timetableCache == null) { timetableCache = timetableTrains.getTimetable(); } return timetableCache; } @Override public String getTrainDepartureTime(String trainId) { if(timetableCache == null) { timetableCache = timetableTrains.getTimetable(); } for(int i = 0; i < timetableCache.length; i++) { if(timetableCache[i].startsWith(trainId+";")) return timetableCache[i]; } return ""; } public void clearCache() { timetableTrains = null; } }
تتحقق الطريقة
getTimetable()
مما إذا كان مصفوفة الجدولة مخزنة مؤقتًا في الذاكرة. إذا لم يكن الأمر كذلك، فإنه يصدر طلبًا لتحميل البيانات من القرص، وتخزين النتيجة. إذا كان الطلب قيد التشغيل بالفعل، فسيقوم بإرجاع كائن من الذاكرة بسرعة.بفضل وظيفتها البسيطة، لم يكن من الضروري إعادة توجيه طريقة getTrainDepartireTime() إلى الكائن الأصلي. لقد قمنا ببساطة بتكرار وظائفها بطريقة جديدة.
لا يمكنك أن تفعل ذلك. إذا اضطررت إلى تكرار التعليمات البرمجية أو إجراء عمليات معالجة مماثلة، فهذا يعني أن هناك خطأ ما وتحتاج إلى النظر إلى المشكلة من زاوية مختلفة. في مثالنا البسيط لا توجد طريقة أخرى، ولكن في المشاريع الحقيقية، على الأرجح، سيتم كتابة التعليمات البرمجية بشكل صحيح أكثر.
-
استبدل إنشاء الكائن الأصلي في كود العميل بكائن بديل:
public class DisplayTimetable { // Измененная link private TimetableTrains timetableTrains = new TimetableElectricTrainsProxy(); public void printTimetable() { String[] timetable = timetableTrains.getTimetable(); String[] tmpArr; System.out.println("Поезд\tОткуда\tКуда\t\tВремя отправления\tВремя прибытия\tВремя в пути"); for(int i = 0; i<timetable.length; i++) { tmpArr = timetable[i].split(";"); System.out.printf("%s\t%s\t%s\t\t%s\t\t\t\t%s\t\t\t%s\n", tmpArr[0], tmpArr[1], tmpArr[2], tmpArr[3], tmpArr[4], tmpArr[5]); } } }
فحص
Поезд Откуда Куда Время отправления Время прибытия Время в пути 9B-6854 Лондон Прага 13:43 21:15 07:32 BA-1404 Париж Грац 14:25 21:25 07:00 9B-8710 Прага Вена 04:48 08:49 04:01 9B-8122 Прага Грац 04:48 08:49 04:01
عظيم، أنه يعمل بشكل صحيح.
يمكنك أيضًا التفكير في مصنع يقوم بإنشاء الكائن الأصلي والكائن البديل وفقًا لشروط معينة.
رابط مفيد بدلا من النقطة
-
مقال ممتاز عن الأنماط والقليل عن "النائب"
GO TO FULL VERSION