ארכיון

ארכיון של יוני, 2010

Web Caching Techniques

בפוסט הזה:

  • כמה מילים על cache
  • טכניקות נפוצות לשימוש ב cache ב web:
    – האובייקט Cache
    – האובייקט Application
    – עבודה עם static properties
  • סיכום ועוד כמה מילים

הקדמה – כמה מילים על Cache

קח מספר

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

למה אני כותב את כל זה? כי למספרים (ולרצפים של מספרים) יש תכונה מעניינת: אפשר לשכפל אותם. יום-יום אני מעתיק קבצים מהמחשב לדיסק-און-קי שלי, וזה אפשרי בגלל שקובץ הוא רצף של מספרים, שניתן לשכפל אותו בקלות. התכונה הזו של שכפול מספרים מאפשרת לנו להשתמש (או לבנות) מערכת של Caching. לפני שנגדיר מה זה מערכת של Caching, בואו נבין מה הצורך.

אז ככה: נניח שיש לנו אפליקציית Web (או למעשה כל אפליקציית Server, אבל Web זו דוגמה נפוצה וברורה), ויש לה הרבה משתמשים בו-זמנית. נניח שזו אפליקציה של מדריך טלפון, שבה המשתמש יכול לחפש טלפון לפי שם פרטי, שם משפחה, וישוב (בישראל). אם כך, כל משתמש רואה ב browser שלו את רשימת הישובים.

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

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

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

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

עבודה עם Cache עונה על הצורך של מהירות, ע"י אחסון עותק של הנתונים בזכרון (בזכרון ה RAM של השרת): שליפת נתונים מהזכרון היא מאוד מהירה יחסית לשליפת נתונים מקובץ בדיסק או ממקור חיצוני אחר (כמו מסד נתונים, Web-Service וכו'). מצד שני, הזכרון הזמין בצד השרת הוא מוגבל בגודלו. לכן לא נוכל לאחסן את כל הנתונים שם.

נשמע אותו דבר

בקיצור, Cache (ובעברית "מטמון") זו פעולה של אחסון נתונים בהתקן מסויים, כך ששליפתם מההתקן הזה תהיה מהירה יותר משליפת הנתונים בצורה המקורית. בדוגמה הקודמת, ההתקן הזה היה זכרון ה RAM של השרת. לפעמים מאחסנים את הנתונים בדיסק מהיר יותר, וגם זה יכול להיות cache. לפעמים מאחסנים את הנתונים ב Flash-Disk, וגם זה יכול להיות cache.
בכל מקרה, מנגנון של Caching יכול גם לבדוק את צריכת הזכרון ע"י הנתונים שמאוחסנים בו, ומדי פעם לנקות חלק מהזכרון לפי חוקים מסויימים. יש מגוון רחב של אלגוריתמים לניהול Cache, ולפרטים נוספים מומלץ לקרוא בויקיפדיה.

בפוסט הזה אני אתמקד ב Caching של אפליקציית Web שכתובה ב ASP.NET, אבל בשורה התחתונה זה יכול להיות לכל שירות כמו WCF שהוא self-hosted.

פתרונות Caching קיימים

באפליקציית Web שכתובה ב ASP.NET יש מספר טכניקות ל Caching של מידע גולמי (כלומר לא HTML) בזכרון השרת:

  • האובייקט Cache
  • האובייקט Application
  • משתנים גלובליים

System.Web.Caching.Cache

האובייקט Cache (מתוך System.Web.Caching) מכיל את האפשרויות העשירות ביותר במתודה Insert: לצד ה key וה value (הנתונים עצמם):

  • אפשר לציין שהנתונים תלויים במקור חיצוני (שאפשר לדגום אותו), כמו קובץ או טבלה ב DB
  • אפשר לקבוע שהנתונים ישארו ב cache רק עד תאריך/שעה מסויימים (absolute expiration)
  • אפשר לקבוע שהנתונים ישארו ב cache כל עוד נעשה בהם שימוש בחלון זמן מסויים (sliding expiration)
  • אפשר לקבוע קדימויות של הפריטים שנשמרים ב cache, כך שאם, למשל, צריך להוציא פריט אחד מה cache, ויש שני פריטים "מועמדים להדחה", אז הפריט שישאר ב cache הוא זה עם הקדימות הגבוהה יותר.
  • ואפשר לדעת בדיוק מתי כל פריט יוצא מה cache

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

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

משתנים גלובליים – האובייקט Application

האלטרנטיבה ל Cache הוא משתנים גלובליים. פעם, בתקופת ASP3 ומטה, מה שנקרא Classic ASP, לא היה לנו אובייקט Cache כל כך חכם, וכדי לאחסן נתונים בזכרון היה לנו רק את האובייקט Application, שהיה רק אוסף של key/value pairs. בקיצור, היה רק dictionary, בלי היכולת לחשב צריכת זכרון, קדימויות, תוקף וכו' לפריטים השונים. כנראה כדי להיות תואמים לאחור, מיקרוסופט הוציאו את ASP.NET עם אותו אובייקט Application שהוא אובייקט מטיפוס HttpApplicationState. כדי להשתמש באובייקט Application נרשום קוד בצורה הזו:

public partial class MyPage : System.Web.UI.Page
{
  protected void Page_Load(object sender, EventArgs e)
  {
    // get a value
    int diamond = (int) Application["Wish"];

    // set a value
    Application["Wall"] = "numb";
  }
}

caching בתקופות קדומות

היתרון של האובייקט Application הוא שהפריטים המאוחסנים בו – נשארים שם, אלא אם כן היתה קריאה מפורשת למתודת Remove. אגב, אפשר להגיע לתוצאות דומות אם נשים פריט באובייקט Cache ונציין NotRemovable ב CacheItemPriority.

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

חסרון נוסף הוא המימוש של האובייקט הזה: לפחות לפי הפריימוורק שמותקן אצלי במחשב, המימוש הוא קצת מיושן ובעייתי. יש שם שימוש בטיפוס Hashtable (קצת מיושן, אבל מילא) ויש שם קוד עם נעילות מהסוג של lock(this) וזה, איך לומר במילים עדינות, לא להיט. מי שלא יודע למה זה לא להיט מוזמן לקרוא עוד ב stackoverflow.

משתנים גלובליים – Static Properties

מימוש אחר ל caching באמצעות משתנים גלובליים הוא static properties. אנשים נוטים לשכוח את האופציה הזו, והיא לדעתי הרבה יותר נוחה ופשוטה מהשימוש ב Application (או באלטרנטיבת ה NotRemovable של האובייקט Cache). הרעיון פשוט: בסה"כ כותבים class ומוסיפים לו static properties. הם יהיו נגישים בכל האפליקציה. עם זאת, מכיוון שזו סביבת Multi-Threading, נצטרך לנעול איכשהו את המשתנים האלה. מכיוון שזה פתרון ל cache, הרי שאנחנו מצפים לקריאת הנתונים בתדירות גבוהה ולעדכון הנתונים לעיתים רחוקות. לשם כך בדיוק כבר כתבתי את ה Safe Value Pattern מהפוסט הקודם, וניקח את המימוש של SafeValueSeldomWrites. הקוד יראה כך:

public class MyGlobals
{
  public static SafeValueSeldomWrites<int> Wish =
    new SafeValueSeldomWrites<int>();
  public static SafeValueSeldomWrites<string> Wall =
    new SafeValueSeldomWrites<string>();
}

את הערכים ההתחלתיים נוכל לשים ב Application_Start, הנה קוד (להמחשה בלבד):

void Application_Start(object sender, EventArgs e)
{
  MyGlobals.Wish.Value = 1234;
  MyGlobals.Wall.Value = "numb";
}

וניתן כמובן לקרוא אותם או לשנות את הערך שלהם. למשל:

protected void Page_Load(object sender, EventArgs e)
{
  // get a value
  int diamond = MyGlobals.Wish.Value;

  // set a value
  MyGlobals.Wall.Value = "Vera";
}

שימו לב שהפעם אין צורך להמיר ל int את הערך המוחזר.

יתרונות וחסרונות

הנה טבלה קטנה שמרכזת את התכונות המרכזיות של השיטות שסקרתי כאן. יתרון יחסי מודגש בירוק.

האובייקט Cache האובייקט Application Static Properties
נעילה גלובלית גלובלית רק על הנתון הרלוונטי
casting בשליפת נתונים צריך צריך לא צריך
מתחשב בזכרון השרת כן לא לא
אז מתי להשתמש? מתי שרק אפשר כשממירים אפליקציית ASP3 לאפליקציית דוט נט, וגם זה כשלב ביניים בלבד כשרוצים לשמור ב cache נתונים שנדרשים בתדירות גבוהה, עם צריכת זכרון נמוכה

סיכום

מימושים שונים ל caching אפשר למצוא בהרבה מקומות במערכות ממוחשבות: יש cache ברמת האפליקציה (כמו שכתבתי בפוסט הזה), יש cache לקריאות מהדיסק ברמת מערכת ההפעלה (ב Windows מספיק מודרני זה חלק מובנה במערכת ההפעלה, ובתקופות קדומות יותר השתמשנו ב SmartDrive :-P). אפרופו הארד-דיסק, גם בתוך הקופסה הזו יש cache (בבקר הפנימי שבתוך הדיסק).

בפוסט הזה, שהתחיל להתגלגל כתוצאה מתשובה שכתבתי ב stackoverflow, הצגתי את האפשרויות שיש לנו במסגרת אפליקציית ASP.NET. עם זאת, יש פתרונות Cache יותר מורכבים:

  • יש את memcached שזה שירות אחסון נתונים בזכרון (כאפליקציה נפרדת, עם API משלה).
  • יש אפשרות לאחסן נתונים ב cache מבוזר (distributed cache). מיועד לאפליקציות שמותקנות על מספר שרתים.

בקיצור, גם cache אפליקטיבי יכול להיות מורכב, הכל תלוי במורכבות של האפליקציה עצמה ובעומס עליה.

אגב, בנוסף ל Cache הרגיל מתוך ה Web, בדוט נט 4 כבר הכניסו אובייקט cache שזמין גם ל Console Applications. קוראים לזה MemoryCache, ופרטים נוספים תמצאו ב MSDN. ה MemoryCache הזה ממחיש את הצורך ב Caching שלא רק בקונטקסט של אפליקציות ווביות, ונראה שמיקרוסופט נענו לצורך הזה. יופי מיקרוסופט, עכשיו רק נשאר להיפטר מה IDisposable המזוויע הזה 😀

כרגיל, תכנות נעים ויעיל!

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

Safe Value Pattern

23 יוני, 2010 2 תגובות

בפוסט הזה: קוד קצר ולעניין של משתנה שקוראים אותו ומעדכנים אותו בסביבה שהיא Multi-Threaded.

כידוע, כדי להגן על המשתנה מפני corruption אנחנו צריכים נעילה (כן, אני יודע, יש גם את Interlock, אבל זה לא העניין פה, אל תטרידו אותי עם עובדות…)

אז במקום להצהיר בכל פעם על משתנה ועל האובייקט שנועל אותו, הנה Mini-Pattern שעושה את העבודה:

הגירסה הפשוטה

מנעול פשוט, עבודה עם lock:

namespace Pepperoni.Lib
{
  public class SafeValue<T>
  {
    private readonly object locker = new object();
    private T innerValue;

    public SafeValue()
    {
    }

    public SafeValue(T initialValue)
    {
      innerValue = initialValue;
    }

    public T Value
    {
      get
      {
        lock (locker)
        {
          return innerValue;
        }
      }
      set
      {
        lock (locker)
        {
          innerValue = value;
        }
      }
    }
  }
}

אין הרבה מה לכתוב ב code-review, בסה"כ גם ב getter וגם ב setter נועלים אובייקט פנימי ורק אז מחזירים את הערך (ב getter) או מעדכנים את הערך (ב setter). קצר, פשוט, קריא, עובד (נראה לי שאני מאמץ את זה בראשי תיבות: קפק"ע! :-P).

אלטרנטיבה – הרבה קריאות, מעט עדכונים

אם אנחנו יודעים שיש הרבה יותר קריאות של הערך לעומת עדכונים של הערך, כלומר הרבה יותר שימושים ב getter מאשר ב setter, אז נוכל לייעל את העבודה אם נשתמש באובייקט ReaderWriterLockSlim (לקריאה נוספת – MSDN). הנה הקוד (עודכן לפי ההצעה של חן אגוזי):

using System.Threading;

namespace Pepperoni.Lib
{
  public class SafeValueSeldomWrites<T>
  {
    private T innerValue;
    private readonly ReaderWriterLockSlim locker = new ReaderWriterLockSlim();
   
    public SafeValueSeldomWrites()
    {
    }

    public SafeValueSeldomWrites(T initialValue)
    {
      innerValue = initialValue;
    }

    public T Value
    {
      get
      {
        locker.EnterReadLock();
        try
        {
          T result = innerValue;        
          return result;
        }
        finally
        {
          locker.ExitReadLock();
        }
      }
      set
      {
        locker.EnterWriteLock();
        try
        {
          innerValue = value;
        }
        finally
        {
          locker.ExitWriteLock();
        }
      }
    }
  }
}

גם כאן הקוד יחסית פשוט וקריא, הנעילה מתבצעת עם ה ReaderWriterLockSlim שמוצהר כ private. רק לא לשכוח ש finally תמיד-תמיד מתבצע, גם אם יש return בתוך ה try שלו.

עוד כמה מילים

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

עניין נוסף: במקרים מסויימים אפשר לעבוד עם Interlock, שמאפשר קריאה/כתיבה בסביבה שהיא Multi-Threaded, ללא שימוש ב lock הסטנדרטי. מומלץ לגגל או לקרוא ב MSDN.

אפשר להוריד מכאן את הקוד, למי ש copy-paste גדול עליו 🙂
תכנות נעים!

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

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. במה אתם תבחרו? איפה אתם עומדים בסקאלה הזו?

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

תכנות נעים!

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

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