תבניות מבנה בתכנות - הסבר ודוגמאות¶
בפוסטים הקודמים דיברנו על תבניות עיצוב באופן כללי, ואז צללנו לתבניות היצירה - Factory, Builder ו-Singleton. עכשיו תור הקטגוריה השנייה - תבניות מבנה. אם תבניות יצירה עונות על "איך יוצרים אובייקט", תבניות מבנה עונות על שאלה אחרת - איך מרכיבים אובייקטים ומחלקות קיימים למבנה גדול יותר, בלי לשבור אותם ובלי לכתוב קוד מסורבל.
Adapter - כשצריך לחבר שני דברים שלא מדברים אותה שפה¶
תבנית Adapter פותרת בעיה שכל מפתח נתקל בה מוקדם או מאוחר - יש לכם שתי מערכות שאמורות לעבוד יחד, אבל הממשקים שלהן פשוט לא תואמים. למשל, אתם עובדים עם ספריית תשלומים ישנה שמחזירה תשובות בפורמט אחד, ועכשיו צריך להחליף אותה בספק חדש שמחזיר פורמט אחר לגמרי, בלי לשנות את כל הקוד שכבר קורא לספק הישן.
class LegacyPaymentAPI:
def make_payment(self, amount_in_cents):
return {"status": "ok", "amount": amount_in_cents}
class NewPaymentAPI:
def charge(self, amount_dollars):
return {"success": True, "charged": amount_dollars}
class PaymentAdapter:
def __init__(self, new_api):
self._new_api = new_api
def make_payment(self, amount_in_cents):
result = self._new_api.charge(amount_in_cents / 100)
return {"status": "ok" if result["success"] else "fail", "amount": amount_in_cents}
ה-Adapter "מתרגם" בין הממשק הישן לחדש, כך שכל הקוד שכבר קורא ל-make_payment ממשיך לעבוד בלי לדעת שמאחורי הקלעים הכל השתנה. זו אחת התבניות השימושיות ביותר כשעובדים עם קוד legacy או ספריות חיצוניות שאין לכם שליטה עליהן.
Decorator - כשרוצים להוסיף התנהגות בלי לשנות את המחלקה המקורית¶
תבנית Decorator מאפשרת "לעטוף" אובייקט קיים בשכבה נוספת של התנהגות, בלי לגעת בקוד המקורי שלו ובלי ליצור מחלקת ירושה חדשה לכל שילוב אפשרי. הדוגמה הקלאסית היא מערכת קפה - קפה רגיל, קפה עם חלב, קפה עם חלב וסוכר. בלי Decorator, הייתם צריכים מחלקה נפרדת לכל שילוב.
class Coffee:
def cost(self):
return 10
class MilkDecorator:
def __init__(self, coffee):
self._coffee = coffee
def cost(self):
return self._coffee.cost() + 2
order = MilkDecorator(Coffee())
print(order.cost()) # 12
אפשר לעטוף עוד ועוד שכבות - סוכר, קצף, תוסף וניל - כל אחת מוסיפה התנהגות בלי לשנות אף מחלקה קיימת. בפייתון עצמו יש תמיכה ישירה ברעיון הזה עם @decorator, שהוא בעצם יישום של אותו עיקרון ברמת השפה.
Facade - כשמערכת מורכבת צריכה חזית פשוטה¶
תבנית Facade נותנת ממשק פשוט ונקי מעל מערכת מורכבת עם הרבה חלקים פנימיים. במקום שהקוד שקורא למערכת יצטרך להכיר עשרה מחלקות שונות ולדעת בדיוק באיזה סדר לקרוא להן, יש מחלקה אחת - החזית - שמסתירה את כל המורכבות הזאת מאחורי מתודה אחת פשוטה.
דוגמה טובה היא תהליך הזמנה באתר מסחר - מאחורי הקלעים יש בדיקת מלאי, חיוב אשראי, יצירת משלוח ושליחת מייל אישור, שכל אחד מהם הוא מחלקה נפרדת עם לוגיקה משלה. מחלקת OrderFacade חושפת החוצה רק מתודה אחת, place_order, שמתאמת בפנים בין כל החלקים האלה. מי שמשתמש בחזית לא צריך לדעת שיש בכלל ארבע מחלקות מאחורי הקלעים.
איך יודעים איזו תבנית מתאימה¶
הכלל דומה לתבניות היצירה - תתחילו מהבעיה, לא מהתבנית. אם יש לכם שני ממשקים לא תואמים שצריכים לעבוד יחד - זה Adapter. אם רוצים להוסיף התנהגות לאובייקט בצורה גמישה ומורכבת משילובים - זה Decorator. אם מערכת מורכבת מדי לשימוש ורוצים לתת חזית פשוטה מעליה - זה Facade.
חשוב לזכור שהמטרה המשותפת של כל תבניות המבנה היא לצמצם צימוד (coupling) בין חלקי הקוד, כדי ששינוי במקום אחד לא יגרור שינוי בכל מקום אחר שמשתמש בו.
לסיכום¶
תבניות מבנה עוסקות בהרכבה, לא ביצירה - איך לוקחים אובייקטים קיימים ומחברים אותם למבנים גדולים יותר בצורה גמישה. Adapter מתרגם בין ממשקים לא תואמים, Decorator מוסיף התנהגות בלי לשנות קוד קיים, ו-Facade מסתיר מורכבות מאחורי ממשק פשוט. בפוסט הבא נעבור לקטגוריה השלישית והאחרונה - תבניות התנהגות.
בקורס ארכיטקטורת תוכנה שלנו אנחנו עוברים על כל תבניות המבנה עם דוגמאות קוד מלאות ומקרים אמיתיים מפרויקטים.
יש לכם קוד legacy שמסרב להתחבר יפה למערכת החדשה? הצטרפו לקהילה בדיסקורד, אולי דווקא Adapter הוא הפתרון שחיפשתם.