עושים סדר בתלויות עם Dependency Injection

Dependency Injectionהוא נושא נפוץ ומדובר למדי, אך בה באותה העת – גם נושא שלא מובן כהלכה. ליאור בר-און עושה קצת סדר ומסביר למה, איך ומאיפה הכל התחיל.

cc-by partido.marianistas.org

לא משנה מה יספרו לכם, המניע העיקרי לשימוש ב-DI) Dependency Injection) הוא בדיקות-יחידה (נושא שכוסה בהרחבה בפוסט קודם). מי שנכנס לנושא ה DI חזק (״התשתית שלנו היא Spring״) – עלול לשכוח מהיכן הכל התחיל.

אני יודע שחלק גדול מכם לא רואה בעניין של בדיקות-יחידה. אולי ניסיתם בעבר וזה לא עבד יפה. למי שלא “חי” בדיקות-יחידה, העניין הזה עשוי להראות תמוה: ״אמרתם שבדיקות-יחידה יחסכו לנו זמן. האם זה נראה לכם נורמלי לעבור לשימוש בתשתית חדשה רק בכדי להצליח ולבצע בדיקות יחידה?? – בדיקות היחידה הללו הן ממש זנב שמכשכש בכלב״ (ואם לא הבנתם: הכלב הוא אתם).

מבחוץ, ניתן לעתים להבין ולהסיק ש”בדיקות-היחידה הפכו למטרה החשובה ביותר, חשובה יותר משחרור המוצר”. מי שהתנסה בבדיקות יחידה מוצלחות מבין שהקושי משתלם: בדיקות יחידה מביאות עמן קוד מודולרי ונקי יותר, גמישות גבוהה לבצע שינויים בקוד (מתאים לאג׳ייל), תיעוד טוב ויכולת של מתכנתים להיכנס בקלות יחסית לקוד לא-שלהם (גם מתאים לאג׳ייל). כמה מאמץ הייתם מוכנים, או צריכים, להשקיע על מנת להשיג את כל אלו? כנראה שהרבה. מי שמוכן להשקיע מאמץ רב בבדיקות יחידה – כנראה שעושה את החשבון הנ”ל.

מה DI בעצם עושה?

DI הוא נושא נפוץ ומדובר למדי, אך בה באותה העת – גם נושא שלא מובן כהלכה ע”י רבים. התוצאה: באזז, מיתוסים ובלבול.

הגדרה מקוצרת: DI מספק דרך חלופית ליצירת אובייקטים שלא דורשת שימוש במילה השמורה new.

כאשר אנו כותבים קוד ורוצים שיהיה “מודולרי ובלתי תלוי”, ממשקים (interfaces) הם הבסיס לעבודה. אפילו אם נגדיר ממשקים טובים שמגדירים חוזה ברור ויציב – עדיין קיימת לנו תלות במחלקה הקונקרטית: בשלב יצירת האובייקט.

דמיינו את מבנה האובייקטים הבא:

HtmlOptimizer – המחלקה העיקרית שלנו, המבצעת שיפורים לדף HTML קיים. היא משתמש ב…
IImageCompressor – ממשק (interface) שמקבל תמונה (IO Stream) ומחזיר תמונה “קטנה יותר”.
JpegCompressor – מימוש ספציפי לממשק ה IImageComressor, אנו מניחים שיש מקום למימושים אחרים.

HtmlOptimizer לא יודע מי נותן לו את שירות ה IImageCompressor. הממשק הוגדר נפלא.
אבל מישהו, מישהו עדיין צריך לקרוא ל:

IImageCompressor compressor = new JpegCompressor();

ולהכיר את המחלקה הקונקרטית.

DI היה פותר אותנו באופן (כמעט) פלאי מהשורה הזו – ומביא אותנו לדרגה גבוהה יותר של חוסר-תלות.

רגע של ספקנות

האם באמת זה כל הסיפור? כל העניין של DI הוא לחסוך לנו שורת קוד פה ושם? בפועל, ל-DI יש מספר שימושים שונים:

  • שימוש אפשרי ל DI הוא להחליט רק בזמן ריצה באיזה מימוש של IImageCompressor להשתמש. מה שנקרא “Late Binding”. הנושא הוא קרקע פורייה לדיון אקדמי / ארכיטקטוני – אך לעתים רחוקות מגיע לכדי שימוש יעיל בפועל. כמה פעמים הקוד שלכם “יחליט” בזמן ריצה להשתמש במחלקה אחרת (במידה ולא מדובר בפולימורפיזם פשוט – שם DI בסיסי לא יספיק)?
    – אם זו בחירה של הלקוח – אפשר ליצור Factory פשוט לפתור את הבעיה. הכניסה ל-DI היא overhead שלא יצדיק את שורות הקוד הבודדות שתחסכו.
    – אם מדובר ב Plug-in Architecture (כמו Eclipse או Visual Studio) אזי יש עניינים של קוד שנארז בנפרד – עניינים ש-DI לא מטפל בהם. כנראה שתזדקקו למשהו כמו OSGi.
  • תמריץ אחר ל-DI הוא שמירה אדוקה יותר של חוסר-תלויות בקוד. לכסות את המיל האחרון. כפי שציינתי קודם לכן, השימוש הנכון בממשקים (Interface) מספק את רוב הערך שניתן להשיג בתחום חוסר-התלות. דקדקנות מעבר לכך, היא בעלת תמורה קטנה יותר. לא נתקלתי בהרבה ישראלים שהיו הולכים לשם.
  • תאוריה מסוימת טוענת ששימוש ב-DI “יטמיע מודולריות” במערכת ויאפשר לעשות שימוש חוזר בקוד – שלא תוכנן מלכתחילה. הניסיון שלי הראה שבדיקות-יחידה הם אמצעי אכיפה יעיל בהרבה למודולריות של המערכת.
  • סיבה נוספת לשימוש ב-DI הוא על מנת לשלוט ולעקוב בתלויות במערכת. DI הוא לא קסם, והיכן שהוא, במחלקה בקוד או בקובץ XML, תהיה רשימה כל התלויות שה-DI מספק. עצם ריכוז כל התלויות במקום אחד מאפשרת לעקוב אחריהן ולוודא שהן תואמות לארכיטקטורה / הגדרות המערכות שהוסכמו. “ניהול תלויות” היא פרקטיקה ארכיטקטונית ידועה. שימוש יתר ב-DI (לצורך זה) יאריך את רשימת התלויות ויהפוך אותה לבלתי שמישה.
  • בדיקות-יחידה. אם אתם עושים אותן (בצורה נכונה) מהר מאוד תגלו שתלויות בין מחלקות “לוגיות” למחלקות “אינטגרציה” (פעולות IO, קריאה למערכות מרוחקות או DB, ציור UI וכו’) – יוצרות את כל העבודה הסיזיפית בכתיבת בדיקות-יחידה. אתם יכולים לעשות דוקטורט ב-EasyMock (או NMock או Mockito) ולעבור לכתיבת Mock Objects בשלושת-רבעי משרה, או שאתם יכולים להשתמש תכופות ב-DI ולשמור את המערכת שלכם Testable ביתר קלות. לא משנה מה יספרו לכם, המניע העיקרי לשימוש ב-DI הוא בדיקות-יחידה.

ההיסטוריה של ה DI/IoC. מקור: picocontainer.codehaus.org

מה DI בעצם עושה (המשך)

DI הוא מקרה פרטי של רעיון שנקרא Inverse Of Control / IoC. הרעיון של IoC אומר שבמקום שמחלקה (או מודול) ינהלו לבד את התלויות שלהם (על-ידי יצירה של Instances) – מישהו אחר, “מומחה בתחום”, יעשה זאת – כך השליטה על התלויות מתהפכת: במקום שהמחלקה מנהלת את התלויות שלה – מישהו אחר מנהל אותם, מה שנקרא Separation of Concerns. זהו עקרון ארכיטקטוני ידוע שאומר שיש לשאוף לכל שכל מודול קוד יבצע דבר אחד, יטפל ב-concern אחד. כשיש כמה עניינים (concerns) לטפל בהם – יש להפריד אותם למודולים שונים. העקרון טוב למודולים כפי שהוא טוב למחלקות או פונקציות.

הגרסה הבסיסית ביותר לרעיון ה-IoC הוא ה-Factory Pattern – מחלקה שכל מומחיותה הוא לייצר מחלקה קונקרטית לא ידועה. Factory יכול להיות יעיל פה ושם (עבור פולימורפיזם), אך אם ננסה לייצר Factory עבור כל מחלקה שאנו רוצים ליצור (אולי אנו רוצים לשלוח בזמן הבדיקות Mock Object) – זו תהיה עבודה רבה.

שלב הבא בהתפתחות של IoC (שאינו עדיין DI), הוא ה Service Locator. יוצרים מחלקה (ServiceLocator), גלויה לכל (public static, global או מה שנכון בשפה שאתם עובדים בה) בעלת 2 מתודות:

  • (getInstance(String key – מחזירה אובייקט (instance) המתאים למשימה (key). בדוגמה שלנו key יכול היה להיות “image.compressor”. יצירה של אובייקט ע”פ שם המחלקה שלו – היא פעולה פשוטה בשפות תכנות רבות. לרוב שורת קוד אחת. בג’אווה: (Class.forName(className.
  • (register(String className, String key – רישום הצמדים “key” מול “שם מחלקה”.

זהו. העלמנו את ה-new. אפשר לסגור את הפוסט?

כמובן שמישהו עדיין אמור “להיות המומחה” ולומר ל-Service Locator איזו מחלקה מתאימה לאיזו משימה (key). במימושי ה-Service Locator שאני ראיתי זה יכול להיות קובץ קונפיגורציה מרכזי או המחלקה הקונקרטית, המספקת את השירות: JpegCompressor נרשם כ-IImageCompressor ואם יותר מ 2 נרשמו על אותו key, מפעילים מספר כללים בכדי לבצע את הבחירה.

DI vs. Service Locator

ההבדל המרכזי בין Service Locator לבין DI, הוא כיוון הבקשה: ב-Service Locator צרכן (HttpOprimizer) פונה ל-Service Locator ומבקש את השירות. ב-DI הצרכן רק מגדיר בקונסטקטור למה הוא זקוק והערכים בקונסטרקטור מתמלאים בערך. לרוב יש קובץ קונפיגורציה או מחלקה (“קונפיגורציה בקוד”) שמגדירה מהו האובייקט הקונקרטי ש”יוזרק” ולמי.

קיימות מספר ספריות (לעתים נקרא Framework או אפילו Container) נפוצות של DI. רובן מוסיפות כמה יכולות נוספות:

  • היכולת לנהל את הקונפיגורציה של ה DI אזורית (לרכיב מערכת, ולא גלובלית) – כך שהתחזוקה תהיה יותר פשוטה.
  • היכולת להחזיק “דלתא” של קונפיגורציה עבור בדיקות-יחידה (כלומר – לא צריך לכתוב הכל מחדש).
  • הגדרת מחלקה כ Singleton או שליטה במספר ה instances שייווצרו בהינף יד.

כיצד ה DI ממומש? יש כמה דרכים שונות. אף אחת לא תפיל אתכם מהכיסא – ולכן אדלג על הסבר זה. מה שחשוב להבין הוא שהניהול של הקשרים בין המחלקות נעשה במקום אחר, שאיננו המחלקה שצורכת את הקשר. מי שייצור את אובייקט a ויישלח אותו כפרמטר ליצירה של אובייקט b – הוא ה DI Framework.

השימוש ב DI

בעבר, (בעקבות ההצלחה של Spring Framework) היה מקובל לנהל את הקונפיגורציה של ה DI בקבצי XML ארוכים. Spring אמנם הייתה הציגה אלטרנטיבה הרבה יותר יעילה מכתיבה של המון Factories במערכת – אך קובצי ה XML היו לה לרועץ. אחד הדברים המרגיזים היה הצורך לקרוא את קובץ ה XML, במקביל לקוד, בכדי להבין את הקוד.

מאז (אולי בעקבות JSR330, Guice) מקובל תחביר מינימליסטי יותר. כלומר: ספריית ה DI “תנחש” ברוב הפעמים למה התכוונתם. לדוגמה: יש לכם ממשק IA ורק מחלקה אחת קונקרטית A שמממשת אותה במערכת? ה-DI יוכל לנחש לבד שמחלקה שמצפה ל IA תקבל מופע של A. השימוש בקוד לכתיבת קונפיגורציה מספק יכולת אימות טובה יותר מקובצי XML – כך שבזמן קומפילציה יש בדיקה שכל שמות המחלקות שציינתם (או חתימות הפונקציות) – אכן שם.

אנחנו צוות מקצועי, אנשים וותיקים, אבל אף פעם לא נזקקנו ל DI. איך אתה מסביר את זה?

ניחוש שלי: לא ניסיתם אף פעם לעשות בדיקות-יחידה ברצינות – המניע היעקרי להכנסת DI לשימוש.

יש לנו DI, ואנו לא עושים בדיקות יחידה. מה זה אומר?

שאתם כנראה משתמשים בו למשהו אחר. נתקלתי פעם במוצר שעשה המון DI, אבל אף אחד לא ידע להסביר למה. לא היו שם בדיקות-יחידה מעולם. ״ככה כותבים כאן קוד״ היה ההסבר הכי משכנע ששמעתי. “ככה אנחנו שומרים על מודולריות” היה טיעון אחר חסר-בסיס, הקוד שם היה האמא-של-הספגטי. אני זוכר שכתבתי ״new״ על מנת לייצר בקוד כמה POJOs – ועוררתי מהומה. המפתחים (וגם ראש צוות אחד) התקשו להסכים ששימוש ב “new” יכול להיות נכון ברגע שיש DI. כשעובדים ב DI (בצורה נבונה) לא אמורים לבצע DI על כל תלות – רק על תלויות שהוגדרו כבעייתיות, מרכזיות וכו’.

זה קצת עצוב, אבל זו לא פעם ראשונה שאני נתקל באנשים שמשתמשים (בצורה כבדה) בתשתית מבלי להבין אותה מספיק טוב, שימוש שאולי עושה יותר נזק מתועלת.

מילות סיכום

מה שהיה לי חשוב בפוסט זה (כמו בכמה פוסטים דומים) הוא לנסות להסיר את שכבת ה”באזז” הכפולה-והנוצצת של המושג “Dependency Injection” ולהתבונן בה קצת יותר בגובה העיניים, להבין מיהי ומה באמת היא עושה.

DI הוא רעיון – לא צריך “framework” על מנת “לעשות DI”. הנה עכשיו אנחנו מנסים קצת DI בקוד ג’אווהסקריפט – ללא כל ספריה. אני חושב שבהחלט כדאי לנסות ולהטמיע רעיונות (DI, NoSQL, ועוד), בשלב ראשון, ללא Framework. דרך זו מסייעת להבין את “הפואנטה” ברעיון – ללא המיסוך והבלבול שמוסיפים ה-Frameworks. אח”כ, במידה ותאמצו Framework ספציפי, תוכלו להשתמש בו בצורה הרבה יותר יעילה ונכונה.

כמובן שיש עוד כמה דברים שכדאי לדעת על DI, כמו שכדאי להמנע מ-Inject לשדות מסוג private – גם כשהתשתית מאפשרת (ה-DI סותר את חוקי ההכמסה של השפה), או ששימוש ב-DI לרוב יעיל יותר בתפרים בין מודולים (Seams) ולא לשימוש פנימי בתוך המודול וכו’. כל זאת אשמור לפעם אחרת.

.

הפוסט פורסם במקור בבלוג Software Archiblog

Avatar

ליאור בר-און

ליאור בר-און הוא Chief Architect בחברת סטארטאפ ישראלית גדולה.

הגב

הגב ראשון!

avatar
Photo and Image Files
 
 
 
Audio and Video Files
 
 
 
Other File Types
 
 
 

* היי, אנחנו אוהבים תגובות!
תיקונים, תגובות קוטלות וכמובן תגובות מפרגנות - בכיף.
חופש הביטוי הוא ערך עליון, אבל לא נוכל להשלים עם תגובות שכוללות הסתה, הוצאת דיבה, תגובות שכוללות מידע המפר את תנאי השימוש של Geektime, תגובות שחורגות מהטעם הטוב ותגובות שהן בניגוד לדין. תגובות כאלו יימחקו מייד.

wpDiscuz

תגיות לכתבה: