ארכיון

רשומות עם התג ‘State Machine’

Modulus Counter – Code Reuse vs. Micro Class

הערה מקדימה: לפעמים אפשר לכתוב קוד סתם בשביל הכיף.

בפוסט הזה אני אראה קוד שנראה די פשוט, שלא לומר על סף הטריוויאלי, ואח"כ נראה אם הוא מתאים למשימות פרקטיות. לאחר מכן נבדוק האם המחלקה ModCounter שמוצגת כאן היא over-engineering או באמת "חומר טוב" לשימוש חוזר בקוד.

ברמת הקוד, הנושא העיקרי כאן הוא פעולת המודולו, שמסומנת ב C# ע"י הסימן אחוז (%).

אז מה יהיה כאן:

  • דוגמה פשוטה להתחלה
  • מעבר מ inline-code ל class
  • סתם בשביל הכיף – שעון
  • בשביל הפרקטיקה – מוניטור לכשלונות רצופים
  • סיכום – האם הגזמנו עם ה ModCounter הזה?

דוגמה פשוטה להתחלה

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

using System;
using System.Threading;

namespace Groundhog.ConsoleApp
{
  class Program
  {
    static void Main(string[] args)
    {
      Console.WriteLine("started");
      DoSomeLengthyActivity();
      Console.WriteLine("done, press ENTER to quit");
      Console.ReadLine();
    }

    private static void DoSomeLengthyActivity()
    {
      for (int i = 0; i < 100; i++)
      {
        Thread.Sleep(100);
      }
    }
  }
}

המתודה DoSomeLengthyActivity מייצגת פעילות ממושכת בלולאה. כלומר פעילות שכל איטרציה שלה לוקחת די הרבה זמן (נניח 100 מילישניות) ויש לה די הרבה איטרציות כאלו.
החלק המהותי הוא ש DoSomeLengthyActivity היא מתודה ארוכה לביצוע ויש בה לולאה (או כל פירוק אחר להרבה מאוד תתי-משימות).

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

using System;
using System.Threading;

namespace Groundhog.ConsoleApp
{
  class Program
  {
    static void Main(string[] args)
    {
      Console.WriteLine("started");
      DoSomeLengthyActivity();
      Console.WriteLine("done, press ENTER to quit");
      Console.ReadLine();
    }

    private static void DoSomeLengthyActivity()
    {
      for (int i = 0; i < 100; i++)
      {
        Console.WriteLine("still working");
        Thread.Sleep(100);
      }
    }
  }
}

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

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

using System;
using System.Threading;

namespace Groundhog.ConsoleApp
{
  class Program
  {
    static void Main(string[] args)
    {
      Console.WriteLine("started");
      DoSomeLengthyActivity();
      Console.WriteLine("done, press ENTER to quit");
      Console.ReadLine();
    }

    private static void DoSomeLengthyActivity()
    {
      for (int i = 0; i < 100; i++)
      {
        if (i % 10 == 0)
          Console.WriteLine("still working");
        Thread.Sleep(100);
      }
    }
  }
}

הגענו אל האיזון הנכסף: המשתמש מקבל אינדיקציה כל 10 איטרציות, שזה בערך פעם בשניה.
אבל מי שמתעסק עם הקוד הזה לא בהכרח מבין מיידית למה עובדים עם המשתנה i ועושים עליו modulus.
ומעבר לזה, לא ניתן לעבוד עכשיו עם לולאה מסוג אחר, כמו foreach, אלא רק עם לולאת for, כי הלוגיקה של האינדיקציות למשתמש "כבולה" למשתנה הלולאה.

הרשו לי להציג: ModCounter

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

public class ModCounter
{
  private readonly int modBase;
  private int theValue;

  public ModCounter(int modBase) : this (modBase, 0)
  {
  }

  public ModCounter(int modBase, int initialValue)
  {
    if (modBase <= 1)
      throw new ArgumentOutOfRangeException("modBase", "value must be greater than 1");
    this.modBase = modBase;
    theValue = initialValue % modBase;
  }

  public int Increase()
  {
    var increased = theValue + 1;
    if (increased == modBase)
      increased = 0;
    theValue = increased;
    return increased;
  }

  public bool IsZero
  {
    get
    {
      return theValue == 0;
    }
  }

  public int Value
  {
    get
    {
      return theValue;
    }
  }
}

לפני סקירת הקוד, בואו נראה את השימוש הפשוט ב class הזה

static void Main(string[] args)
{
  Console.WriteLine("ModCounter example");
  ModCounter myCounter = new ModCounter(3);
  Console.WriteLine(myCounter.Value); // value is 0
  myCounter.Increase();
  Console.WriteLine(myCounter.Value); // value is 1
  myCounter.Increase();
  Console.WriteLine(myCounter.Value); // value is 2
  myCounter.Increase();
  Console.WriteLine(myCounter.Value); // value is 0
  Console.WriteLine(myCounter.IsZero); // True
  Console.WriteLine("done, press ENTER to quit");
  Console.ReadLine();
}

והפלט:

ModCounter example
0
1
2
0
True
done, press ENTER to quit

סקירה קצרה על הקוד

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

עכשיו נכניס את ה ModCounter לתוכנית המקורית ונראה איך הקוד משתנה

private static void DoSomeLengthyActivity()
{
  var myCounter = new ModCounter(10);
  for (int i = 0; i < 100; i++)
  {
    myCounter.Increase();
    if (myCounter.IsZero)
      Console.WriteLine("still working");
    Thread.Sleep(100);
  }
}

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

public class ModCounter
{
  public event Action OnZero;

  private void InvokeOnZero()
  {
    Action action = OnZero;
    if (action != null) action();
  }

  // .. code ..

  public int Increase()
  {
    var increased = theValue + 1;
    var shouldResetToZero = increased == modBase;
    if (shouldResetToZero)
    {
      theValue = 0;
      InvokeOnZero();
    }
    else
      theValue = increased;
    return theValue;
  }
}

ועכשיו נוכל לכתוב את הקוד המקורי בצורה קצת יותר קריאה:

private static void DoSomeLengthyActivity()
{
  var myCounter = new ModCounter(10);
  myCounter.OnZero += () => Console.WriteLine("still working");
  for (int i = 0; i < 100; i++)
  {
    myCounter.Increase();
    Thread.Sleep(100);
  }
}

לסיכום שלב א' – העברנו את הלוגיקה של האינדיקציות ממשתנה הלולאה אל מחלקה חיצונית בשם ModCounter.
בתוך ה ModCounter, כל קריאה למתודה Increase מקדמת את הערך הפנימי ב 1, ומתבצעת בדיקה: אם הוא הגיע לגבול העליון אז הוא מתאפס והאירוע OnZero מופעל.

בשביל הכיף – ClockCounter

נשחק קצת עם ה ModCounter שלנו וניצור מחלקה חדשה בשם ClockCounter, שמדמה שעון, שאפשר לתת לו ערך התחלתי ולקדם אותו בשניה מתי שרוצים.

נכון, המלעיזים יגידו שאפשר לעשות את זה עם TimeSpan, אבל זה רק בשביל הכיף, אז המלעיזים יכולים להלעיז חופשי, אני בכלל עם אוזניות.

הקוד? הנה הקוד:

public class ClockCounter
{
  private readonly ModCounter seconds;
  private readonly ModCounter minutes;
  private readonly ModCounter hours;

  public ClockCounter() : this(0, 0, 0)
  {
  }

  public ClockCounter(TimeSpan initialTime) :
    this(initialTime.Hours, initialTime.Minutes, initialTime.Seconds)
  {
  }

  public ClockCounter(int hours, int minutes, int seconds)
  {
    this.seconds = new ModCounter(60, seconds);
    this.minutes = new ModCounter(60, minutes);
    this.hours = new ModCounter(24, hours);
    this.seconds.OnZero += () => this.minutes.Increase();
    this.minutes.OnZero += () => this.hours.Increase();
  }

  public void IncreaseSecond()
  {
    this.seconds.Increase();
  }

  public TimeSpan Time
  {
    get
    {
      return new TimeSpan(hours.Value, minutes.Value, seconds.Value);
    }
  }
}

והשימוש בקוד:

static void Main(string[] args)
{
  Console.WriteLine("started ClockCounter example");
  var clockCounter = new ClockCounter(22, 59, 59);
  Console.WriteLine(clockCounter.Time);
  clockCounter.IncreaseSecond();
  Console.WriteLine(clockCounter.Time);

  Console.WriteLine("now let's try again");
  clockCounter = new ClockCounter(23, 59, 59);
  Console.WriteLine(clockCounter.Time);
  clockCounter.IncreaseSecond();
  Console.WriteLine(clockCounter.Time);

  Console.WriteLine("done, press ENTER to quit");
  Console.ReadLine();
}

והפלט, כצפוי:

started ClockCounter example
22:59:59
23:00:00
now let's try again
23:59:59
00:00:00
done, press ENTER to quit

חביב? -בהחלט. כמעט כל הקוד הוא ב wiring-up בקונסטרקטור, וזה מגניב. שימושי? -לא ממש. 😛

בשביל הפרקטיקה – HealthMonitor

עוד Health Monitor

בואו ננסה בכל זאת לעשות משהו פרקטי – מוניטור על פעילות תקינה של מערכת.

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

ה Web-Service הזה צריך לבצע הרבה פעולות לכל request, ובסופן יש לו מדד אחד ויחיד: הצליח או נכשל. "הצליח" משמעותו שהמערכת קלטה בהצלחה את ה request וטיפלה בו במלואו, ו"נכשל" משמעותו שמשהו השתבש בדרך, ואין מה לעשות כרגע. ההסתברות לשיבוש היא לא זניחה, ונרצה לעשות קצת live-monitoring על המערכת שלנו. הלוגיקה של ה monitoring (בעברית זה "ניטור") היא פשוטה: אחרי X כשלונות רצופים – להודיע משהו למישהו במייל. במידה שהיתה הצלחה אחרי כשלון מספר 1, 2, או כל ערך קטן מ X – מונה הכשלונות מתאפס ומתחילים מחדש.

לשם כך נצטרך רק להוסיף את המתודה Reset ב ModCounter שלנו (שפשוט תאפס את המונה הפנימי שלו) ולעטוף אותו ב class חדש שנקרא לו HealthMonitor, שנראה כך:

public class HealthMonitor
{
  public int MaxConsequtiveFailures { get; private set; }
  private readonly ModCounter modCounter;

  public HealthMonitor(int maxConsequtiveFailures) :
    this(new ModCounter(maxConsequtiveFailures))
  {
    MaxConsequtiveFailures = maxConsequtiveFailures;
  }

  private HealthMonitor(ModCounter modCounter)
  {
    this.modCounter = modCounter;
    this.modCounter.OnZero += InvokeReachedMaxConsequentFailures;
  }

  public void Pass()
  {
    modCounter.Reset();
  }

  public void Fail()
  {
    modCounter.Increase();
  }

  public event Action ReachedMaxConsequentFailures;

  private void InvokeReachedMaxConsequentFailures()
  {
    Action failures = ReachedMaxConsequentFailures;
    if (failures != null) failures();
  }
}

השימוש ב HealthMonitor מאפשר להפריד בין הלוגיקה של דיווח על הצלחה/כשלון (שמגיע מתוך ה Web-Service) לבין הטיפול ברצף של כשלונות (שיכול להיות בכל מקום אחר באפליקציה), מתוך הנחה שיש instance יחיד של ה HealthMonitor באפליקציה שלנו. אפשר לממש את זה בכל מיני צורות (סינגלטון או סתם משתנה סטאטי באחת המחלקות של האפליקציה). לצורך הדוגמה, ניקח את המקרה הפשוט של משתנה סטאטי זמין לכל, ונאתחל אותו ב Global.asax. הנה הקוד:

public class Global : System.Web.HttpApplication
{
  public static HealthMonitor HealthMonitor;

  protected void Application_Start(object sender, EventArgs e)
  {
    HealthMonitor = new HealthMonitor(3);
    HealthMonitor.ReachedMaxConsequentFailures += SendMailToAppAdmin;
  }

  void SendMailToAppAdmin()
  {
    // code to send a warning mail
    // to the application administrator
  }

  // .. code ..
}

ואז הקוד שלנו ב Web-Service יראה כך:

public class MyService : System.Web.Services.WebService
{
  [WebMethod]
  public void DoSomething()
  {
    bool allOk = false;
    // do a lot of stuff,
    // set allOk to false if needed
    // …
    if (allOk)
      Global.HealthMonitor.Pass();
    else
      Global.HealthMonitor.Fail();
  }
}

סקירה קצרה

  • הקוד של ה Web-Service לא מטפל ברצף של כשלונות, הוא רק מודיע על הצלחה או כשלון.
  • הקוד הוא לדוגמה בלבד, ואין כאן התייחסות להיבט של Multi-Threading (שאכן מתקיים בסביבת Web).
  • הקוד המוצג כאן מגדיל את הצימוד בין ה Web-Service לבין המחלקה Global (שהיא ה Global.asax.cs) שלנו. וזה, במילים עדינות, לא להיט.

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

סיכום

בפוסט הזה הצגתי מחלקה סופר-פשוטה בשם ModCounter. כל תפקידה הוא קידום מונה פנימי שישמור תוצאה שהיא מודולו בסיס מסויים. תוך כדי הפוסט הרחבתי אותה קצת פה ושם, והיא עדיין נשארה פשוטה. למעשה ה ModCounter הוא state-machine (ואני, מה לעשות, די מחבב state-machines). הראיתי שימוש פרקטי ב ModCounter ע"י מחלקה שעוטפת אותו, שמבצעת monitoring למערכת. האם הייתי משתמש ב ModCounter בפועל? התשובה היא כן, המחלקה HealthMonitor היא לא המצאה בשביל הפוסט, היא באמת נכתבה למערכת קיימת (עם שינויים קלים).

גם זה איזון

ועדיין נשארת אצלי השאלה, האם באמת כדאי להכניס לוגיקה יחסית פשוטה לתוך class. איפה עובר הגבול בין good-practice לבין over-engineering ("הינדוס יתר"). היתרון הגדול של לוגיקה פשוטה בתוך מחלקה משל עצמה הוא שאפשר לבדוק אותה בקלות. מרגע שהיא נבדקה ונמצאה תקינה (עד כמה שאפשר) – אפשר להשתמש במחלקה הזו בכל הזדמנות, ואז גם קבלנו שימוש חוזר בקוד. יתרה מכך, זה קוד שאנחנו סומכים עליו שהוא תקין.

החסרון הוא מהצד של over-engineering – פעולות כל כך פשוטות יכולות להישאר כקוד שחוזר על עצמו. אין טעם ליצור micro-class, כי אז הקוד הופך להיות לא מספיק קריא. וזה עוד לפני שדברנו על להוסיף רפרנס לאסמבלי/פרוייקט חיצוני.

אז חזרנו לדרך האמצע, לאיזון הנכסף, בין קוד קריא לקוד שקל לתחזק אותו. בין שימוש חוזר לבין over-engineering. במה אתם תבחרו? איפה אתם עומדים בסקאלה הזו?

האמת, בלי קשר לבחירה שלכם – כמו שכתבתי בתחילת הפוסט, לפעמים זה כיף לכתוב קוד גם אם הוא לא הכי שימושי :-).

תכנות נעים!

וכמובן – כל הקוד זמין כאן.

קטגוריות:תכנות תגיות:, , ,

Startable – State Machine part 2

16 אפריל, 2010 תגובה אחת

בפוסט הזה אני אמשיך ב 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 וגם, באופן חלקי, לנעילות המתבקשות.
מצד שני, הירושה יכולה לגרום לכאב ראש כשנכנסים לדיבאג (או כשסתם מנסים להבין את זה).
בנוסף, יש לנו לא מעט קבצים לתחזק, רק כדי לכתוב קוד "נכון".
מה המסקנה? לא תמיד יש מסקנה חד משמעית, צריך לנסות ולראות אם זה מתאים לכם.
הערות והארות, או סתם תגובות יתקבלו בברכה. ובכלל, מי שקרא עד הלום – סחתן, כה לחי וישר כוח 🙂

אפשר להוריד את כל הקוד, כמובן.

קטגוריות:תכנות תגיות:, ,

Startable – State Machine part 1

בפוסט הזה:

  • קצת על State Machine
  • Startable State Machine
  • מתי משתמשים
  • מימוש רגיל
  • הפרדת interface מהמימוש
  • מימוש עם נעילות
  • הדגמה פשוטה

בפוסט הבא אני אראה איך אפשר להשתמש ב state machine הזה (בירושה, למשל)

קצת על State Machine

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

קבוצת המצבים הזו יכולה להיות מתוארת כמכונת מצבים (סופית), או באנגלית Finite State Machine, ובקיצור FSM. המעבר ממצב למצב נקרא transition ובד"כ הצורך לעבור ממצב אחד לאחר נובע מקלט כזה או אחר, או הודעה פנימית. אפשר לקרוא עוד בויקיפדיה (עברית ואנגלית) כדי להעמיק.

Startable State Machine

תרשו לי להציג מכונת מצבים פשוטה ונחמדה. אולי פשוטה מדי, אבל אני מקווה שאצליח לשכנע שהיא נחוצה ומעודדת שימוש חוזר בקוד (שזו מטרה נאה לכל הדעות). אני קורא למצבים פשוטים כאלה "mini patterns". לא ממש design pattern, אבל בהחלט מעודד שימוש חוזר בקוד.

מדובר בסה"כ בשני מצבים אפשריים: Started ו Stopped. או בעברית: מופעל/מופסק.

נדמיין שני לחצנים: לחצן Start ולחצן Stop (כמו Play ו Stop במכשיר DVD, למשל)

אם המצב הנוכחי הוא Stopped, ולחצנו על Start – עוברים למצב Started. כל לחיצה נוספת על Start לא משנה את המצב.

וכנ"ל להיפך: אם המצב הנוכחי הוא Started, ולחצנו על Stop – עוברים למצב Stopped. כל לחיצה נוספת על Stop לא משנה את המצב.

לצורך העניין, מצב הפתיחה הוא Stopped.

התרשים הבא יכול לתאר את זה:

Startable State Machine - chart

מתי משתמשים?

השימוש העיקרי שלי ב Startable הוא כשבתוך אפליקציה קיימת, אני צריך איזשהו שירות שיהיה פעיל ברקע כל הזמן, או פעיל לפעמים. למשל: שרת UDP שמאזין להודעות נכנסות בפורט מסויים, סטרימר ששולח Streaming Video למחשב אחר, רכיב שבודק כל X שניות מה הסטטוס של רכיב אחר, וכו' וכו'. לכל רכיב שכזה יש בד"כ שני מצבים: "פעיל" או "לא פעיל". קלאסי ל Startable.

מימוש רגיל

המימוש הוא די פשוט:

namespace Groundhog.Lib
{
    public enum StartableStatus
    {
        Started,
        Stopped
    }

    public class StartableStateMachine
    {
        private StartableStatus status;

        public event Action OnStart;
        public event Action OnStop;

        public StartableStateMachine() : this(StartableStatus.Stopped)
        {
        }

        public StartableStateMachine(StartableStatus initialStatus)
        {
            status = initialStatus;
        }

        private void InvokeAction(Action e)
        {
            if (null == e)
                return;
            e();
        }

        private void InvokeOnStop()
        {
            InvokeAction(OnStop);
        }

        private void InvokeOnStart()
        {
            InvokeAction(OnStart);
        }

        public void Start()
        {
            if (status == StartableStatus.Started)
                return;

            // it was "stopped"
            // change to "started"            
            status = StartableStatus.Started;
            // and raise the proper event
            InvokeOnStart();
        }

        public void Stop()
        {
            if (status == StartableStatus.Stopped)
                return;

            // it was "started"
            // change to "stopped"            
            status = StartableStatus.Stopped;
            // and raise the proper event
            InvokeOnStop();
        }

        public StartableStatus Status
        {
            get
            {
                return status;
            }
        }        
    }
}

סקירת קוד קצרה:

  • יש כאן enum על הסטטוס: Started או Stopped
  • אפשר לקבל את הסטטוס הנוכחי באמצעות המאפיין Status (דה!)
  • בנוסף יש שתי מתודות: Start ו Stop שהן ה"לב" של ה state machine שלנו
  • וכדי להודיע לעולם שהסטטוס השתנה, יש שני אירועים: OnStart ו OnStop.

מה אין כאן?
הקוד הוא לא thread-safe, מיד נגיע לזה.

הפרדת interface מהמימוש

ממוש, תפריד לי את ה interface מהמימוש, טוב?
הנה, מותק:

namespace Groundhog.Lib
{
  public interface IStartableStateMachine
  {
    event Action OnStart;
    event Action OnStop;
    void Start();
    StartableStatus Status { get; }
    void Stop();
  }
}

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

מימוש עם נעילות

כדי שהקוד יהיה thread-safe, אפשר:

  • להשתמש בנעילות רגילות (או באבסטרקציה שהצעתי בפוסט אחר)
  • לעבוד עם Interlock, שמהווה מנגנון יותר light-weight לנעילות מהסוג שצריך כאן

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

הדגמה פשוטה

הנה קוד של Console Application שנותן לנו קצת להרגיש את כל מה שהיה לנו עד עכשיו.

using Groundhog.Lib;
namespace Groundhog.ConsoleApp
{
  class Program
  {
    static void Main(string[] args)
    {
      IStartableStateMachine ssm = new StartableStateMachine();
      ssm.OnStart += MyOnStart;
      ssm.OnStop += MyOnStop;

      Console.WriteLine("===========================");
      Console.WriteLine("simple flow: start and stop");
      Console.WriteLine("===========================");
      Console.WriteLine(ssm.Status);
      ssm.Start();
      Console.WriteLine(ssm.Status);
      ssm.Stop();
      Console.WriteLine(ssm.Status);

      Console.WriteLine();
      Console.WriteLine();
      Console.WriteLine("========================================");
      Console.WriteLine("multiple starts and then multiple stops,");
      Console.WriteLine("should invoke each event just once");
      Console.WriteLine("========================================");
      ssm.Start();
      ssm.Start();
      ssm.Start();
      ssm.Start();
      Console.WriteLine(ssm.Status);
      ssm.Stop();
      ssm.Stop();
      ssm.Stop();
      ssm.Stop();
      ssm.Stop();
      Console.WriteLine(ssm.Status);

      Console.WriteLine();
      Console.WriteLine();
      Console.WriteLine("===================");
      Console.WriteLine("press enter to quit");
      Console.WriteLine("===================");
      Console.ReadLine();
    }

    static void MyOnStart()
    {
      Console.WriteLine("hey, it started!");
    }

    static void MyOnStop()
    {
      Console.WriteLine("dude, this thing stopped");
    }
  }
}

והפלט יהיה:

===========================
simple flow: start and stop
===========================
Stopped
hey, it started!
Started
dude, this thing stopped
Stopped

========================================
multiple starts and then multiple stops,
should invoke each event just once
========================================
hey, it started!
Started
dude, this thing stopped
Stopped

===================
press enter to quit
===================

כפי שניתן לראות, גם אחרי מספר קריאות עוקבות ל start, הסטטוס נשאר started, וכנ"ל גם לגבי קריאות למתודה stop. כלומר יש לנו state machine בדיוק כמו שרצינו.

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

תכנות נעים!

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

קטגוריות:תכנות תגיות:,
Quantcast