IoC Container Explained

בפוסט הזה אני רוצה להסביר את עיקרי הדברים סביב IoC, שזה ראשי תיבות של Inversion of Control. למעשה אני רוצה לספר מעט על התיאוריה ולהרחיב על הפרקטיקה.

בקיצור, אני רוצה להסביר ולהדגים מה זה IoC Container.

הנושא כולו די רחב, ואפשר להתעמק בו עוד ועוד, ועדיין ללמוד דברים חדשים. אני חושב שלמען ההגינות, כדאי מאוד לקרוא מאמרים שמסבירים בצורה טובה את הקונספט של IoC Container ושל Dependency Inversion.

הנה קצת לינקים חיצוניים, שמסבירים (באנגלית, מן הסתם) את הנושא:

  • אחד מהמאמרים המפורסמים ביותר בנושא הוא של Martin Fowler. המאמר נקרא
    "Inversion of Control Containers and the Dependency Injection pattern"
  • בויקיפדיה אפשר למצוא את:

אחרי התיאוריה וההקדמות, בואו ניגש לעניין עצמו.

הפוסט הזה הוא המשך של פוסט קודם: "מנשקים לפנים". בפוסט ההוא, הסברתי את המשמעות של ממשקים, ואת היתרונות של תכנות מול ממשקים.

נרענן מעט את הזכרון. יש לנו מחלקה שנקראת CustomerReminder, והגענו בסופו של תהליך, לקוד הבא:

[sourcecode gutter="false" language="csharp"]
namespace Canberra.InterfacesDemo.Lib
{
public class CustomersReminder
{
public void Remind(ICustomersFetcher customersFetcher, IEmailSender emailSender)
{
foreach (var customer in customersFetcher.GetSleepingCustomers())
{
emailSender.SendRemindMessage(customer.FirstName,
customer.LastName,
customer.EmailAddress);
}
}
}
}
[/sourcecode]

השאלה שנותרה פתוחה היא: "מי אמור לספק instance של מחלקות שמממשות ICustomerFetcher ו IEmailSender?"

אפשרות אחת היא ליצור instance כחלק מהקוד עצמו, שקורא לפונקציה Remind. הקוד יראה כך:

[sourcecode gutter="false" lang="csharp"]
var customersReminder = new CustomersReminder();
customersReminder.Remind(new CustomersFetcher(), new EmailSender());
[/sourcecode]

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

כדי שנוכל לקמפל ולבנות את הפרויקט שבו נמצאת המחלקה CustomerReminder, הפרויקט צריך להכיר את המחלקות המממשות את הממשקים המדוברים.

כלומר, אם כתבנו בפרויקט נפרד, שנקרא לו MyMail, את המחלקה EmailSender, שמממשת את IEmailSender, ונרצה להשתמש בה, אז אנחנו צריכים רפרנס לפרויקט MyMail. הרפרנס יכול להיות בצורה של רפרנס בין פרויקטים באותו solution, והרפרנס יכול להיות ל DLL של MyMail. זה לא משנה לצורך העניין.

מה שמשנה כאן הוא שיש תלות בין הפרויקט שלנו, לרפרנס של מימוש ספציפי של הממשק IEmailSender.

אם נרצה יום אחד להשתמש במימוש אחר לממשק IEmailSender, נצטרך לבנות מחדש את את הפרויקט שלנו.

בואו נבדוק לרגע מה זה אומר:

  • לבנות מחדש פרויקט – Build
  • לבדוק שוב שהכל תקין – QA
  • להפיץ את התוצר לאן שצריך – Deploy

כל זה רק כי רצינו לשנות את המימוש של ממשק מסוים.
זה לא מעט עבודה…

אז מה האלטרנטיבה?

הבעיה העיקרית שלנו היא איך להיפטר מהפעולה new בקוד שלנו.

נרצה להגיע לקוד שבו:

  • אין את הפעולה new
  • אין התייסות בקוד עצמו או ברפרנסים של הפרויקט לאסמבלי שנרצה להשתמש בו

ובמילים אחרות:

הבחירה באסמבלי שיממש את הממשק תהיה בקונפיגורציה.

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

או שלא בא.

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

אז במקום שאנחנו נשבור את הראש כדי להשתמש ברפלקשן לעניין זה – נוכל להשתמש בפריימוורק שאחרים כתבו, ושהם כבר שברו את הראש בשבילנו. לפריימוורק שכזה קוראים IoC Container.

IoC Container

IoC זה ראשי תיבות של Inversion of Control. לצערי IoC זה מושג שקשה עד בלתי אפשרי לתרגם אותו לעברית, ולכן אני אשאר איתו כמו שהוא. IoC Container זה איזשהו אמצעי/כלי/מיכל/קונטיינר/פריימוורק/ספריה לביצוע הפעולות של IoC. באופן כללי, זה אמצעי ליצור instance של מחלקה בלי שיהיה לה רפרנס בפרויקט.

בסביבת דוט נט יש מספר IoC Containers נפוצים. מתוכם אני שמח להזכיר את:

  • Spring.NET
  • Castle Windsor
  • StructureMap
  • Autofac
  • Unity
  • Ninject

וזו רשימה חלקית. מי שרוצה לראות השוואות בין הפריימוורקים מוזמן לגגל או להיכנס ל stackoverflow ולהמשיך משם.

באופן אישי יצא לי לעבוד עם Spring.NET, עם Ninject, עם StructureMap (כיום) ועם Unity (גם כיום; למעשה אנחנו בתהליך של החלפת SM ב Unity).

בואו נחזור לבעיה שלנו ונראה איך משתמשים ב Unity כדי לקבל instance של הממשק IEmailSender.

הדגמה עם Unity

קודם כל, צריך רפרנס מהפרויקט שלנו ל Unity. נוסיף. יש.

עכשיו, לפקודה עצמה. במקום הקוד הזה:

[sourcecode gutter="false" lang="csharp"]
var customersReminder = new CustomersReminder();
customersReminder.Remind(new CustomersFetcher(), new EmailSender());
[/sourcecode]

נרשום כך:

[sourcecode gutter="false" lang="csharp"]
var container = new UnityContainer();
var section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");
section.Configure(container);

var customersReminder = new CustomersReminder();
customersReminder.Remind(new CustomersFetcher(), container.Resolve<IEmailSender>());
[/sourcecode]

יפה. אבל לפני שאנחנו קופצים משמחה, איך תדע ה Unity להחזיר אובייקט כתוצאה מהשורה הזו?

-נצטרך לקנפג אותה כמובן.

נוכל להשתמש בקובץ ה config הסטנדרטי לצורך העניין. בואו נראה דוגמה:

[sourcecode gutter="false" lang="xml"]
<?xml version="1.0"?>
<configuration>
<configSections>
<section
name="unity"
type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,
Microsoft.Practices.Unity.Configuration"
/>
</configSections>
<unity>
<typeAliases>
<typeAlias
alias="IEmailSender"
type="Canberra.InterfacesDemo.Lib.IEmailSender,
Canberra.InterfacesDemo.Lib"
/>
<typeAlias
alias="EmailSender"
type="MyMail.EmailSender, MyMail"
/>
</typeAliases>
<containers>
<container>
<types>
<type type="IEmailSender" mapTo="EmailSender"/>
</types>
</container>
</containers>
</unity>
</configuration>
[/sourcecode]

כלומר, אנחנו מגדירים ל Unity בדיוק באיזה מימוש להשתמש עבור הממשק IEmailSender.

בדוגמה הזו, כנגד הבקשה למימוש של IEmailSender, כלומר בקשת ה Resolve, ה Unity תחזיר לנו instance של המחלקה MyMail.EmailSender, שנמצאת באסמבלי MyMail.

כדי שזה באמת יקרה, קבצי ה dll של MyMail (וכל התלויות שלהם) צריכים להיות באותה תיקיה של ה exe של הפרויקט שלנו (או בתיקיית bin במקרה של web application).

זהו, עכשיו אפשר לקפוץ משמחה.

כדי להנות קצת יותר, במקרה של אפליקציית web, מומלץ לאתחל את ה Unity ב Application_Start שב global.asax, ולהגדיר משתנה סטאטי שתמיד נפנה אליו:

[sourcecode gutter="false" lang="csharp"]
using System;
using System.Configuration;
using Microsoft.Practices.Unity;
using Microsoft.Practices.Unity.Configuration;

namespace Canberra.InterfacesDemo.WebApp
{
public class Global : System.Web.HttpApplication
{
public static UnityContainer Container;

protected void Application_Start(object sender, EventArgs e)
{
Container = new UnityContainer();
var section = (UnityConfigurationSection)ConfigurationManager
.GetSection("unity");
section.Configure(Container);
}
}
}
[/sourcecode]

וכך, הקוד שלנו שמשתמש ב Resolve של Unity יצטמצם באופן הבא:

[sourcecode gutter="false" lang="csharp"]
var customersReminder = new CustomersReminder();
customersReminder.Remind(new CustomersFetcher(),
Global.Container.Resolve<IEmailSender>());
[/sourcecode]

והשמחה רבה 🙂

אחרי ששמחנו ונהנינו, בואו נסכם את התהליך של שימוש ב IoC Container:

  • מוסיפים רפרנס ל IoC Container עצמו
  • במקום new בקוד שלנו, משנים לתחליף שמציע ה IoC Container. בדוגמה שלנו עם Unity זה Global.Container.Resolve<IEmailSender>()
  • מגדירים עבור ה IoC Container מה יהיה המימוש לכל interface שנרצה
  • מוודאים שכל קבצי האסמבלי והתלויות שלהם נמצאים בתיקיית ה bin של הפרויקט ה"ראשי", אחרת מקבלים אקספשן…

עוד כמה מילים על IoC Container

השימוש הבסיסי ביותר ל IoC Container (להלן IoCC) הוא ליצור instance של מחלקה לפי interface, או לפי כל type אחר. הרעיון המרכזי הוא להימנע מ new בקוד.

קונפיגורציה – בקובץ או דרך קוד?

הקונפיגורציה של ה IoCC יכולה להיות בקובץ חיצוני או בקוד עצמו. היתרונות של קובץ קונפיגורציה הם ברורים – לא צריך לבנות מחדש את האפליקציה וכו'. עם זאת, הקונפיגורציה יכולה להיות מעיקה וארוכה, ולכן יש לא מעט אנשים שמעדיפים לקנפג את ה IoC Container בקוד עצמו. למשל, מה שמציע Ninject זה קונפיגורציה רק דרך קוד. זה מאוד נקי, ואפשר להעביר לקונפיגורציה רק מה שצריך באמת.

אתחול של אובייקטים מורכבים

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

IoCC as a Factory

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

ועוד הרחבות

IoCC נותן הרבה מעבר ליצירת אובייקטים פשוטה. אפשר למשל להגדיר שיצירת אובייקט מסויים תהיה יחידה למשך כל חיי האפליקציה, ואז יש לנו סינגלטון דרך ה IoCC. כלומר אין צורך להשתמש ב Singleton Design Pattern, ה IoCC עושה את כל הסיפור הזה בשבילנו.

לסיכום

בפוסט הזה הראיתי איך אפשר להשתמש ב IoC Container (ובקיצור – IoCC).

הרעיון המרכזי – להעביר את הלוגיקה של איתחול של אובייקט מהקוד שלנו אל קובץ קונפיגורציה. במילים אחרות: השימוש ב IoCC מאפשר לנו להימנע מ new בקוד.

למה זה טוב?

-כי אז אפשר לבחור במימוש של ממשקים בלי להיות תלוי בקומפילציה מחדש. כלומר, אנחנו מקטינים את הצימוד ברמת הפרויקטים שלנו.

למה זה לא טוב?

-כי לפעמים זה מורכב מדי למתכנתים מתחילים;

-כי זה לא כל כך פשוט להרכיב את הפאזל הזה: גם לקנפג וגם לוודא שהקבצים יגיעו לתיקיית bin ונגזרותיה… קצת כאב ראש;

-באופן כללי, קצת קשה לעשות back-tracking לקוד ולהבין מה מתבצע בפועל

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

ובשורה התחתונה, חייבים לעבוד ככה?

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

מעבר לזה, IoCC מתווה לנו את הדרך לכתיבת קוד שמחולק כראוי לחלקים שהם ניתנים להפרדה ולשינוי, והופך אותנו ממתכנתים "סתם" למהנדסי תוכנה.

 

הקוד לפוסט הזה נמצא כאן.

הינדוס נעים!

8 Comments

  1. תודה רון על המאמר

    אך לדעתי המאמר קצת קצר מדי [בטח לאור הציפיות…] לדוגמה תוכל להרחיב קצת על פונקציות ה register ?

    שאלה נוספת ,האם מתוכנן מאמר על soa ?

  2. @צבי
    הפוסט הזה מסביר בצורה תכליתית את השימוש ב IoCC. בחרתי שלא להרחיב מעבר לזה, כי לדעתי זה נכנס להגדרות ספציפיות של Unity, וזה מסיט אותי מהעיקר.
    בהמשך אני אפרסם פוסט על DI, ואדגים איך הכל מתחבר: ממשקים, IoCC ו DI. נקווה שזה יקרה בעשור הנוכחי 🙂
    כרגע אני לא מתכנן מאמר על SOA, ובהקשר הזה אני מציע לקרוא את המאמרים של ארנון רותם-גל-עוז:
    הבלוג שלו: http://arnon.me
    מאמר על SOA בשם what is SOA anyway נמצא כאן: http://rgoarchitects.bit.ly/8xLQLi

כתיבת תגובה

האימייל לא יוצג באתר. שדות החובה מסומנים *

This site uses Akismet to reduce spam. Learn how your comment data is processed.