Dependency injection

הזרקת תלויות (באנגלית: dependency injection) היא תבנית עיצוב המאפשרת בחירה של רכיבי תוכנה בזמן ריצה (ולא בזמן ההידור). תבנית זו יכולה, לדוגמה, לשמש כדרך פשוטה לטעינה דינאמית של plug-ins או בחירה באובייקטי דמה (mock objects) בסביבות בדיקה, במקום להשתמש באובייקטים אמיתיים של סביבת הייצור. תבנית עיצוב זו מזריקה את האלמנט שתלויים בו (אובייקט או ערך, וכדומה) אל היעד שלו, בהתבסס על ידיעה של הצרכים של היעד. הזרקת תלויות כוללת לפחות שלושה אלמנטים: "צרכן" התלוי (dependent) באלמנט מסוג מסוים. הצהרה על התלויות (dependencies) של הרכיב, אשר מוגדרות בצורה של חוזים על בסיס ממשקים. "מזריק" (injector), לעיתים נקרא גם "ספק" (provider) או container, אשר יוצר מופעים של מחלקות המממשות ממשק של תלות מסוימת, בהתאם לדרישה. האובייקט "התלוי" מתאר את רכיב התוכנה שהוא תלוי בו לצורך ביצוע העבודה שלו. "המזריק" מחליט אילו מחלקות קונקרטיות (concrete classes) מספקות את הדרישות של האובייקט התלוי, ומספק לו אותן. בתכנות מסורתי, האובייקט התלוי מחליט בעצמו באיזה סוג של מחלקה קונקרטית הוא ישתמש. בעבודה עם הזרקת תלויות, החלטה זו מועברת אל ה"מזריק" שיכול להחליט בזמן ריצה להחליף בין מימושים קונקרטיים שונים של הממשק של התלות (dependency). היכולת לבצע את ההחלטה הזו בזמן ריצה (ולא בזמן ההידור) היא היתרון המרכזי של הזרקת תלויות. בזמן ריצה, ניתן ליצור מימושים רבים שונים של אותו רכיב תוכנה, ולהעביר ("להזריק") אותו לאותו קוד בדיקות. כך קוד הבדיקות יכול לבדוק את כל רכיבי התוכנה השונים, מבלי להיות מודע לכך שמה שהוזרק לו ממומש באופן שונה. המטרה העיקרית בשימוש בהזרקת תלויות היא על מנת לאפשר בחירה מבין מגוון מימושים של ממשק מסוים (הוא התלות - dependency) - בזמן ריצה או באמצעות קובצי קונפיגורציה, במקום בזמן ההידור. תבנית עיצוב זו שימושית במיוחד על מנת לספק מימושי "דמה" (mock) של רכיבים מורכבים בזמן ביצוע בדיקות תוכנה. כמו כן, לעיתים קרובות משתמשים בתבנית זו גם לצורך איתור רכיבי פלאג-אין או לאיתור ואתחול של שירותי תוכנה. ביצוע של בדיקות יחידה של רכיבים במערכות תוכנה גדולות הוא קשה בגלל שלעיתים קרובות הרכיבים הנבדקים דורשים קיום של תשתית רחבה על מנת שבכלל יוכלו לפעול. הזרקת תלויות מפשטת את תהליך היצירה של מופע עובד של רכיב בודד לצורך בדיקה שלו. בזכות העובדה שרכיבים מכריזים על התלויות (dependencies) שלהם, טסט יכול באופן אוטומטי להביא רק את הרכיבים שתלויים בהם, הנדרשים לצורך ביצוע הבדיקה. מעבר לזאת, בזמן ביצוע בדיקות, ניתן להגדיר את ה"מזריק" להחליף את המימוש הרגיל של הרכיב שתלויים בו, במימוש "דמה" מפושט. הרעיון הוא שהרכיב הנבדק יכול להיבדק בנפרד כל עוד הרכיבים שמוזרקים לו מממשים את החוזה של הממשק שהוא תלוי בו, באופן מספק לצורך ביצוע בדיקת היחידה הספציפית. בתור דוגמה ניתן לחשוב על תוכנה למסחר אוטומטי במניות, אשר מתקשרת בזמן אמת עם שירות מסחר און-ליין, ושומרת את הנתונים ההיסטוריים בבסיס נתונים מבוזר. במצב רגיל, על מנת לבדוק את הרכיב שממליץ על עסקאות, יהיה צורך בחיבור אל שירות האון-ליין, ובבסיס נתונים מבוזר אמיתי, המכיל נתונים מתאימים לצורך הבדיקה. בשימוש בהזרקת תלויות, ניתן להחליף לגמרי את הרכיבים שמספקים גישה אל שירות האון-ליין ואל בסיס הנתונים, במימושי דמה של הממשקים שתלויים בהם, ובכך לספק את ה"חוזה", ולספק רק את הפונקציונליות הנדרשת לצורך ביצוע הבדיקות על הרכיב הנבדק, ולא מעבר לכך. ללא הזרקת תלויות, הרכיב ה"צרכן" אשר נזקק לשירות מסוים לצורך ביצוע משימה כלשהי, חייב ליצור מופע של מחלקה קונקרטית אשר מממשת את ממשק התלות. לעומת זאת, כאשר משתמשים בהזרקת תלויות, הרכיב ה"צרכן" מציין את חוזה השירות על ידי ממשק, והרכיב ה"מזריק" בוחר את המימוש עבור הרכיב ה"תלוי". במימוש הפשוט ביותר שלו, הקוד שיוצר את האובייקטים שהצרכן תלוי בהם, מספק את התלויות לאובייקט הזה דרך ארגומנטים של constructor או על ידי קביעת ערכים למשתני מחלקה (properties) של האובייקט (באמצעות מתודות set). מימושים מורכבים יותר, כדוגמת Google Guice ,Spring Framework ו-(Microsoft Managed Extensibility Framework (MEF, מספקים אוטומציה של תהליך זה. תשתיות תוכנה אלה מזהות את הארגומנטים שקונסטרקטורים מקבלים, או את משתני המחלקה שיש ליצור, בתור בקשות לאובייקטים שתלויים בהם. במסגרת תהליך היצירה של האובייקט התלוי, הן אוטומטית מזריקות לו מופעים של התלויות שנוצרים מראש, כארגומנטים לקונסטרקטור או קובעות אותם כערכים עבור משתני המחלקה שלו. הלקוח יוצר בקשה למערכת הזרקת התלויות לקבלת מימוש של ממשק מסוים; מערכת הזרקת התלויות יוצרת את האובייקט, ובאופן אוטומטי ממלאת את התלויות בהתאם לצורך. יתרון אחד של שימוש בגישת הזרקת התלויות הוא בהפחתת הכמות של קוד החוזר על עצמו (boilerplate code) באובייקטים של היישום, מאחר שהעבודה לאתחול או הקמה של התלויות מבוצעת על ידי הרכיב ה"ספק". יתרון נוסף הוא בכך שהזרקת תלויות מאפשרת גמישות בקונפיגורציה. ניתן להשתמש במימושים אלטרנטיביים של שירות מסוים מבלי להדר מחדש את הקוד. דבר זה שימושי בבדיקות יחידה מכיוון שזה מקל על הזרקת מימושים "מזויפים" של שירות לתוך אובייקט נבדק, על ידי עריכת קובץ קונפיגורציה, או על ידי שינויים ברישום האובייקטים בזמן ריצה. כמו כן, שימוש בהזרקת תלויות מסייע בכתיבת קוד נוח לבדיקה. מומחה התוכנה מרטין פאולר זיהה שלוש דרכים בהן אובייקט יכול לקבל קישור (reference) למודול חיצוני, בהתאם לתבנית המשמשת כדי לספק את התלות: הזרקת ממשק (interface injection) – המודול המיוצא מספק ממשק שהמשתמשים בו חייבים לממש על מנת לקבל את התלויות בזמן ריצה. הזרקה באמצעות מתודות setter injection) set) – המודול התלוי (dependent), חושף מתודת set שהתשתית משתמשת בה כדי להזריק דרכה את התלות (dependency). הזרקה באמצעות קונסטרקטור (constructor injection) – התלויות מסופקות דרך פונקציית הבנאי של המחלקה. תשתיות תוכנה אחרות יכולות להשתמש גם בסוגי הזרקה שונים מאלה שמפורטים כאן. בהתבסס על דוגמת התוכנה למסחר במניות שהוזכרה קודם, הדוגמאות הבאות בשפת Java ממחישות את הדרכים הטיפוסיות לביצוע הזרקת תלויות ידנית, והזרקת תלויות המבוצעת על ידי תשתיות (frameworks) ייעודיות. הממשקים הבאים מגדירים את החוזים עבור התנהגות הרכיבים במערכת המסחר במניות: public interface IOnlineBrokerageService { String[] getStockSymbols(); double getAskingPrice(String stockSymbol); double getOfferPrice(String stockSymbol); void putBuyOrder(String stockSymbol, int shares, double bidPrice); void putSellOrder(String stockSymbol, int shares, double offerPrice); } public interface IStockAnalysisService { double getEstimatedValue(String stockSymbol); } public interface IAutomatedStockTrader { void executeTrades(); } הדוגמה הבאה מציגה קוד ללא יישום של הזרקת תלויות: public class VerySimpleStockTraderImpl implements IAutomatedStockTrader { private IStockAnalysisService analysisService = new StockAnalysisServiceImpl(); private IOnlineBrokerageService brokerageService = new NewYorkStockExchangeBrokerageServiceImpl(); public void executeTrades() { for (String stockSymbol : brokerageService.getStockSymbols()) { double askPrice = brokerageService.getAskingPrice(stockSymbol); double estimatedValue = analysisService.getEstimatedValue(stockSymbol); if (askPrice < estimatedValue) { brokerageService.putBuyOrder(stockSymbol, 100, askPrice); } } } } public class MyApplication { public static void main(String[] args) { IAutomatedStockTrader stockTrader = new VerySimpleStockTraderImpl(); stockTrader.executeTrades(); } } המחלקה VerySimpleStockTraderImpl יוצרת מופעים של IStockAnalysisService ו-IOnlineBrokerageService על ידי קישור נוקשה בקוד (hard coding) למחלקות הקונקרטיות שממשות את הממשקים (קריאה ישירה לקונסטרקטורים של המחלקות הקונקרטיות באמצעות המילה השמורה new). ארגון מחדש של הקוד מהדוגמה הקודמת לשימוש בהזרקת תלויות ידנית תיראה כך: public class VerySimpleStockTraderImpl implements IAutomatedStockTrader { private IStockAnalysisService analysisService; private IOnlineBrokerageService brokerageService; //constructor public VerySimpleStockTraderImpl(IStockAnalysisService analysisService, IOnlineBrokerageService brokerageService) { this.analysisService = analysisService; this.brokerageService = brokerageService; } public void executeTrades() { … } } public class MyApplication { public static void main(String[] args) { IStockAnalysisService analysisService = new StockAnalysisServiceImpl(); IOnlineBrokerageService brokerageService = new NewYorkStockExchangeBrokerageServiceImpl(); IAutomatedStockTrader stockTrader = new VerySimpleStockTraderImpl(analysisService, brokerageService); stockTrader.executeTrades(); } } בדוגמה זו הקוד במתודת ה-main שבמחלקה MyApplication ממלא תפקיד של מזריק תלויות. בפונקציה הזאת בוחרים את המימושים הקונקרטיים של התלויות הנדרשות על ידי VerySimpleStockTraderImpl, ואז מספקים את התלויות האלה באמצעות הזרקה לקונסטרקטור (constructor injection). קיימות מספר תשתיות (frameworks) המאפשרות אוטומציה של ניהול תלויות באמצעות delegation. בדרך כלל זה נעשה על ידי שימוש בהגדרות XML או metadata. ארגון מחדש של הקוד מהדוגמה הקודמת לשימוש בהגדרות XML חיצוניות דרך תשתית תוכנה תיראה כך: קובץ קונפיגורציה ב-XML: <contract id="IAutomatedStockTrader"> <implementation>VerySimpleStockTraderImpl</implementation> </contract> <contract id="IStockAnalysisService" singleton="true"> <implementation>StockAnalysisServiceImpl</implementation> </contract> <contract id="IOnlineBrokerageService" singleton="true"> <implementation>NewYorkStockExchangeBrokerageServiceImpl</implementation> </contract> הקוד ב-Java: public class VerySimpleStockTraderImpl implements IAutomatedStockTrader { private IStockAnalysisService analysisService; private IOnlineBrokerageService brokerageService; public VerySimpleStockTraderImpl(IStockAnalysisService analysisService, IOnlineBrokerageService brokerageService) { this.analysisService = analysisService; this.brokerageService = brokerageService; } public void executeTrades() { … } } public class MyApplication { public static void main(String[] args) { IAutomatedStockTrader stockTrader = (IAutomatedStockTrader) DependencyManager.create(IAutomatedStockTrader.class); // here is where the magic happens :) stockTrader.executeTrades(); } } בדוגמה זו משתמשים בשירות להזרקת תלויות בשם DependencyManager, לצורך קבלת מופע של מחלקה המממשת את חוזה ה- IAutomatedStockTrader. ה-DependencyManager מסיק מקובץ הקונפיגורציה (XML), שעליו ליצור מופע של המחלקה VerySimpleStockTraderImpl. על ידי בחינה של הארגומנטים של הקונסטרקטור באמצעות reflection, ה-DependencyManager מסיק גם שלמחלקה VerySimpleStockTraderImpl יש שתי תלויות; לכן הוא יוצר מופעים של IStockAnalysisService ו-IOnlineBrokerageService ומספק תלויות אלה כארגומנטים לקונסטרקטור. מכיוון שקיימות שיטות רבות לממש הזרקת תלויות, רק חלק קטן מהן מוצג בדוגמאות אלה. לכן העברת ניהול התלויות ממודול אחד לאחר ניתנת לביצוע במגוון דרכים. בדיקה של תוכנה לסחר במניות כנגד שירות ברוקרים אמיתי עלולה לגרום לתוצאות הרסניות. ניתן להשתמש בהזרקת תלויות כדי להחליף את המימושים האמיתיים במימושים לצורך בדיקות על מנת לפשט את תהליך בדיקות היחידה. בדוגמה שלמטה, בדיקת היחידה רושמת מימושים חלופיים של IOnlineBrokerageService ו-IStockAnalysisService לצורך ביצוע בדיקות וכדי לבצע ולידציה על ההתנהגות של VerySimpleStockTraderImpl. public class VerySimpleStockBrokerTest { // Simplified "mock" implementation of IOnlineBrokerageService. public static class MockBrokerageService implements IOnlineBrokerageService { public String[] getStockSymbols() { return new String[] {"ACME"}; } public double getAskingPrice(String stockSymbol) { return 100.0; // (just enough to complete the test) } public double getOfferPrice(String stockSymbol) { return 100.0; } public void putBuyOrder(String stockSymbol, int shares, double bidPrice) { Assert.Fail("Should not buy ACME stock!"); } public void putSellOrder(String stockSymbol, int shares, double offerPrice) { // not used in this test. throw new NotImplementedException(); } } public static class MockAnalysisService implements IStockAnalysisService { public double getEstimatedValue(String stockSymbol) { if (stockSymbol.equals("ACME")) return 1.0; return 100.0; } } public void TestVerySimpleStockTraderImpl() { // Direct the DependencyManager to use test implementations. DependencyManager.register( IOnlineBrokerageService.class, MockBrokerageService.class); DependencyManager.register( IStockAnalysisService.class, MockAnalysisService.class); IAutomatedStockTrader stockTrader = (IAutomatedStockTrader) DependencyManager.create(IAutomatedStockTrader.class); stockTrader.executeTrades(); } } Inversion of control Spring Framework פלאג אין Strategy pattern מדיה וקבצים בנושא Dependency injection בוויקישיתוף הסבר על Dependency injection - מתוך מדריך וידאו ל-Spring Framework באתר YouTube (באנגלית)