אהלן לכם!
"פוסט הבית" (הפוסט המומלץ ביותר לקריאה) הוא UDP Messaging – The SOLID Way, שעוסק הרבה יותר ב OOP מאשר ב UDP. עוד אפשר (וכדאי) לקרוא כאן על Multi-Threading, על Design-Patterns וגם על log4net כמובן. 'סתכלו בעננצ'יק משמאל ותראו הכל.
קריאה מהנה! -רון

Toolbox: A Decent Text Editor

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

ההמלצה הראשונה שלי היא עורך טקסט נורמלי. משתמשי Windows מכירים את ה Notepad (סליחה, "פנקס הרשימות"). זה בהחלט עורך טקסט, אבל לא מהמשובחים (למרות שבענייני BiDi הוא מעולה). אין לו טאבים, למשל. והוא גם לא יודע להציג XML עם צבעים (Syntax highlighting) כמו הויז'ואל סטודיו.

למה בכלל צריך עורך טקסט? האמת, לא חייבים, הרי יש ויז'ואל סטודיו. אבל הוא כל כך כבד ומגושם, שעד שהוא עולה לוקח הרבה זמן. מה גם שההתקנה שלו לא טריויאלית. אז אם רק צריך לעדכן איזה web.config על המחשב (ואולי אפילו על שרת בדיקות, שאין עליו בכלל VS) – אנחנו נרצה עורך טקסט, ואם אפשר כזה שמגיע עם פונקציונאליות מתקדמת כמו:

  • טאבים (או כל פטנט אחר שמאפשר לצפות במספר קבצים במקביל בצורה נוחה)
  • צביעת טקסט – Syntax highlighting
  • סימון השורה הפעילה
  • חיפוש מתקדם של טקסט בתוך הקובץ/הקבצים הפעילים/בתיקיה מסויימת
  • וכו'

והכי חשוב – טעינה מהירה (של העורך עצמו ושל הקובץ שרוצים לערוך).

יש די הרבה עורכי טקסט טובים:

  • Notepad++
  • Notepad2
  • UltraEdit
  • TextPad
  • EditPad
  • EditPlus

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

אני אישית בוחר ב Notepad++ בתור העורך "שלי". לא שעשיתי סקירה מקיפה, פשוט למדתי לעבוד איתו, והוא מספק את הסחורה. יש לו גם פיצ'ר חביב שבקליק ימני על קובץ יש לי אפשרות לערוך איתו את הקובץ:

קליק ימני על קובץ מאפשר לערוך אותו עם NPP

עוד יתרון ב NPP (שמו השני של Notepad++) הוא מערכת plug-ins מובנית, שמאפשרת להרחיב את הפונקציונליות שלו. כך למשל אפשר להתקין plug-in שיהיה hex-editor, ויש גם plug-in להשוואה בין קבצים.
מהנסיון הלא מועט שלי עם NPP יקירי, יש לי רק תלונה אחת: יש לו בעיה ב find/replace. לא משהו ברור, אבל יש חריקות. לא נורא, הוא עדיין הבחירה שלי. את הסריקות אפשר לעשות עם Advanced Find and Replace – תוכנה יעודית וסופר-מהירה לעניין זה.
אה כן, זה freeware לגמרי, open-source ברשיון GPL. בקיצור, זה אחלה (וזה גדול).

נו, הנה לינק לדף הבית שלהם. חלאס, מה הלחץ?!

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

Tip: Just Run It

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

אבל Visual Studio 2010, כברירת מחדל, מחייב אותי לשמור את הפרוייקט בתיקיה, ורק אז אני אוכל להתחיל לעבוד. זה יכול להיות קצת מציק, כי אני רק רוצה לבדוק משהו בקטנה, לא להרים פרויקט פורמלי עם קבצים ותיקיית bin\debug.

כאן באה לעזרתנו הגדרה גלובלית ב Visual Studio 2010: מתפריט Tools בוחרים ב Options, ובחלון שנפתח בוחרים ב Projects and Solutions, ושם יש את האופציה הבאה:
Save new projects when created
כפי שרואים בתמונה כאן, האופציה הזו מסומנת (checked) כברירת מחדל:

אפשר להסיר את הסימון מהאופציה הזו, ואז חלק מהפרויקטים שתבחרו (כולל Console Application) – יפתחו מבלי שנצטרך לבחור תיקיה לשמירה וכו'. נוכל גם להריץ, לדבג והכל. בסיום העבודה (סגירת הויז'ואל או סגירת ה solution) נקבל את הדיאלוג הבא:

אם נבחר באופציה Save – נוכל, כמובן, לשמור את ה solution שלנו.

אבל אם נבחר באופציה Discard – הפרויקט הנ"ל ימחק לחלוטין, כולל תיקיות bin\debug! יש!!
(למעשה כל הפרויקט נשמר ב <current-user>\AppData\Local\Temporary Projects ומשם הוא נמחק)

נייס! :-)

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

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 המזוויע הזה :-D

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

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

Safe Value Pattern

בפוסט הזה: קוד קצר ולעניין של משתנה שקוראים אותו ומעדכנים אותו בסביבה שהיא 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();
        T result = innerValue;
        locker.ExitReadLock();
        return result;
      }
      set
      {
        locker.EnterWriteLock();
        innerValue = value;
        locker.ExitWriteLock();
      }
    }
  }
}

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

עוד כמה מילים

אם רוצים, אז אפשר לעשות כאן אבסטרקציה לשני המימושים (ע"י 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 בקונסטרקטור, וזה מגניב. שימושי? -לא ממש. :-P

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

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

תכנות נעים!

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

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

UDP Messaging – The SOLID Way

הפוסט הזה הוא לא על UDP. כלומר, כן, הוא בהחלט על UDP. אבל הוא גם, ובעיקר, על כתיבת קוד נכונה, על OOP, על Dependency Injection, על Refactoring, ועל Design. וגם, מה המחיר של כתיבת קוד "נכונה", ואולי זה לא תמיד משתלם?

בפוסט הזה אני אציג UDP Listener די פשוט, שמבוסס על המחלקה UdpClient שבאה עם הדוט נט.

בהמשך אני אציג מחלקות נוספות שמתבססות על ה UDP Listener הזה. למשל: UDP Echo Server, וכן רכיב Forward, או "גב אל גב".

וכאמור, תוך כדי הפוסט אנסה להמחיש חלק מעקרונות תכנות שנקראים SOLID, עקרונות שמנחים אותנו לכתוב ולתכנן רכיבי תוכנה בצורה טובה. טובה מאוד. ואולי טובה מדי? ;-)

נתחיל מההתחלה: מה זה UDP בכלל, ולמה זה עוזר לנו?

כמה מילים על UDP

תקשורת בין מחשבים נעשית כיום בשתי דרכים עיקריות: TCP ו UDP.

תקשורת ב UDP היא כזו ששולחת חבילות מידע בצורה שהיא lossy: חבילות יכולות להגיע ליעדן ויכולות גם ליפול איפשהו. זה שימושי למשל בהזרמת אודיו/וידאו, כי שם אם איזשהו פריים "נפל", אז אפשר לעבור לפריים הבא, ושלום על ישראל. גם ב log4net ניתן לראות שימוש ב UDP, כפי שכתבתי בפוסטים קודמים. ראשי התיבות של UDP הן User Datagram Protocol. אפשר לומר, בתרגום חופשי, ש Datagram זו חבילת מידע שעוברת בפרוטוקול הזה.

ניכנס קצת יותר לפרטים. כל תקשורת ב UDP יוצאת ממחשב A דרך פורט X ומגיעה אל מחשב B, אל פורט Y. לפעמים A ו B הם אותו המחשב. בד"כ פורט היציאה (X) הוא רנדומלי, אבל הוא קיים. התמונה להמחשה בלבד:

UDP Message sent from A:X to B:Y

הודעת UDP שנשלחת ממחשב A ומפורט X למחשב B לפורט Y.

הצד שמקבל את חבילת המידע ב UDP יודע גם מאיפה היא יצאה. כלומר, יחד עם חבילת המידע יש כמה מאפיינים נוספים כמו הכתובת של השולח (A) ופורט היציאה (X)

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

My UDP Listener

ניגש לקוד עצמו. מה שיש לנו, כנקודת מוצא, זה class שיורש מ Startable Base שהוא Thread Safe. את ה Startable הזה הכרנו בפוסט קודם, ואני אזכיר שזה base-class שאחראי על ניהול start/stop.

בנוסף יש לנו אירוע שמתרחש כאשר הגיעה הודעת UDP. ה delegate נראה כך:

public delegate void OnDatagramReceived(byte[] bytes, IPEndPoint fromThisEndPoint);

קדימה לקוד, זה נראה ככה:

using System;
using System.Net;
using System.Net.Sockets;
using Groundhog.Lib.MiniPatterns.Startable.ThreadSafe;

namespace Groundhog.Lib.Net.Udp
{
  public class Listener : ThreadSafeStartableBase, IListener
  {
    private readonly IPEndPoint localAddress;
    private readonly object syncLock = new object();
    private UdpClient udpClient;

    public Listener(int localPort)
      : this(new IPEndPoint(IPAddress.Any, localPort))
    {
    }

    private Listener(IPEndPoint localAddress) : base()
    {
      if (localAddress == null) throw new ArgumentNullException("localAddress");
      this.localAddress = localAddress;
    }

    protected override void OnStart()
    {
      lock (syncLock)
      {
        udpClient = new UdpClient(localAddress);
        BeginReceive();
      }
    }

    private void BeginReceive()
    {
      udpClient.BeginReceive(ReceiveCallback, udpClient);
    }

    private void ReceiveCallback(IAsyncResult ar)
    {
      var uc = (UdpClient) ar.AsyncState;
      IPEndPoint remoteEndPoint = null;
      try
      {
        var bytes = uc.EndReceive(ar, ref remoteEndPoint);
        InvokeDatagramReceived(bytes, remoteEndPoint);
      }
      catch(ObjectDisposedException)
      {
        // this one occures right after we invoke Close method
        System.Diagnostics.Debug.WriteLine("swallowed the exception here");
      }
      lock (syncLock)
      {
        if (Status == StartableStatus.Started)
          BeginReceive();
      }
    }

    protected override void OnStop()
    {
      lock (syncLock)
      {
        udpClient.Close();
      }
    }

    public event OnDatagramReceived DatagramReceived;

    private void InvokeDatagramReceived(byte[] bytes, IPEndPoint fromThisEndPoint)
    {
      OnDatagramReceived received = DatagramReceived;
      if (received != null) received(bytes, fromThisEndPoint);
    }
  }
}

הסבר:

הקריאה ל OnStart יוצרת מופע חדש של UdpClient, שזו מחלקה דוט נטית.
מיד לאחר מכן, יש קריאה ל BeginReceive, שזו מתודה אסינכרונית (כלומר היא תתבצע על ת'רד אחר).
הכל עטוף ב lock, כי אולי, תוך כדי ביצוע תהליך ה Start, מישהו יקרא למתודת Stop, ועלולות להתקבל תוצאות בעייתיות.
הקריאה ל Stop גורמת לכך שה Socket עצמו יסגר.

חדי העין שביניכם (שהרי רובנו גיקים עם משקפיים) בוודאי שמו לב שבתוך המתודה ReceiveCallback יש "בליעה" של Exception.
למה זה?
שאלה מצויינת: מסתבר שיש באג (מה, במיקרוסופט?? באג??) שגורם לקריאה ל Callback הנ"ל גם אחרי הקריאה ל Close. לביל גייטס פתרונים. הנה תיעוד מ Microsoft Connect. אז אולי אפשר להימנע מה try/catch, אבל אז מתחילים להסתבך עם דגלים ועם נעילות עליהם (שהרי הקוד אמור לרוץ על יותר מת'רד אחד). לכן נראה לי הגיוני יותר להשאיר את ה try/catch הידוע לשימצה ופשוט לחיות עם זה.

Start it up

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

כדי להאזין להודעות UDP שמתקבלות בפורט 9777 (וזה סתם מספר שרירותי לצורך העניין) במחשב המקומי, ניצור לנו Console Application ונכתוב כך:

using System;
using System.Net;

namespace Groundhog.ConsoleApp
{
  class Program
  {
    static void Main(string[] args)
    {
      Console.WriteLine("starting up…");
      var listener = new Lib.Net.Udp.Listener(9777);
      listener.DatagramReceived += OnReceived;
      listener.Start();
      Console.WriteLine("started, press ENTER to quit");
      Console.ReadLine();
      listener.Stop();
    }

    private static void OnReceived(byte[] bytes, IPEndPoint remoteEndPoint)
    {
      Console.WriteLine("Get {0} bytes from {1}", bytes.Length, remoteEndPoint);
    }
  }
}

לבדיקה, איך נשמע?

בדיקות.. אבל איך?

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

בדיקת אינטגרציה בעידן אחר

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

כן, מסתבר ש Unit Testing זה לא תרופת פלא ולא קסם לכל דבר. ואגב, גם TDD טהור זה לא כזה גליק (ועל כך בפעם אחרת).

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

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

  1. אנחנו כותבים את הקוד ל UdpSender
  2. מישהו אחר כתב את הקוד ל UdpSender, או שיש ממש תוכנת מדף מוכנה לעניין זה.

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

אז נלך על אופציה 2. ובכן, גם כאן אפשר להתפלפל ולנסות להגיע לתוכנת מדף (ואני בטוח שיש כזו), אבל אני דווקא בחרתי לקחת code-snippet בשפת Ruby. למה Ruby? כי הקוד שם כל כך קצר (למרות שהסינטקס, איך לומר בעדינות, קצת מוזר), וגם כי נחמד להכיר. (עוד על רובי בויקיפדיה)

הקוד ששולח את ההודעה "hello" ל UdpListener שלנו נראה ככה ברובי:

require 'socket'
s = UDPSocket.new
s.send("hello", 0, 'localhost', 9777)

סבבצ'יק, ועכשיו, אם נריץ את כל הסיפור נקבל ב Console Application שלנו את הפלט הבא:

starting up…
started, press ENTER to quit
Get 5 bytes from 127.0.0.1:52086

וכדי להיות בטוחים יותר שאכן חמשת הבייטים שהתקבלו הם לא שטות שרירותית, נמיר אותם לטקסט:

private static void OnReceived(byte[] bytes, IPEndPoint remoteEndPoint)
{
  var text = System.Text.Encoding.ASCII.GetString(bytes);
  Console.WriteLine("Get '{0}' from {1}", text, remoteEndPoint);
}

ואכן:

starting up…
started, press ENTER to quit
Get 'hello' from 127.0.0.1:59855

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

בניית UDP Echo Server

אחד היישומים הפשוטים של ה UDP Listener שלנו הוא UDP Echo Server, שזה רכיב שרץ ומאזין להודעות UDP על פורט מסויים, וכל הודעה שמגיעה – שולח בחזרה אל המקור.
בשביל מה זה טוב? בעיקר כדי לבדוק תקשורת UDP (נניח, כדי לוודא שה FireWall מאפשר העברת הודעות UDP בין מחשבים, וכו'). מכיוון שזה כל כך פשוט, לא התאפקתי וכתבתי את הגירסה שלי לעניין.

קודם כל, הוצאתי interface מתוך ה UdpListener:

public interface IListener : IStartable
{
  event OnDatagramReceived DatagramReceived;
}

ואח"כ כתבתי את ה EchoServer כך:

using System;
using System.Net;
using System.Net.Sockets;
using Groundhog.Lib.MiniPatterns.Startable;

namespace Groundhog.Lib.Net.Udp
{
  public class EchoServer : IStartable
  {
    private readonly IListener listener;
    private readonly UdpClient sender = new UdpClient();

    public EchoServer(int port) : this(new Listener(port))
    {
    }

    public EchoServer(IListener listener)
    {
      if (listener == null) throw new ArgumentNullException("listener");
      this.listener = listener;
      listener.DatagramReceived += DatagramReceived;
    }

    private void DatagramReceived(byte[] bytes, IPEndPoint fromThisEndPoint)
    {
      if (bytes == null) throw new ArgumentNullException("bytes");
      if (fromThisEndPoint == null) throw new ArgumentNullException("fromThisEndPoint");
      try
      {
        sender.Send(bytes, bytes.Length, fromThisEndPoint);
      }
      catch (Exception)
      {
      }
    }

    public void Start()
    {
      listener.Start();
    }

    public void Stop()
    {
      listener.Stop();
    }

    public StartableStatus Status
    {
      get { return listener.Status; }
    }
  }
}

נעבור קצת על הקוד:

  • הקונסטרקטור EchoServer(IListener listener) מראה לנו שכל הפונקציונליות שנדרשת מה Listener היא חיצונית ל Echo Server שלנו, ולכן "מוזרקת" אליו פנימה. במילים אחרות, יש כאן Dependency Injection (ובקיצור, DI). צורת העבודה הזו מאפשרת לכל מפתח שהוא ליצור Listener משלו, ולהשתמש בו בקונסטרקטור הזה. כל עוד ה Listener הנ"ל יממש את ה interface שהגדרנו, הכל ימשיך לעבוד כרגיל. זה היופי של DI.
  • עם כל הכבוד ל DI, ויש כבוד, אנחנו יכולים בהחלט לחוס על המפתחים תמימי הלב ורכי הנפש שרק רוצים להריץ UDP Echo Server על פורט מסויים. לכן אנחנו יוצרים עוד קונסטרקטור ש"יזריק" את המימוש שכבר כתבנו ל IListener. כך נוכל לתת מענה מיידי בלי להתקשקש על הרחבות והזרקות, רק פורט להאזין בו ולסגור עניין.
  • כל קריאת start/stop/status מועברת הלאה ל IListener שיש לנו. הלוגיקה הזו כבר מוטמעת שם היטב, אין צורך להמציא אותה מחדש
  • הרכיב שמבצע את שליחת ההודעות הוא UdpClient הדוט נטי המובנה. מתכנתים שנוטים לקוד טהור יותר בהיבטים של OOP ושל SOLID יוכלו לטעון שגם זה יכול להיות מוזרק פנימה. ואני אומר, אוקיי, אפשר, אבל כמה כבר יכול להיות שונה המימוש של שליחת הודעה ב UDP? נקודה למחשבה. נגיע לזה בהמשך.
  • השאר הוא רק להאזין לאירוע של ה Listener ולשלוח בחזרה בדיוק אותה הודעה. סה טו.

איך בודקים את זה? אה, זה כבר יותר מורכב, כי כאן אנחנו צריכים שני מחשבים. בגדול, מריצים את ה UDP Echo Server על מחשב X, ושיאזין על פורט P. הולכים למחשב Y, מריצים WireShark (או כל network monitor אחר שיודע לנטר UDP), ושולחים הודעת UDP אל מחשב X ואל פורט P. ה WireShark כבר יראה לנו מהו פורט היציאה של ההודעה ואם התקבלה הודעה חוזרת. אני מעדיף לחסוך את תמונות המסך, תצטרכו לנסות בעצמכם או פשוט להאמין לי.

בניית UDP Forwarder

רכיב כמו UDP Forwarder הוא רכיב שמצד אחד מקבל הודעות UDP, ומצד שני הוא שולח אותן הלאה לכתובת אחרת. מכאן שהוא מתפקד כ Forwarder, "מקדם" או "מקפיץ" את ההודעות לכתובת אחרת.
מתי שימושי? בזמנו, כשעסקנו ב Video Streaming ובפרוטוקולים כמו SIP ו RTP, יצא לנו להגיע למצב שבו המערכת היתה חייבת להזרים וידאו מכתובת מסויימת, גם אם מקור ה streaming לא היה מאותה כתובת (וזה ככה מטעמי security). אז הרצנו UDP Forwarder שכזה (ובחרנו לו שם ציורי יותר: Back to Back UDP server), שמצד אחד קיבל את הוידאו מכתובת מסויימת, ומצד שני רק המשיך את הזרמת הוידאו אל כתובת אחרת.

ניגש ישר לקוד, ונראה שההבדלים בין ה UDP Forwarder ל UDP Echo Server הם ממש זעירים. לא מפתיע, כי שניהם מבצעים כמעט אותה פעולה: ה Echo Server שולח את ההודעה שהתקבלה למקור שממנו הגיעה, וה Forwarder שולח את ההודעה שהתקבלה ליעד מוגדר מראש. בסה"כ שני החבר'ה האלה הם ממש דומים. אולי נעשה קצת refactoring אח"כ? אולי. הנה הקוד, בכל מקרה:

using System;
using System.Net;
using System.Net.Sockets;
using Groundhog.Lib.MiniPatterns.Startable;

namespace Groundhog.Lib.Net.Udp
{
  public class Forwarder : IStartable
  {
    private readonly IListener listener;
    private readonly IPEndPoint destination;
    private readonly UdpClient sender = new UdpClient();

    public Forwarder(int port, IPEndPoint destination) : this(new Listener(port), destination)
    {
    }

    public Forwarder(IListener listener, IPEndPoint destination)
    {
      if (listener == null) throw new ArgumentNullException("listener");
      if (destination == null) throw new ArgumentNullException("destination");
      this.listener = listener;
      this.destination = destination;
      listener.DatagramReceived += DatagramReceived;
    }

    private void DatagramReceived(byte[] bytes, IPEndPoint fromThisEndPoint)
    {
      if (bytes == null) throw new ArgumentNullException("bytes");
      try
      {
        sender.Send(bytes, bytes.Length, destination);
      }
      catch (Exception)
      {
      }
    }

    public void Start()
    {
      listener.Start();
    }

    public void Stop()
    {
      listener.Stop();
    }

    public StartableStatus Status
    {
      get { return listener.Status; }
    }
  }
}

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

To Refactor Or Not To Refactor

Keep it DRY

יש לנו שני classים ביד, שלמעשה מבצעים כמעט אותה עבודה. כלומר יש להם הרבה מן המשותף. אחד העקרונות בתכנות הוא DRY, קרי Don't Repeat Yourself. בעברית זה יוצא "אתעע" :-) . במילים אחרות, אם יוצא לנו לכתוב קוד מאוד דומה, כדאי למצוא את המכנה המשותף ולכתוב אותו פעם אחת בלבד. למה? כי אם יש באג, אז צריך לפתור אותו בשני מקומות, וצריך בכלל לזכור שיש שני מקומות כאלה מלכתחילה. אם מתכנת זיהה באג ברכיב אחד, אין לו שום יכולת לדעת שאותו באג קיים גם ברכיב השני, כל עוד שניהם כתובים בנפרד.

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

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

בואו ניקח את הצד של התביעה ונעשה כאן קצת refactoring. התוצאה היא base-class (שוב ירושה, מה קורה לי בזמן האחרון?), שנראה כך:

using System;
using System.Net;
using System.Net.Sockets;
using Groundhog.Lib.MiniPatterns.Startable;

namespace Groundhog.Lib.Net.Udp
{
  public class UdpReplierBase : IStartable
  {
    private readonly IListener listener;
    private readonly UdpClient udpClient = new UdpClient();
    private readonly Func<IPEndPoint, IPEndPoint> destinationChooser;

    public UdpReplierBase(int port, Func<IPEndPoint, IPEndPoint> destinationChooser) : this(new Listener(port), destinationChooser)
    {
    }

    public UdpReplierBase(IListener listener, Func<IPEndPoint, IPEndPoint> destinationChooser)
    {
      if (listener == null) throw new ArgumentNullException("listener");
      if (destinationChooser == null) throw new ArgumentNullException("destinationChooser");
      this.listener = listener;
      this.destinationChooser = destinationChooser;
      listener.DatagramReceived += DatagramReceived;
    }

    private void DatagramReceived(byte[] bytes, IPEndPoint fromThisEndPoint)
    {
      if (bytes == null) throw new ArgumentNullException("bytes");
      try
      {
        var destination = destinationChooser(fromThisEndPoint);
        udpClient.Send(bytes, bytes.Length, destination);
      }
      catch (Exception)
      {
      }
    }

    public void Start()
    {
      listener.Start();
    }

    public void Stop()
    {
      listener.Stop();
    }

    public StartableStatus Status
    {
      get { return listener.Status; }
    }
  }
}

ועכשיו היורשים הגאים הצטצמו פלאים, והם נראים ככה:

using System;
using System.Net;

namespace Groundhog.Lib.Net.Udp
{
  public class EchoServer : UdpReplierBase
  {
    private static IPEndPoint DestChooser(IPEndPoint x)
    {
      return x; // same as input
    }

    public EchoServer(int port)
      : base(port, DestChooser)
    {
    }

    public EchoServer(IListener listener)
      : base(listener, DestChooser)
    {
    }
  }

  public class Forwarder : UdpReplierBase
  {
    public Forwarder(int port, IPEndPoint destination)
      : base(port, x => destination)
    {
    }

    public Forwarder(IListener listener, IPEndPoint destination)
      : base(listener, x => destination)
    {
    }
  }
}

קומפקטי, לא? קבלנו אותה פונקציונליות, ואנחנו לא חוזרים על עצמנו (יש חזרה קטנטונת ב lamba expression שב Forwarder, אבל זה בקטנה).
יתרה מכך, שמרנו בדיוק על אותם קונסטרקטורים ועל אותו API חיצוני, כך שאם כבר יש קוד כתוב – הקוד יכול להשאר כפי שהוא, בלי לשנות כלום.
התביעה חוגגת, ההגנה נחה.

שיפורים, שיפורים

הפוסט הזה, כמו שכתבתי בתחילתו, הוא לא רק על UDP. יש כאן הרבה מעבר לפרוטוקול עצמו. נכנסתי כאן גם ל Dependency Injection, שזאת אחת השיטות לממש את ה D ב SOLID, והראיתי קצת Refactoring. ה UDP הוא מצע נוח יחסית להמחיש עקרונות בתכנות.

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

  • כמו שציינתי קודם לכן, ה Sender יכול לקבל אבסטרקציה מלאה. אם הוא יהיה אבטרקטי, כלומר יהיה לנו interface עם מתודה אחת ויחידה כמו Send, אז נוכל גם לכתוב Unit Testing לקוד שלנו.
  • הקוד לא מתייחס עד הסוף להיבט של ת'רדים. למשל, האם ה Echo Server צריך לשלוח את ההודעה שהוא קיבל על אותו ת'רד שמטפל באירוע קבלת ההודעה? אולי כדאי לשלוח בת'רד נפרד? אם היינו כותבים את האבטרקציה ל Sender, אפשר היה לבחור במימושים שונים ל Sender: כזה ששולח על אותו ת'רד, או כזה ששולח בת'רד נפרד.

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

namespace Groundhog.Lib.Net.Udp
{
  public interface ISender
  {
    void Send(byte[] dgram, int bytes, System.Net.IPEndPoint endPoint);
  }
}

Simple Implementation

נמשיך עם מימוש פשוט, ע"י ה UdpClient הדוט נטי:

using System.Net;
using System.Net.Sockets;

namespace Groundhog.Lib.Net.Udp
{
  public class BlockingSender : ISender
  {
    private readonly UdpClient udpClient = new UdpClient();

    public void Send(byte[] dgram, int bytes, IPEndPoint endPoint)
    {
      udpClient.Send(dgram, bytes, endPoint);
    }
  }
}

Multi-Threaded Implementation

ועכשיו למימוש עם ת'רד נוסף, לצורך הפשטות – נממש בעזרת ה ThreadPool:

using System;
using System.Net;
using System.Threading;

namespace Groundhog.Lib.Net.Udp
{
  public class ThreadPoolSender : ISender
  {
    private class MyState
    {
      public byte[] dgram;
      public int bytes;
      public IPEndPoint endPoint;
    }

    private readonly ISender blockingSender;

    public ThreadPoolSender(ISender blockingSender)
    {
      if (blockingSender == null) throw new ArgumentNullException("blockingSender");
      this.blockingSender = blockingSender;
    }

    public void Send(byte[] dgram, int bytes, IPEndPoint endPoint)
    {
      var myState = new MyState()
                {
                  dgram = dgram,
                  bytes = bytes,
                  endPoint = endPoint
                };
      ThreadPool.QueueUserWorkItem(MyCallback, myState);
    }

    private void MyCallback(object state)
    {
      var myState = (MyState) state;
      blockingSender.Send(myState.dgram, myState.bytes, myState.endPoint);
    }
  }
}

קצת review על הקוד:

שימו לב שגם כאן יש Dependency Injection: אין כאן עבודה מול Sender קונקרטי, אלא מול ISender, שאמור להתקבל בקונסטרקטור. זאת משום שהאחריות היחידה של ה class הזה היא בביצוע על ת'רד אחר. כאן אנחנו נוגעים באות נוספת מה SOLID: ה S מייצגת את העקרון SRP, שזה Single Responsibility Principle.

Putting it all together

ועכשיו "נתפור" את ה ISender עם ה UdpReplierBase, שהרי זו היתה המוטיבציה לכך מלכתחילה, ונראה את התוצר הסופי:

using System;
using System.Net;
using Groundhog.Lib.MiniPatterns.Startable;

namespace Groundhog.Lib.Net.Udp
{
  public class UdpReplierBase : IStartable
  {
    private readonly IListener listener;
    private readonly ISender sender;
    private readonly Func<IPEndPoint, IPEndPoint> destinationChooser;

    public UdpReplierBase(int port, Func<IPEndPoint, IPEndPoint> destinationChooser)
      : this(new Listener(port), destinationChooser)
    {
    }

    public UdpReplierBase(IListener listener, Func<IPEndPoint, IPEndPoint> destinationChooser)
      : this(listener, new BlockingSender(), destinationChooser)
    {
    }

    public UdpReplierBase(IListener listener, ISender sender, Func<IPEndPoint, IPEndPoint> destinationChooser)
    {
      if (listener == null) throw new ArgumentNullException("listener");
      if (sender == null) throw new ArgumentNullException("sender");
      if (destinationChooser == null) throw new ArgumentNullException("destinationChooser");
      this.listener = listener;
      this.sender = sender;
      this.destinationChooser = destinationChooser;
      listener.DatagramReceived += DatagramReceived;
    }

    private void DatagramReceived(byte[] bytes, IPEndPoint fromThisEndPoint)
    {
      if (bytes == null) throw new ArgumentNullException("bytes");
      try
      {
        var destination = destinationChooser(fromThisEndPoint);
        sender.Send(bytes, bytes.Length, destination);
      }
      catch (Exception)
      {
      }
    }

    public void Start()
    {
      listener.Start();
    }

    public void Stop()
    {
      listener.Stop();
    }

    public StartableStatus Status
    {
      get { return listener.Status; }
    }
  }
}

נוכל לכתוב למחלקות היורשות עוד קונסטרקטור. ניקח למשל את ה EchoServer שלנו:

using System.Net;

namespace Groundhog.Lib.Net.Udp
{
  public class EchoServer : UdpReplierBase
  {
    private static IPEndPoint DestChooser(IPEndPoint x)
    {
      return x; // same as input
    }

    public EchoServer(int port)
      : base(port, DestChooser)
    {
    }

    public EchoServer(IListener listener)
      : base(listener, DestChooser)
    {
    }

    public EchoServer(IListener listener, ISender sender)
      : base(listener, sender, DestChooser)
    {
    }
  }
}

כדי להפעיל את ה EchoServer שלנו ב Console Application, נוכל לכתוב כך:

using System;
using Groundhog.Lib.Net.Udp;

namespace Groundhog.ConsoleApp
{
  class Program
  {
    static void Main(string[] args)
    {
      Console.WriteLine("starting up…");
      var echoServer = new EchoServer(9777);
      echoServer.Start();
      Console.WriteLine("started, press ENTER to quit");
      Console.ReadLine();
      echoServer.Stop();
    }
  }
}

אבל זו ההפעלה שלו בתצורה של ברירת מחדל. אם נרצה, למשל, ששליחת Datagrams בחזרה תתבצע על ת'רד אחר, נצטרך להזריק פנימה instance של ThreadPoolSender:

using System;
using Groundhog.Lib.Net.Udp;

namespace Groundhog.ConsoleApp
{
  class Program
  {
    static void Main(string[] args)
    {
      Console.WriteLine("starting up…");
      var threadPoolSender = new ThreadPoolSender(new BlockingSender());
      var echoServer = new EchoServer(new Listener(9777), threadPoolSender);
      echoServer.Start();
      Console.WriteLine("started, press ENTER to quit");
      Console.ReadLine();
      echoServer.Stop();
    }
  }
}

מה הרווחנו בינתיים?

עניין ראשון הוא שה classים שיש לנו הם יותר loosely coupled, כלומר הקטנו את הצימוד בין ה UdpReplierBase ל Sender. מה שנשאר לנו ביד זה החוזה, או ההתנהגות של ה Sender. מעכשיו, ה UdpReplierBase בכלל לא מתעניין במימוש של השליחה, הוא רק יודע שאמור להיות לו איזה רפרנס ל Sender שכזה. במילה אחת, עשינו decoupling.

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

ומה עוד הרווחנו? עכשיו אפשר לעשות קצת Unit Testing ל classים שלנו (לא להכל). בקוד המצורף ניתן לראות את הטסטים עצמם, כאן בפוסט אני אציג רק טסט אחד, שהוא למעשה הטסט העיקרי ל Echo Server, כי דרכו אפשר לוודא שהקוד שכתבנו אכן יעבוד כמצופה, כל עוד המימושים של ה Listener ושל ה Sender יעבדו כראוי (וזאת המהות של unit testing – לבדוק את ה unit, ולא שום דבר אחר):

using System.Net;
using Groundhog.Lib.Net.Udp;
using Moq;
using NUnit.Framework;

namespace Groundhog.Lib.Testing
{
  [TestFixture]
  public class EchoServerTests
  {
    [Test]
    public void ReceivedMessageIsSentToSource()
    {
      var listenerMock = new Mock<IListener>();
      var senderMock = new Mock<ISender>(MockBehavior.Strict);
      var echoServer = new EchoServer(listenerMock.Object, senderMock.Object);
      echoServer.Start();
      byte[] dgram = {1, 2, 3, 4, 5};
      var sourceEndPoint = new IPEndPoint(new IPAddress(new byte[] {192, 168, 1, 5}), 5544);
      senderMock.Setup(x => x.Send(dgram, dgram.Length, sourceEndPoint)).AtMostOnce();
      listenerMock.Raise(x => x.DatagramReceived += null, dgram, sourceEndPoint);
      senderMock.VerifyAll();
    }
  }
}

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

מה הפסדנו?

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

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

שנית, אפשר לכתוב מתודות סטטיות, כמו Create, שיוצרות instance בקלות יתרה. לדוגמה:

using System.Net;

namespace Groundhog.Lib.Net.Udp
{
  public class EchoServer : UdpReplierBase
  {
    // code, code, code…

    public static EchoServer CreateMultiThreaded(int port)
    {
      var threadPoolSender = new ThreadPoolSender(new BlockingSender());
      var echoServer = new EchoServer(new Listener(port), threadPoolSender);
      return echoServer;
    }
  }
}

והקריאה עצמה:

using System;
using Groundhog.Lib.Net.Udp;

namespace Groundhog.ConsoleApp
{
  class Program
  {
    static void Main(string[] args)
    {
      Console.WriteLine("starting up…");
      var echoServer = EchoServer.CreateMultiThreaded(9777);
      echoServer.Start();
      Console.WriteLine("started, press ENTER to quit");
      Console.ReadLine();
      echoServer.Stop();
    }
  }
}

ובאופן דומה, אפשר לכתוב Factory class שעושה אותו הדבר.

אז מה היה לנו?

בפוסט הזה הראיתי איך אפשר לקחת משימה יחסית פשוטה כמו UDP Echo Server ולהדגים דרכה מספר מרכיבים בתכנות מודרני:

  • כתיבת קוד OOP, עם מימושים שונים של Solid, ביניהם DI ו SRP.
  • Refactoring
  • Unit Testing

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

כמו כן, קיים הבדל משמעותי בין מספר הקבצים שצריך לתחזק (ולדבג) כאשר כותבים קוד בצורה יותר "נכונה". לכתוב UDP Echo Server אפשר גם ב class יחיד. כן, אתם קוראים נכון: אחד, Uno, ואחד, one. וכאן, אחרי כל הפירוקים וההפרדות שלנו, הגענו לשבע מחלקות רק בפוסט הזה, ועוד כמה שקשורות לפנוקציונליות של Start/Stop מהפוסטים הקודמים. אז במקום class אחד יש לנו בסביבות 10 מחלקות אחרות. זה המון. האם זה מוצדק? לפעמים כן, במיוחד כאשר החלק של Unit-Testing יותר משמעותי (וחוסך בדיקות אינטגרציה בפועל), ולפעמים לא. כמו תמיד, אין תשובה אחת, וזה תלוי במידת ה code-reuse שמתקבל (ועד כמה זה באמת תורם ברמה הפרקטית), במידת הבדיקתיות, ובגורמים נוספים. לא תמיד אפשר לדעת מראש שיתקבלו כל כך הרבה מחלקות, ובכל מקרה תמיד אפשר לסדר קצת את הקוד שלנו ע"י הוספת תיקיות מתאימות.

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

תכנות נעים!

אתם מוזמנים להוריד את הקוד לפוסט הזה (בתצורה הסופית שלו).

Startable – State Machine part 2

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

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

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

A Few Words on StackOverflow.com

הלוגו של stackoverflow

הפוסט הזה הוא על האתר stackoverflow.com (ויקיפדיה), אתר שהפך להיות אחד המרכזיים והפעילים בתחום פיתוח תוכנה וכל מה שמסביב, בסגנון של מישהו-שואל-שאלה-ומלא-אנשים-עונים-עליה. או, בניסוח שלהם: Stack Overflow is a programming Q & A site that's free. איך זה שהאתר הזה הפך להיות כל כך פופולרי תוך זמן קצר? ואולי, לצד ההצלחה, יש לו גם כמה צדדים פחות טובים?

נתחיל מההצלחה עצמה: האתר הפך להיות ה-אתר ה-מוביל בעולם בשאלות/תשובות בענייני תכנות, וכל זאת כמעט מיד עם השקתו. האתר כולל מערכת ניקוד עשירה (שמזכירה פה ושם את המתחרה שנשאר הרחק מאחור, experts-exchange), ונכון להיום, הוא במקום 545 לפי מדד אלקסה. כבוד.

אז מה כל כך טוב ב stackoverflow הזה?

נו, זה פשוט נראה טוב

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

כן, וגם חוויית משתמש טובה

אין ספק, החבר'ה ב stackoverflow עשו עבודה טובה בהיבט של User Experience, ובכל הקשור לתגובתיות ולמהירות הניווט – זה פשוט עובד, ועובד נפלא. השימוש ב ajax הופך את העניינים לפחות "תהליכיים" והכל זורם יותר טוב (לדוגמה – הצמדת תגים לשאלה חדשה זה עניין ממש פשוט, גם כאשר מדובר בתג חדש).

אה כן, אין שם קטגוריות, הכל תגיות

בשנים האחרונות חל מפנה בארגון המידע, ובמקום היררכיה של קטגוריות (נניח קטגוריית "מסדי נתונים" ושם תתי-קטגוריה של "Oracle" ו"MS SQL Server"), ב stackoverflow יוצרים תגיות. אמנם, גם "תגית" וגם "קטגוריה" מסווגים את המידע ומאפשרים סינון, אלא שמידע מבוסס תגיות הוא שטוח, ואין צורך להסתגל להיררכיית סיווג מידע שחשב עליה עורך תוכן או מידען כזה או אחר. אם נרצה, למשל, לצפות בשאלות שעוסקות אך ורק ב SMTP וב Oracle – נוכל לסנן לפי התגיות הרלוונטיות וזהו. לא נצטרך להתחקות אחר הקטגוריות המובנות. ב stackoverflow הבינו את זה, ולקחו את זה עד הסוף, גם נוח וגם יעיל. אגב, גם אתרים כמו themarker הישראלי הרימו את הכפפה של תגיות.

ויש מערכת מוניטין עשירה (Voting and Reputation)

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

זה לא נגמר רק במוניטין של נקודות נטו. יש גם מערכת badgeים: מעין "תגים", או "עיטורי כבוד" לפעילות כזו או אחרת. שאלת שאלה שיש עליה יותר מ 10 הצבעות חיוביות – קיבלת את ה"עיטור" שנקרא Nice Question. שאלת שאלה שזכתה ל 2500 צפיות ומעלה – קיבלת את ה"עיטור" שנקרא Notable Question. ועוד ועוד.

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

יש שם המון המון (המון!) ידע. באמת המון

stackoverflow?

מתכנתי כל העולם התאחדו! אם ידע הוא כוח, הרי ש stackoverflow הוא לפחות הענק הירוק. קחו שאלה כמו Hidden Features of C# ותראו כמה ידע מזוקק אפשר לקבל מהרבה אנשים שכנראה מעולם לא הכרתם אישית.

יש היענות

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

ויש גם קצת הומור פה ושם

ה-gravatar שלי?

יום אחד, למשל, הוסיפו לינק מוזר של קשת בענן לכל שאלה (עם הכיתוב Cornify). כאילו בארבי בעצמה החליטה לתת חסות לאתר. לחיצה על הלינק המוזר גרמה להופעת תמונה בסגנון של קשת בענן, חד קרן, ירח וכד'. אחרי כמה לחיצות רצופות היתה תחושה שבארבי ובראץ חברו יחד (!) כדי להשתלט על הגיקים שמבקרים ב stackoverflow: המסך התמלא בתמונות קיטש מזעזעות. ואז, אחרי עוד לחיצה או שתיים, הופיעה הודעת "אחד באפריל" על המסך. חמוד, מצחיק, וגם, מה לעשות, קצת גיקי :wink: . כל זה היה באחד באפריל 2009. השנה (2010) המתיחה היתה יותר צנועה, ורק שינו את ה gravater של כולם להיות בסגנון של חד-קרן-קשת-בענן-לבבות.

נו, אז אם הכל כל כך טוב ויפה, אז מה יכול להיות רע?

אז נעבור לדברים… הלא כל כך טובים

זה תזזיתי

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

הניקוד יכול להטעות

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

stackoverflow?

שאלות קיטבג

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

אז מה נסגר?

טוב, זה עדיין נחשב להיות ה-אתר ה-מוביל של שאלות ותשובות בענייני תכנות. בעקבות ההצלחה שלו נפתחו אתרים דומים בתחומים אחרים (למשל, server-fault בתחום ה IT), או אתרים בהשראתו, מוצלחים יותר או פחות (למשל, iask הישראלי).

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

נתראה!

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

SMS Integration: Israel is wayyy behind US

והפעם פוסט סביב אינטגרציה.

החלטנו להוסיף למוצר שלנו מערכת התראות ב SMS. הנמענים אמורים להיות בישראל, בלי מגבלה על הרשתות והמפעילים.
האינסטינקט הראשוני הוא ליצור קשר עם ספקי SMS בישראל, שמאפשרים התממשקות באמצעות Web Service כזה או אחר, להתרשם ולקנות חבילה.
גיגלתי ומצאתי שני ספקים ישראליים:

  • SimpleSMS
  • שירות של גולדמן תקשורת שנקרא SMS API

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

אז כאמור, גולדמן תקשורת. שם זה סיפור אחר: באתר שלהם מדברים על API בצורת Web Service, ואפילו יש screenshot שממחיש את זה, אבל תיעוד אמיתי אין. ודוגמאות קוד, זה בכלל "מוקצה". האמת, חייבים להיכנס ולראות כדי להאמין.
אבל אני, אופטימיסט אני. אמרתי, ניתן צ'אנס, נרים טלפון. התקשרתי ובקשתי לנסות את המוצר, או לפחות לקבל את ה API הנכסף (ולא ב screenshot). מהעבר השני של הקו הסביר לי, בתקיפות לא ברורה, שהם היחידים שעובדים דרך סלקום, כבר שנים, ושהמתחרים לא אמינים כמותם. הסיטואציה לא היתה מספיק ברורה (לשנינו, כנראה), וסיכמנו שכדאי לי לדבר עם אסף, הבן שלו, בעניין ה API.
מסתבר שתחום ה pre sales בגולדמן תקשורת קצת בעייתי, כי אסף סרב לספק את התיעוד ל API, והסביר שאני צריך לקנות את השירות כדי לקבל את ה API והתיעוד. עכשיו, נכון שהחבילה המינימלית עולה בסה"כ 20 ש"ח, וזה סכום פעוט לחברות תוכנה, אבל למה להחביא את ה API? מה כל כך סודי בו? אסף הסביר לי שמנסיון העבר שיש להם, זו מדיניות החברה.
יש משהו קצת פישי בהסתרת API, ולא בעיה להרים שירות Dummy שיהווה "מתקן אימונים" אם רוצים להסתיר את הכתובת של השירות האמיתי. במילים אחרות, אני לא רואה בעיה שהיא לא פתירה, ובגלל זה אני מסיק שיש כאן בעיה של מקצועיות.
אז גם גולדמן תקשורת נפסל.

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

נחזור רגע לאינסטינקט הראשוני (מתחילת הפוסט), שאומר שבשביל שירות אינטגרציה מקומי כדאי לפנות לספקים מקומיים, כי הם בטח מתמצאים יותר ויהיו דרכם פחות בעיות טכניות. מסתבר שהפעם, פניה לספק חיצוני העלתה תוצאה טובה יותר: האמריקאים כנראה יודעים לעשות את זה טוב יותר ומקצועי יותר מהישראלים (ובכמות גדולה של הודעות, גם זול יותר). אולי כי הסליקה שלהם עובדת בלי בעיה, ואולי כי הכל פשוט מתקתק כמו שצריך, עם שירות מבוסס ReST, ועם תיעוד מלא ומקיף (ולא מוסתר…).
זה אולי לא מדגם מייצג, אבל בכל זאת, בשנת 2010, הייתי מצפה מחברות טכנולוגיה/אינטגרציה מקומיות להרים שירות בסטנדרט גבוה יותר.

לא נורא, העיקר שעובד :-)

קטגוריות:אינטגרציה, תכנות תגיות:,

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 בדיוק כמו שרצינו.

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

תכנות נעים!

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

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