הבנת מחזור החיים של הזרקת תלות: Singleton, Scoped, ו-Transient עם דוגמאות מפורטות

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

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

  1. סינגלטון
  2. מוגדרת
  3. זמנית

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

הגדרות

תנו לי להתחיל בהגדרות:

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

מתי להשתמש

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

מהו בקשת לקוח? במילים פשוטות מאוד, תוכל לשקול אותה כשיחת API/REST המגיעה ליישום שלך על ידי לחיצות על הכפתור של המשתמש כדי לקבל את התגובה.

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

דוגמה

ראשית, ניצור ממשקים/שירותים ומחלקות:

C#

 

	// אנו מצהירים על 3 שירותים כפי שמתואר למעלה
		Public interface ISingleton
		Public interface ITransient 
		Public interface IScoped 

עכשיו נכתוב את המימוש עבור כל ממשק/שירות שנוצר לעיל. ננסה להבין את המושג על ידי ניסיון לעדכן את המשתנה callMeSingleton, callMeTransient ו-callMeScoped.

  • מימוש קלאס סינגלטון:
C#

 

class SingletonImplementation: ISingleton
{
	var callMeSingleton = ""

	// מימושים נוספים
	public SetSingleton(string value)
	{
		callMeSingleton = value;
	}
	// מימושים נוספים
}

  • מימוש קלאס טרנזיאנט:
C#

 

class TransientImplementation: ITransient 
{
	var callMeTransient = ""
	
	// מימושים נוספים
	public SetTransient(string value)
	{
		callMeTransient = value;
	}
	// מימושים נוספים
}

  • מימוש קלאס סקופד:
C#

 

class ScopedImplementation: IScoped 
{
	var callMeScoped = ""
			
	// מימושים נוספים
	public SetScoped(string value)
	{
		callMeScoped = value;
	}
	// מימושים נוספים		
}

נרשמת (ConfigureServices) עם DI (התלות בהזנה) כדי להחליט על מחזור החיים של כל מופע של השירות:

C#

 

services.AddSingleton<ISingleton, SingletonImplementation>();
services.AddTransient<ITransient , TransientImplementation>();
services.AddScoped<IScoped , ScopedImplementation>();

בואו נשתמש/נקרא לשירותים האלו מתוך 3 כיתות שונות (ClassA, ClassB, ו-ClassC) כדי להבין את מחזור החיים של כל שירות:

  • ClassA:
C#

 

public class ClassA
{
	private ISingleton _singleton;
	//בונה כדי ליצור 3 שירותים שונים שיצרנו
	public ClassA(ISingleton singleton,
                  ITransient _transient,
                  IScoped _scoped)
	{
		_singleton = singleton;
	}
		
	public void UpdateSingletonFromClassA()
	{
		_singleton.SetSingleton("I am from ClassA");
	} 
	
  	public void UpdateTransientFromClassA()
    {
		_transient.SetTransient("I am from ClassA");
	} 
	
  	public void UpdateScopedFromClassA()
	{
		_scoped.SetScoped("I am from ClassA");
	}

	// יישום אחר 
}

  • ClassB:
C#

 

public class ClassB
{
	private ISingleton _singleton;
	//בונה כדי ליצור 3 שירותים שונים שיצרנו
	public ClassB(ISingleton singleton,
                  ITransient _transient,
                  IScoped _scoped)
	{
		_singleton = singleton;
	}
		
	public void UpdateSingletonFromClassB()
	{
		_singleton.SetSingleton("I am from ClassB");
	} 
	
  	public void UpdateTransientFromClassB()
    {
		_transient.SetTransient("I am from ClassB");
	} 
	
  	public void UpdateScopedFromClassB()
	{
		_scoped.SetScoped("I am from ClassB");
	}

	// יישום אחר 
}

  • ClassC:
C#

 

public class ClassC
{
	private ISingleton _singleton;
	//בונה כדי ליצור 3 שירותים שונים שיצרנו
	public ClassC(ISingleton singleton,
                  ITransient _transient,
                  IScoped _scoped)
	{
		_singleton = singleton;
	}
		
	public void UpdateSingletonFromClassC()
	{
		_singleton.SetSingleton("I am from ClassC");
	} 
	
  	public void UpdateTransientFromClassC()
    {
		_transient.SetTransient("I am from ClassC");
	} 
	
  	public void UpdateScopedFromClassC()
	{
		_scoped.SetScoped("I am from ClassC");
	}

	// יישום אחר 
}

ניתוח

בואו ננתח את התוצאות וההתנהגות של כל מחזור חיים אחד אחד מתוך היישום הנ"ל:

סינגלטון

כל הכיתות (ClassA, ClassB, ו-ClassC) ישתמשו באותו מופע יחיד של כיתת SingletonImplementation לאורך כל מחזור החיים של היישום. זה אומר שהתכונות, השדות והפעולות של כיתת SingletonImplementation יהיו משותפים בין המופעים המשמשים בכל הכיתות הקוראות. כל עדכון לתכונות או שדות ידרוס שינויים קודמים.

לדוגמה, בקוד לעיל, ClassA, ClassB ו־ClassC משתמשים כולם בשירות SingletonImplementation כמופע יחיד ומשתמשים ב־SetSingleton כדי לעדכן את המשתנה callMeSingleton. במקרה זה, יהיה לערך של המשתנה callMeSingleton ערך יחיד עבור כל הבקשות שמנסות לגשת למאפיין זה. המחלקה שמגשימה לו גישה אחרונה תתעלם מערך המשתנה callMeSingleton.

  • ClassA – תהיה לו אותו מופע כמו שאר המחלקות עבור השירות TransientImplementation.
  • ClassB – תהיה לו אותו מופע כמו שאר המחלקות עבור השירות TransientImplementation.
  • ClassC – תהיה לו אותו מופע כמו שאר המחלקות עבור השירות TransientImplementation.

ClassA, ClassB ו־ClassC מעדכנים את אותו מופע של המחלקה SingletonImplementation, שיכול לדרוס את ערך המשתנה callMeSingleton. לכן, יש להיות זהירים בעת הגדרה או עדכון מאפיינים במימוש שירות ייחודי.

שירותים בודדים מועתקים בסיום היישום (כלומר, באיחוד היישום מחדש).

Transient

כל הכיתות (ClassA, ClassB, וClassC) ישתמשו במופעים שלהן האינדיבידואליים של הכיתה TransientImplementation. זה אומר שאם כיתה אחת תקרא למאפיינים, שדות או פעולות של הכיתה TransientImplementation, היא תעדכן או תשתלט רק על הערכים של המופע האינדיבידואלי שלה. עדכונים למאפיינים או שדות לא נשתפים בין מופעים אחרים של TransientImplementation.

תרשו להבין:

  • ClassA – יהיה לה מופע של שירות של TransientImplementation.
  • ClassB – יהיה לה מופע של שירות של TransientImplementation.
  • ClassC – יהיה לה מופע של שירות של TransientImplementation

נניח שיש לכם ClassD שמקריא שירות זמני ממופעים של ClassA, ClassB, וClassC. במקרה זה, כל מופע של כיתה יתייחס כמופע נפרד/שונה ולכל כיתה תהיה הערכה משלה של callMeTransient. קראו את ההערות המודליות למטה עבור ClassD:

C#

 

public ClassD
{
	// מימולציה אחרת
		
    // השורה הבאה תעדק את הערך של callMeTransient ל "אני מקבצ 'ClassA'" רק עבור המקבץ של ClassA.
    // וזה לא יישנה על ידי הקשרים הבאים מ Class B או קבוצת B.
	ClassA.UpdateTransientFromClassA(); 		
       
    // השורה הבאה תעדק את הערך של callMeTransient ל "אני מקבצ 'ClassB'" רק עבור המקבץ של ClassB.
    // והיא לא תשנה את הערך לקבוצות A או תשנה אותו על ידי הקשרים הבאים מ Class C.
	ClassB.UpdateTransientFromClassB(); 
    
    // השורה הבאה תעדק את הערך של callMeTransient ל "אני מקבצ 'ClassC'" רק עבור המקבץ של ClassC.
    // והיא לא תשנה את הערך עבור קבוצות A ו B או תשנה אותו על ידי הקשרים הבאים מ קבוצות אחרות.
    ClassC.UpdateTransientFromClassC(); 

	// מימולציה אחרת
}

שירותים ניתנים ישרים נסגרים בסוף כל בקשה. השימוש בשירותים ניתנים ישרים מוערך כשבעצם צריך התנהגות חסרת מצב בתוך הבקשה.

סקופ

כל הקבוצות (ClassA, ClassB, וClassC) ישתמשו במיקסימום אחד של המימולציה ScopedImplementation לכל בקשה. זה אומר שקשורים בתוך המימולציה ScopedImplementation יתרחשו במקבץ יחיד בתוך הסקוף של הבקשה. כל העדכונים של המאפיינים/השדות/הפעלות על ScopedImplementation יתפשטו בין הקבוצות האחרות.

בואו נבין:

  • ClassA – יהיה לה מקבץ של שירות של TransientImplementation.
  • ClassB – תהיה לו אותה מופע של שירות של TransientImplementation כמו ClassA.
  • ClassC – תהיה לו אותה מופע של שירות של TransientImplementation כמו ClassA ו־ClassB.

נניח שיש לך ClassD שקורא לשירות התחום מ־ClassA, ClassB, ו־ClassC. במקרה זה, לכל מחלקה תהיה מופע יחיד של מחלקת ScopedImplementation. קרא את ההערות הפנימיות עבור ClassD למטה.

C#

 

public class ClassD
{
  // אחר מימוש
 
  // הקוד מ below יעדד את הערך של callMeScoped ל "אני מקבוצת A" עבור המיקסיס של קבוצת A
  // אך בגלל מחזור החיים הסקופי, הוא מחזיק את המיקסיס היחיד של ScopedImplementation
  // אז הוא יכול להשתקף על ידי קריאה הבאה מ קבוצת B או קבוצת C
	ClassA.UpdateScopedFromClassA();  
  
  // הקוד מ below יעדד את הערך של callMeScoped ל "אני מקבוצת B" עבור המיקסיס היחיד של ScopedImplementation 
  // והוא ישתקף את הערך של callMeScoped למיקסיס של קבוצת A גם כן. 
	ClassB.UpdateScopedFromClassB();
  
  // עכשיו אם קבוצת A תבצע איתור על מיקסיס הסקופי,
  // הוא ישתמש בתכונות/ערכים החדשים המשתקפים שנשתקפו על ידי קבוצת B.
	
  // הקוד מ below יעדד את הערך של callMeScoped ל "אני מקבוצת C"
  // והוא ישתקף את הערך של callMeScoped עבור המיקסיסים של קבוצת A ו קבוצת B גם כן.
	ClassC.UpdateScopedFromClassC(); 
  // אז אם קבוצת B או קבוצת A תבצע איתור על מיקסיס הסקופי, הן ישתמשו בתכונות/ערכים החדשים המשתקפים שנשתקפו על ידי קבוצת C.

    // אחר מימוש
}

שירותים סקופיים מסוףים בסוף כל בקשה. השתמשות בסקופ כשאתה רוצה התנהגות ללא מצב בין בקשות בודדות.

שעת טריביה

חיי השירות יכולים להיעדר על ידי שירות הור בזמן ההתחילה שלו. מבולש? תנסי להסביר:

בואו ניקח את אותו דוגמה מהכיתות לעיל ונאתחל את השירותים החולפים והמותאמים מSingletonImplementation (שהוא סינגלטון) כמו למטה. זה יאתחל את השירותים ITransient ו-IScoped וישנה את מחזור החיים שלהם למחזור חיים של סינגלטון כמו שירות האב. במקרה הזה האפליקציה שלך לא תכלול שירותים חולפים או מותאמים (בהנחה שיש לך רק את שלושת השירותים שהשתמשנו בדוגמאות שלנו).

קרא את השורות בקוד למטה:

C#

 

public class SingletonImplementation: ISingleton
{
	// בנאי להוספת אתחול השירותים.
	private readonly ITransient _transient 
	private readonly IScoped _scoped 
		
	SingletonImplementation(ITransient transient, IScoped scoped)
	{
		_transient = transient;  
        // עכשיו _transient יתנהג כשירות סינגלטון ללא קשר לאיך שנרשם כחולף
		_scoped = scoped;
        // עכשיו scoped יתנהג כשירות סינגלטון ללא קשר לאיך שנרשם כמותאם
	}
    var callMeSingleton = ""
		
	// יישום אחר
}

סיכום

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

Source:
https://dzone.com/articles/understanding-the-dependency-injection-lifecycle