لماذا تحتاج إلى وكيل؟
يساعد هذا النمط في حل المشكلات المرتبطة بالوصول المتحكم فيه إلى كائن ما. قد تتساءل: "لماذا نحتاج إلى الوصول الخاضع للرقابة؟" دعونا نلقي نظرة على بعض المواقف التي ستساعدك على معرفة ما هو.مثال 1
تخيل أن لدينا مشروعًا كبيرًا يحتوي على مجموعة من التعليمات البرمجية القديمة، حيث يوجد فئة مسؤولة عن تصدير التقارير من قاعدة البيانات. الفصل يعمل بشكل متزامن. أي أن النظام بأكمله يكون خاملاً بينما تقوم قاعدة البيانات بمعالجة الطلب. في المتوسط، يستغرق إنشاء التقرير 30 دقيقة. وعليه تبدأ عملية التصدير في تمام الساعة 12:30 ظهراً، وتتلقى الإدارة التقرير صباحاً. كشفت عملية التدقيق أنه سيكون من الأفضل أن تكون قادرًا على تلقي التقرير على الفور خلال ساعات العمل العادية. لا يمكن تأجيل وقت البدء، ولا يمكن للنظام الحظر أثناء انتظار الاستجابة من قاعدة البيانات. الحل هو تغيير طريقة عمل النظام، وإنشاء التقرير وتصديره في موضوع منفصل. سيسمح هذا الحل للنظام بالعمل كالمعتاد، وستتلقى الإدارة تقارير جديدة. ومع ذلك، هناك مشكلة: لا يمكن إعادة كتابة الكود الحالي، لأن أجزاء أخرى من النظام تستخدم وظائفه. في هذه الحالة، يمكننا استخدام نمط الوكيل لتقديم فئة وكيل وسيطة تتلقى طلبات تصدير التقارير، وتسجيل وقت البدء، وإطلاق سلسلة رسائل منفصلة. بمجرد إنشاء التقرير، ينتهي الموضوع ويكون الجميع سعداء.مثال 2
يقوم فريق التطوير بإنشاء موقع ويب للأحداث. للحصول على بيانات حول الأحداث الجديدة، يستعلم الفريق عن خدمة خارجية. مكتبة خاصة خاصة تسهل التفاعل مع الخدمة. أثناء التطوير، يتم اكتشاف مشكلة: يقوم نظام الطرف الثالث بتحديث بياناته مرة واحدة يوميًا، ولكن يتم إرسال طلب إليه في كل مرة يقوم فيها المستخدم بتحديث الصفحة. يؤدي هذا إلى إنشاء عدد كبير من الطلبات، وتتوقف الخدمة عن الاستجابة. الحل هو تخزين استجابة الخدمة مؤقتًا وإرجاع النتيجة المخزنة مؤقتًا للزائرين عند إعادة تحميل الصفحات، وتحديث ذاكرة التخزين المؤقت حسب الحاجة. في هذه الحالة، يعد نمط تصميم الوكيل حلاً ممتازًا لا يغير الوظيفة الحالية.المبدأ الكامن وراء نمط التصميم
لتنفيذ هذا النمط، تحتاج إلى إنشاء فئة وكيل. يقوم بتنفيذ واجهة فئة الخدمة، وتقليد سلوكها لرمز العميل. بهذه الطريقة، يتفاعل العميل مع الوكيل بدلاً من الكائن الحقيقي. كقاعدة عامة، يتم تمرير كافة الطلبات إلى فئة الخدمة، ولكن مع إجراءات إضافية قبل أو بعد. ببساطة، الوكيل هو طبقة بين رمز العميل والكائن الهدف. خذ بعين الاعتبار مثال نتائج استعلام التخزين المؤقت من قرص ثابت قديم وبطيء للغاية. لنفترض أننا نتحدث عن جدول زمني للقطارات الكهربائية في بعض التطبيقات القديمة التي لا يمكن تغيير منطقها. يتم إدخال قرص به جدول زمني محدث كل يوم في وقت محدد. اذا لدينا:TrainTimetable
واجهه المستخدم.ElectricTrainTimetable
، الذي ينفذ هذه الواجهة.- يتفاعل رمز العميل مع نظام الملفات من خلال هذه الفئة.
TimetableDisplay
فئة العميل. تستخدم طريقتهاprintTimetable()
أساليب الفصلElectricTrainTimetable
.
printTimetable()
، ElectricTrainTimetable
يصل الفصل إلى القرص، ويحمل البيانات، ويقدمها إلى العميل. يعمل النظام بشكل جيد، ولكنه بطيء جدًا. ونتيجة لذلك، تم اتخاذ القرار لزيادة أداء النظام عن طريق إضافة آلية التخزين المؤقت. يمكن القيام بذلك باستخدام نمط الوكيل: وبالتالي، TimetableDisplay
لا يلاحظ الفصل أنه يتفاعل مع الفصل ElectricTrainTimetableProxy
بدلاً من الفصل القديم. يقوم التنفيذ الجديد بتحميل الجدول الزمني مرة واحدة يوميًا. بالنسبة لطلبات التكرار، تقوم بإرجاع الكائن الذي تم تحميله مسبقًا من الذاكرة.
ما هي المهام الأفضل للوكيل؟
فيما يلي بعض المواقف التي سيكون فيها هذا النمط مفيدًا بالتأكيد:- التخزين المؤقت
- التهيئة المتأخرة أو البطيئة لماذا يتم تحميل كائن على الفور إذا كان بإمكانك تحميله حسب الحاجة؟
- طلبات التسجيل
- التحقق الوسيط من البيانات والوصول إليها
- بدء مؤشرات الترابط العاملة
- تسجيل الوصول إلى كائن
المميزات والعيوب
- + يمكنك التحكم في الوصول إلى كائن الخدمة كيفما تشاء
- + قدرات إضافية تتعلق بإدارة دورة حياة كائن الخدمة
- + يعمل بدون كائن خدمة
- + يعمل على تحسين الأداء وأمن التعليمات البرمجية.
- - هناك خطر أن يتدهور الأداء بسبب الطلبات الإضافية
- - يجعل التسلسل الهرمي للفصل أكثر تعقيدا
نمط الوكيل في الممارسة العملية
لننفذ نظامًا يقرأ جداول مواعيد القطارات من القرص الصلب:
public interface TrainTimetable {
String[] getTimetable();
String getTrainDepartureTime();
}
هذا هو الفصل الذي ينفذ الواجهة الرئيسية:
public class ElectricTrainTimetable implements TrainTimetable {
@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 TimetableDisplay {
private TrainTimetable trainTimetable = new ElectricTrainTimetable();
public void printTimetable() {
String[] timetable = trainTimetable.getTimetable();
String[] tmpArr;
System.out.println("Train\\tFrom\\tTo\\t\\tDeparture time\\tArrival time\\tTravel time");
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;London;Prague;13:43;21:15;07:32
BA-1404;Paris;Graz;14:25;21:25;07:00
9B-8710;Prague;Vienna;04:48;08:49;04:01;
9B-8122;Prague;Graz;04:48;08:49;04:01
دعونا اختبار ذلك:
public static void main(String[] args) {
TimetableDisplay timetableDisplay = new timetableDisplay();
timetableDisplay.printTimetable();
}
انتاج:
Train From To Departure time Arrival time Travel time
9B-6854 London Prague 13:43 21:15 07:32
BA-1404 Paris Graz 14:25 21:25 07:00
9B-8710 Prague Vienna 04:48 08:49 04:01
9B-8122 Prague Graz 04:48 08:49 04:01
الآن دعونا نتعرف على الخطوات المطلوبة لتقديم نمطنا:
-
حدد واجهة تسمح باستخدام الوكيل بدلاً من الكائن الأصلي. في مثالنا هذا
TrainTimetable
. -
قم بإنشاء فئة الوكيل. يجب أن يحتوي على مرجع لكائن الخدمة (قم بإنشائه في الفصل أو تمريره إلى المُنشئ).
إليك فئة الوكيل الخاصة بنا:
public class ElectricTrainTimetableProxy implements TrainTimetable { // Reference to the original object private TrainTimetable trainTimetable = new ElectricTrainTimetable(); private String[] timetableCache = null @Override public String[] getTimetable() { return trainTimetable.getTimetable(); } @Override public String getTrainDepartureTime(String trainId) { return trainTimetable.getTrainDepartureTime(trainId); } public void clearCache() { trainTimetable = null; } }
في هذه المرحلة، نقوم ببساطة بإنشاء فئة مع إشارة إلى الكائن الأصلي وإعادة توجيه جميع الاستدعاءات إليه.
-
دعونا ننفذ منطق فئة الوكيل. بشكل أساسي، يتم دائمًا إعادة توجيه المكالمات إلى الكائن الأصلي.
public class ElectricTrainTimetableProxy implements TrainTimetable { // Reference to the original object private TrainTimetable trainTimetable = new ElectricTrainTimetable(); private String[] timetableCache = null @Override public String[] getTimetable() { if (timetableCache == null) { timetableCache = trainTimetable.getTimetable(); } return timetableCache; } @Override public String getTrainDepartureTime(String trainId) { if (timetableCache == null) { timetableCache = trainTimetable.getTimetable(); } for (int i = 0; i < timetableCache.length; i++) { if (timetableCache[i].startsWith(trainId+";")) return timetableCache[i]; } return ""; } public void clearCache() { trainTimetable = null; } }
يتحقق
getTimetable()
مما إذا كان قد تم تخزين مصفوفة الجدول الزمني مؤقتًا في الذاكرة. إذا لم يكن الأمر كذلك، فإنه يرسل طلبًا لتحميل البيانات من القرص وحفظ النتيجة. إذا تم طلب الجدول الزمني بالفعل، فإنه يعيد الكائن من الذاكرة بسرعة.بفضل وظيفتها البسيطة، لم يكن من الضروري إعادة توجيه طريقة getTrainDepartureTime() إلى الكائن الأصلي. لقد قمنا ببساطة بتكرار وظائفه بطريقة جديدة.
لا تفعل هذا. إذا كان عليك تكرار الكود أو القيام بشيء مماثل، فهذا يعني أن هناك خطأ ما، وتحتاج إلى النظر إلى المشكلة مرة أخرى من زاوية مختلفة. وفي مثالنا البسيط، لم يكن لدينا خيار آخر. ولكن في المشاريع الحقيقية، من المرجح أن يتم كتابة التعليمات البرمجية بشكل صحيح أكثر.
-
في تعليمات العميل البرمجية، قم بإنشاء كائن وكيل بدلاً من الكائن الأصلي:
public class TimetableDisplay { // Changed reference private TrainTimetable trainTimetable = new ElectricTrainTimetableProxy(); public void printTimetable() { String[] timetable = trainTimetable.getTimetable(); String[] tmpArr; System.out.println("Train\\tFrom\\tTo\\t\\tDeparture time\\tArrival time\\tTravel time"); 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]); } } }
يفحص
Train From To Departure time Arrival time Travel time 9B-6854 London Prague 13:43 21:15 07:32 BA-1404 Paris Graz 14:25 21:25 07:00 9B-8710 Prague Vienna 04:48 08:49 04:01 9B-8122 Prague Graz 04:48 08:49 04:01
عظيم، أنه يعمل بشكل صحيح.
يمكنك أيضًا التفكير في خيار المصنع الذي يقوم بإنشاء كائن أصلي وكائن وكيل، وفقًا لشروط معينة.
GO TO FULL VERSION