הזרקת תלויות - Dependency Injection ברמת הארכיטקטורה¶
הזרקת תלויות, Dependency Injection, מוכרת לרוב כטכניקת קוד - במקום שמחלקה תיצור בעצמה את התלויות שלה, מעבירים לה אותן מבחוץ. זה נכון, אבל זו רק חצי מהתמונה. ברמת הארכיטקטורה, DI היא הכלי שמאפשר למערכת שלמה להישאר גמישה מספיק כדי להחליף רכיבים מרכזיים - מסד נתונים, ספק תשלומים, שירות שליחת מיילים - בלי לפרק את המערכת מחדש.
ברמת הקוד - התזכורת המהירה¶
נניח מחלקת OrderService שצריכה לשלוח מייל אישור. בלי DI, היא יוצרת את שירות המייל בעצמה בפנים.
class OrderService:
def __init__(self):
self.email_service = SmtpEmailService() # תלות קשיחה
def place_order(self, order):
# ... לוגיקת הזמנה
self.email_service.send_confirmation(order)
עם DI, התלות מוזרקת מבחוץ, בדרך כלל דרך הקונסטרוקטור.
class OrderService:
def __init__(self, email_service: EmailService):
self.email_service = email_service
def place_order(self, order):
self.email_service.send_confirmation(order)
OrderService תלוי עכשיו בממשק EmailService, לא במימוש קונקרטי. זה ה-Dependency Inversion Principle מ-SOLID בפעולה - מודולים ברמה גבוהה לא תלויים במודולים ברמה נמוכה, שניהם תלויים בהפשטה משותפת.
המעבר לרמת הארכיטקטורה¶
עד כאן זו טכניקת קוד. הקפיצה לרמת הארכיטקטורה קורית כשמסתכלים על המערכת כולה, ולא על מחלקה בודדת. אם כל שירות במערכת בנוי כך שהוא תלוי בממשקים ולא במימושים קונקרטיים, כל המערכת כולה מקבלת תכונה חשובה - אפשר להחליף רכיב מרכזי שלם, בלי לגעת בכל שאר המערכת שתלויה בו.
דוגמה מוחשית - מערכת שמתחילה עם שירות תשלומים אחד, ואחרי שנתיים המנהלים מחליטים לעבור לספק אחר, או להוסיף ספק שני למקרה שהראשון נופל. אם כל השירותים במערכת תלויים בממשק PaymentProvider ולא במחלקה הקונקרטית של הספק הראשון, המעבר הוא עניין של מימוש מחלקה חדשה שמיישמת את אותו ממשק, והזרקה שלה במקום הישנה - בלי לגעת בעשרות מקומות בקוד שמשתמשים בתשלומים.
Composition Root - המקום שבו הכל מתחבר¶
בפרויקט קטן, אפשר להזריק תלויות ידנית בכל מקום. בפרויקט גדול, זה נעשה במקום מרכזי אחד שנקרא Composition Root - נקודת הכניסה של האפליקציה, שבה כל האובייקטים נבנים ומחוברים זה לזה, פעם אחת, בתחילת ההרצה.
# composition_root.py - הרכבה חד פעמית של המערכת
email_service = SmtpEmailService(config.SMTP_HOST)
payment_provider = StripePaymentProvider(config.STRIPE_KEY)
order_repo = SqlOrderRepository(config.DB_URL)
order_service = OrderService(
email_service=email_service,
payment_provider=payment_provider,
order_repo=order_repo,
)
זה נותן מבט אחד ויחיד על כל המערכת - איזה מימוש קונקרטי משמש עבור כל ממשק. מעבר לספק אחר, או להזרקת גרסה מדומה בבדיקות, קורה במקום אחד בלבד, ולא דורש חיפוש בכל הקוד.
מיכלי DI - כשההרכבה הידנית נהיית מסורבלת¶
במערכות גדולות עם עשרות תלויות, בנייה ידנית של כל האובייקטים ב-Composition Root יכולה להיות מסורבלת. מיכלי DI - כמו Spring ב-Java, Autofac ב-.NET, או ספריות מקבילות בשפות אחרות - מנהלים את הבנייה וההזרקה אוטומטית, לפי הגדרות (annotations או קונפיגורציה). הם לא הופכים את הרעיון לשונה מהותית, רק חוסכים קוד חיבור ידני חוזר בפרויקטים גדולים.
מתי DI מלא מוגזם¶
בפרויקט קטן עם רכיב אחד שסביר להניח לא יוחלף לעולם - מסד נתונים יחיד קבוע, ספק תשלומים יחיד - הזרקת תלויות מלאה עם ממשקים מופשטים לכל דבר היא הפשטה מוקדמת. הכלל הפרקטי - כדאי DI במיוחד עבור רכיבים שיש סבירות סבירה שיוחלפו, שרוצים להחליף בבדיקות עם מוקים, או שתלויים בשירותים חיצוניים לא יציבים. עבור לוגיקה פנימית טהורה שלא תלויה בכלום חיצוני, אין צורך להזריק כלום.
בקורס ארכיטקטורת תוכנה שלנו עוברים על DI ברמת המערכת כולה, לצד עקרון היפוך התלויות שממנו הוא נגזר.
התלבטתם אם להוסיף DI מלא לפרויקט או שזה מוגזם לגודל שלו? הצטרפו לקהילה בדיסקורד ותביאו את המקרה שלכם לדיון.
לסיכום¶
הזרקת תלויות ברמת הארכיטקטורה היא הרבה מעבר לטכניקת קוד להעברת פרמטרים לקונסטרוקטור - היא מה שמאפשר למערכת שלמה להישאר תלויה בממשקים ולא במימושים, כדי שאפשר יהיה להחליף רכיב מרכזי כמו ספק תשלומים או מסד נתונים במקום אחד, בלי לפרק את שאר המערכת. Composition Root מרכז את ההחלטה הזאת למקום אחד וברור.