למה אתה צריך פרוקסי?
דפוס זה עוזר לפתור בעיות הקשורות לגישה מבוקרת לאובייקט. אתה עשוי לשאול, "למה אנחנו צריכים גישה מבוקרת?" בואו נסתכל על כמה מצבים שיעזרו לכם להבין מה זה מה.דוגמה 1
תארו לעצמכם שיש לנו פרויקט גדול עם חבורה של קוד ישן, שבו יש כיתה שאחראית על ייצוא דוחות ממסד נתונים. הכיתה עובדת באופן סינכרוני. כלומר, המערכת כולה פעילה בזמן שבסיס הנתונים מעבד את הבקשה. בממוצע, לוקח 30 דקות להפיק דוח. בהתאם לכך, תהליך הייצוא מתחיל בשעה 12:30, וההנהלה מקבלת את הדיווח בבוקר. בדיקה העלתה כי עדיף לקבל מיד את הדיווח בשעות העבודה הרגילות. לא ניתן לדחות את שעת ההתחלה, והמערכת לא יכולה לחסום בזמן שהיא ממתינה לתגובה ממסד הנתונים. הפתרון הוא לשנות את אופן פעולת המערכת, הפקה וייצוא של הדוח בשרשור נפרד. פתרון זה יאפשר למערכת לעבוד כרגיל, וההנהלה תקבל דיווחים טריים. עם זאת, יש בעיה: לא ניתן לשכתב את הקוד הנוכחי, מכיוון שחלקים אחרים של המערכת משתמשים בפונקציונליות שלו. במקרה זה, נוכל להשתמש בדפוס ה-proxy כדי להציג מחלקת פרוקסי ביניים שתקבל בקשות לייצוא דוחות, לרשום את שעת ההתחלה ולהפעיל שרשור נפרד. ברגע שהדוח נוצר, השרשור מסתיים וכולם מרוצים.דוגמה 2
צוות פיתוח יוצר אתר אירועים. כדי לקבל נתונים על אירועים חדשים, הצוות שואל שירות של צד שלישי. ספרייה פרטית מיוחדת מקלה על האינטראקציה עם השירות. במהלך הפיתוח מתגלה בעיה: מערכת הצד השלישי מעדכנת את הנתונים שלה פעם ביום, אך נשלחת אליה בקשה בכל פעם שמשתמש מרענן עמוד. זה יוצר מספר רב של בקשות, והשירות מפסיק להגיב. הפתרון הוא לאחסן את תגובת השירות במטמון ולהחזיר את התוצאה המאוחסנת למבקרים כאשר הדפים נטענים מחדש, תוך עדכון המטמון לפי הצורך. במקרה זה, דפוס עיצוב ה-proxy הוא פתרון מצוין שאינו משנה את הפונקציונליות הקיימת.העיקרון מאחורי תבנית העיצוב
כדי ליישם דפוס זה, עליך ליצור מחלקת proxy. הוא מיישם את הממשק של מחלקת השירות, מחקה את ההתנהגות שלו עבור קוד הלקוח. באופן זה, הלקוח מקיים אינטראקציה עם פרוקסי במקום האובייקט האמיתי. ככלל, כל הבקשות מועברות למחלקת השירות, אך עם פעולות נוספות לפני או אחרי. במילים פשוטות, פרוקסי הוא שכבה בין קוד הלקוח לאובייקט היעד. שקול את הדוגמה של תוצאות שאילתות במטמון מדיסק קשיח ישן ואיטי מאוד. נניח שאנחנו מדברים על לוח זמנים של רכבות חשמליות באיזו אפליקציה עתיקה שאי אפשר לשנות את ההיגיון שלה. מדי יום מוכנס דיסק עם לוח זמנים מעודכן בשעה קבועה. אז יש לנו:TrainTimetable
מִמְשָׁק.ElectricTrainTimetable
, המיישמת את הממשק הזה.- קוד הלקוח מקיים אינטראקציה עם מערכת הקבצים באמצעות מחלקה זו.
TimetableDisplay
כיתת לקוחות. השיטה שלוprintTimetable()
משתמשת בשיטות שלElectricTrainTimetable
המחלקה.
printTimetable()
השיטה, ElectricTrainTimetable
המחלקה ניגשת לדיסק, טוענת את הנתונים ומציגה אותם ללקוח. המערכת פועלת בסדר, אבל היא איטית מאוד. כתוצאה מכך, התקבלה ההחלטה להגביר את ביצועי המערכת על ידי הוספת מנגנון מטמון. ניתן לעשות זאת באמצעות דפוס ה-proxy: לפיכך, 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 "";
}
}
בכל פעם שאתה מקבל את לוח הזמנים של הרכבת, התוכנית קוראת קובץ מהדיסק. אבל זו רק ההתחלה של הצרות שלנו. הקובץ כולו נקרא בכל פעם שאתה מקבל את לוח הזמנים אפילו לרכבת בודדת! טוב שקוד כזה קיים רק בדוגמאות של מה לא לעשות :) Class Client:
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
. -
צור את מחלקת ה-proxy. צריך להיות לו הפניה לאובייקט השירות (צור אותו במחלקה או העבר לבנאי).
הנה כיתת ה-proxy שלנו:
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; } }
בשלב זה, אנו פשוט יוצרים מחלקה עם הפניה לאובייקט המקורי ומעבירים אליו את כל הקריאות.
-
בואו ליישם את ההיגיון של מחלקת ה-proxy. בעיקרון, שיחות מופנות תמיד לאובייקט המקורי.
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() לאובייקט המקורי. פשוט שכפלנו את הפונקציונליות שלו בשיטה חדשה.
אל תעשה את זה. אם אתה צריך לשכפל את הקוד או לעשות משהו דומה, משהו השתבש, ואתה צריך להסתכל על הבעיה שוב מזווית אחרת. בדוגמה הפשוטה שלנו, לא הייתה לנו אפשרות אחרת. אבל בפרויקטים אמיתיים, סביר להניח שהקוד ייכתב בצורה נכונה יותר.
-
בקוד הלקוח, צור אובייקט proxy במקום האובייקט המקורי:
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