בפוסט הזה אני אמשיך ב state machine שהצגתי כבר קודם, והפעם אני אצור abstract class שיכול להיות שימושי כאשר רוצים רכיב שרץ ברקע (נניח, רכיב שמציג מניות, רכיב שמאזין לבקשות TCP וכד').
מטבע הדברים, רכיב שכזה רץ על thread משלו, והקריאה למתודת ה Start שלו, ואח"כ גם ל Stop – יכולות להגיע מ threadים שונים, ולכן נצטרך לדאוג לנעילות.
בפשטות
נתחיל מהמקרה הפשוט יותר, בלי התייחסות לענייני multi-threading.
בואו נקפוץ פנימה לקוד ונסביר מה קורה:
public abstract class StartableBase
{
private readonly IStartableStateMachine innerStateMachine;
protected StartableBase()
: this(StartableStatus.Stopped)
{
}
protected StartableBase(StartableStatus initialStatus)
: this(new StartableStateMachine(initialStatus))
{
}
protected StartableBase(IStartableStateMachine innerStateMachine)
{
if (innerStateMachine == null)
throw new ArgumentNullException("innerStateMachine");
this.innerStateMachine = innerStateMachine;
innerStateMachine.OnStart += OnStart;
innerStateMachine.OnStop += OnStop;
}
protected abstract void OnStart();
protected abstract void OnStop();
public void Start()
{
innerStateMachine.Start();
}
public void Stop()
{
innerStateMachine.Stop();
}
public StartableStatus Status
{
get { return innerStateMachine.Status; }
}
}
סקירה קצרה:
יש כאן abstract class, כלומר הכוונה היא שיהיה class שיורש מה class שלנו.
מי שמכיר אותי כבר יודע שאני לא מת על ירושה בקוד, אבל כאן זה מתבקש, כי זה באמת מעודד שימוש חוזר בקוד, ומצד שני די קל להבין מה הולך פה.
בכל מקרה, בקלות אפשר לעקוף ירושה ע"י composition, אבל לא נרחיב על זה כאן. בשביל זה יש גוגל.
בקיצור, ה class הזה חושף לעולם שתי מתודות: Start ו Stop.
בנוסף, ב class יש קונסטרקטורים שבסופו של דבר נסמכים על state machine חיצוני, שבו נמצא כל הקוד למעברים בין המצבים.
התפקיד של ה class הוא להעביר את הקריאות של ה Start וה Stop החיצוניות שלו אל ה state machine הנ"ל. ההתנהגות של ה state machine, עם ה events שלו, מתגלגלת ל class היורש, באמצעות מתודות אבסטרקטיות.
במילים אחרות, כל התפקיד של ה class הזה הוא לעטוף את ההתנהגות של ה state machine ולהפוך אותה ל abstract class. סבבצ'יק.
ניצור class חדש, שיורש מה StartableBase הנ"ל, כדי לקבל את התחושה.
נקרא לו MyCoolService. כי אנחנו גיקים שנותנים דוגמאות של קוּלים. זה למעשה המקסימום coolness שגיק יכול להיות, וזה רק הופך אותו ליותר גיק.
ככה זה, עולם אכזר.
נו, בכל אופן, הקוד:
public class MyCoolService : StartableBase
{
protected override void OnStart()
{
Console.WriteLine("hey, now I should start working!");
}
protected override void OnStop()
{
Console.WriteLine("ok, now I should stop.");
}
}
אכן, קוּל למהדרין. ככה משתמשים:
static void Main(string[] args)
{
var myCoolService = new MyCoolService();
Console.WriteLine("myCoolService.Status = " + myCoolService.Status);
myCoolService.Start();
Console.WriteLine("myCoolService.Status = " + myCoolService.Status);
myCoolService.Stop();
Console.WriteLine("myCoolService.Status = " + myCoolService.Status);
}
וזו התוצאה:
myCoolService.Status = Stopped
hey, now I should start working!
myCoolService.Status = Started
ok, now I should stop.
myCoolService.Status = Stopped
טוב, זה היה רק כדי לקבל תחושה, זה לא באמת קול.
Threads Ahoy!
זה מתחיל להיות מעניין כשעובדים עם יותר מ thread אחד.
אז גם כאן אולי כדאי שנתחיל מ abstract class, שמתבסס על הקודם (בירושה! סלח לי אבי כי חטאתי!), ונקפוץ לקוד:
/// <summary>
/// Marks the implementation as a thread-safe one.
/// Nothing more than that.
/// </summary>
public interface IThreadSafeStateMachine : IStartableStateMachine
{
}
public abstract class ThreadSafeStartableBase : StartableBase
{
protected ThreadSafeStartableBase() :
this(StartableStatus.Stopped)
{
}
protected ThreadSafeStartableBase(StartableStatus initialStatus) :
this(new StartableStateMachineInterlock(initialStatus))
{
}
protected ThreadSafeStartableBase(IThreadSafeStateMachine innerStateMachine)
: base(innerStateMachine)
{
}
}
סה"כ מעטפת ל abstract class שכבר היה לנו, עם תוספות קלות.
כדי להשתמש בכל הסיפור הזה, צריך רק לרשת ולהתחיל לעבוד עם קצת נעילות.
בדוגמה הבאה, יש לנו class שבכל X שניות (או כל TimeSpan שנקבע) מבצע פעולה מסויימת, נניח משהו כמו פינג.
בנוסף, יש לנו Counter שמונה את הפינגים שבוצעו. הקוד:
public class MyThreadSafeService : ThreadSafeStartableBase, IDisposable
{
private readonly TimeSpan period;
private readonly object syncLock;
private readonly Timer timer;
private int counter = 0;
public MyThreadSafeService(TimeSpan period)
: this(period, new object())
{
}
private MyThreadSafeService(TimeSpan period, object syncLock)
: base(new StartableStateMachineSimpleLock(StartableStatus.Stopped, syncLock))
{
this.period = period;
this.syncLock = syncLock;
timer = new Timer(MyTimerCallback);
}
private void MyTimerCallback(object state)
{
int current;
lock (syncLock)
{
counter++;
current = counter;
}
Console.WriteLine("Ping! so far {0} pings", current);
}
public int Counter
{
get
{
lock (syncLock)
{
return counter;
}
}
}
protected override void OnStart()
{
counter = 0;
timer.Change(TimeSpan.Zero, period);
}
protected override void OnStop()
{
timer.Change(TimeSpan.FromMilliseconds(-1), TimeSpan.FromMilliseconds(-1));
}
public void Dispose()
{
Stop();
timer.Dispose();
}
}
הקריאה:
static void Main(string[] args)
{
var period = new TimeSpan(0, 0, 1); // ping every second
var myThreadSafeService = new MyThreadSafeService(period);
myThreadSafeService.Start();
Console.ReadLine();
myThreadSafeService.Stop();
Console.WriteLine("Counter = " + myThreadSafeService.Counter);
myThreadSafeService.Dispose();
}
והתוצאה (אחרי בערך 5 שניות):
======== Thread Safe Example ===========
Ping! so far 1 pings
Ping! so far 2 pings
Ping! so far 3 pings
Ping! so far 4 pings
Ping! so far 5 pings
Counter = 5
=========== End of Example =============
press --enter-- to quit
וואלה עובד.
סיכום
מי שכותב מדי פעם רכיב שפועל ברקע, כנראה מצא את עצמו חוזר על קוד התשתית של "להתחיל משהו, לדאוג לנעילות, לסנכרן סטטוס" וכו'.
בשני הפוסטים האלה הראיתי איך אפשר לכתוב base class שמעודד שימוש חוזר בקוד מהסוג הזה.
היתרון העיקרי הוא שאפשר להפריד בין המנגנון הפנימי של ה state machine וגם, באופן חלקי, לנעילות המתבקשות.
מצד שני, הירושה יכולה לגרום לכאב ראש כשנכנסים לדיבאג (או כשסתם מנסים להבין את זה).
בנוסף, יש לנו לא מעט קבצים לתחזק, רק כדי לכתוב קוד "נכון".
מה המסקנה? לא תמיד יש מסקנה חד משמעית, צריך לנסות ולראות אם זה מתאים לכם.
הערות והארות, או סתם תגובות יתקבלו בברכה. ובכלל, מי שקרא עד הלום – סחתן, כה לחי וישר כוח 🙂
אפשר להוריד את כל הקוד, כמובן.
אם יורשה לו להגיב