ארכיון

ארכיון הכותב

IoC Container Explained

14 יולי, 2011 4 תגובות

בפוסט הזה אני רוצה להסביר את עיקרי הדברים סביב IoC, שזה ראשי תיבות של Inversion of Control. למעשה אני רוצה לספר מעט על התיאוריה ולהרחיב על הפרקטיקה.

בקיצור, אני רוצה להסביר ולהדגים מה זה IoC Container.

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

הנה קצת לינקים חיצוניים, שמסבירים (באנגלית, מן הסתם) את הנושא:

  • אחד מהמאמרים המפורסמים ביותר בנושא הוא של Martin Fowler. המאמר נקרא
    "Inversion of Control Containers and the Dependency Injection pattern"
  • בויקיפדיה אפשר למצוא את:

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

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

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

namespace Canberra.InterfacesDemo.Lib
{
  public class CustomersReminder
  {
    public void Remind(ICustomersFetcher customersFetcher, IEmailSender emailSender)
    {
      foreach (var customer in customersFetcher.GetSleepingCustomers())
      {
        emailSender.SendRemindMessage(customer.FirstName,
		  customer.LastName,
		  customer.EmailAddress);
      }
    }
  }
}

השאלה שנותרה פתוחה היא: "מי אמור לספק instance של מחלקות שמממשות ICustomerFetcher ו IEmailSender?"

אפשרות אחת היא ליצור instance כחלק מהקוד עצמו, שקורא לפונקציה Remind. הקוד יראה כך:

var customersReminder = new CustomersReminder();
customersReminder.Remind(new CustomersFetcher(), new EmailSender());

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

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

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

מה שמשנה כאן הוא שיש תלות בין הפרויקט שלנו, לרפרנס של מימוש ספציפי של הממשק IEmailSender.

אם נרצה יום אחד להשתמש במימוש אחר לממשק IEmailSender, נצטרך לבנות מחדש את את הפרויקט שלנו.

בואו נבדוק לרגע מה זה אומר:

  • לבנות מחדש פרויקט – Build
  • לבדוק שוב שהכל תקין – QA
  • להפיץ את התוצר לאן שצריך – Deploy

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

אז מה האלטרנטיבה?

הבעיה העיקרית שלנו היא איך להיפטר מהפעולה new בקוד שלנו.

נרצה להגיע לקוד שבו:

  • אין את הפעולה new
  • אין התייסות בקוד עצמו או ברפרנסים של הפרויקט לאסמבלי שנרצה להשתמש בו

ובמילים אחרות:

הבחירה באסמבלי שיממש את הממשק תהיה בקונפיגורציה.

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

או שלא בא.

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

אז במקום שאנחנו נשבור את הראש כדי להשתמש ברפלקשן לעניין זה – נוכל להשתמש בפריימוורק שאחרים כתבו, ושהם כבר שברו את הראש בשבילנו. לפריימוורק שכזה קוראים IoC Container.

IoC Container

IoC זה ראשי תיבות של Inversion of Control. לצערי IoC זה מושג שקשה עד בלתי אפשרי לתרגם אותו לעברית, ולכן אני אשאר איתו כמו שהוא. IoC Container זה איזשהו אמצעי/כלי/מיכל/קונטיינר/פריימוורק/ספריה לביצוע הפעולות של IoC. באופן כללי, זה אמצעי ליצור instance של מחלקה בלי שיהיה לה רפרנס בפרויקט.

בסביבת דוט נט יש מספר IoC Containers נפוצים. מתוכם אני שמח להזכיר את:

  • Spring.NET
  • Castle Windsor
  • StructureMap
  • Autofac
  • Unity
  • Ninject

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

באופן אישי יצא לי לעבוד עם Spring.NET, עם Ninject, עם StructureMap (כיום) ועם Unity (גם כיום; למעשה אנחנו בתהליך של החלפת SM ב Unity).

בואו נחזור לבעיה שלנו ונראה איך משתמשים ב Unity כדי לקבל instance של הממשק IEmailSender.

הדגמה עם Unity

קודם כל, צריך רפרנס מהפרויקט שלנו ל Unity. נוסיף. יש.

עכשיו, לפקודה עצמה. במקום הקוד הזה:

var customersReminder = new CustomersReminder();
customersReminder.Remind(new CustomersFetcher(), new EmailSender());

נרשום כך:

var container = new UnityContainer();
var section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");
section.Configure(container);

var customersReminder = new CustomersReminder();
customersReminder.Remind(new CustomersFetcher(), container.Resolve<IEmailSender>());

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

-נצטרך לקנפג אותה כמובן.

נוכל להשתמש בקובץ ה config הסטנדרטי לצורך העניין. בואו נראה דוגמה:

<?xml version="1.0"?>
<configuration>
  <configSections>
    <section
		name="unity"
		type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,
			Microsoft.Practices.Unity.Configuration"
	/>
  </configSections>
  <unity>
    <typeAliases>
      <typeAlias
		alias="IEmailSender"
		type="Canberra.InterfacesDemo.Lib.IEmailSender,
			Canberra.InterfacesDemo.Lib"
	  />
      <typeAlias
		alias="EmailSender"
		type="MyMail.EmailSender, MyMail"
	  />
    </typeAliases>
    <containers>
      <container>
        <types>
          <type type="IEmailSender" mapTo="EmailSender"/>
        </types>
      </container>
    </containers>
  </unity>
</configuration>

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

בדוגמה הזו, כנגד הבקשה למימוש של IEmailSender, כלומר בקשת ה Resolve, ה Unity תחזיר לנו instance של המחלקה MyMail.EmailSender, שנמצאת באסמבלי MyMail.

כדי שזה באמת יקרה, קבצי ה dll של MyMail (וכל התלויות שלהם) צריכים להיות באותה תיקיה של ה exe של הפרויקט שלנו (או בתיקיית bin במקרה של web application).

זהו, עכשיו אפשר לקפוץ משמחה.

כדי להנות קצת יותר, במקרה של אפליקציית web, מומלץ לאתחל את ה Unity ב Application_Start שב global.asax, ולהגדיר משתנה סטאטי שתמיד נפנה אליו:

using System;
using System.Configuration;
using Microsoft.Practices.Unity;
using Microsoft.Practices.Unity.Configuration;

namespace Canberra.InterfacesDemo.WebApp
{
  public class Global : System.Web.HttpApplication
  {
    public static UnityContainer Container;

    protected void Application_Start(object sender, EventArgs e)
    {
      Container = new UnityContainer();
      var section = (UnityConfigurationSection)ConfigurationManager
  		.GetSection("unity");
      section.Configure(Container);
    }
  }
}

וכך, הקוד שלנו שמשתמש ב Resolve של Unity יצטמצם באופן הבא:

var customersReminder = new CustomersReminder();
customersReminder.Remind(new CustomersFetcher(),
    Global.Container.Resolve<IEmailSender>());

והשמחה רבה :-)

אחרי ששמחנו ונהנינו, בואו נסכם את התהליך של שימוש ב IoC Container:

  • מוסיפים רפרנס ל IoC Container עצמו
  • במקום new בקוד שלנו, משנים לתחליף שמציע ה IoC Container. בדוגמה שלנו עם Unity זה Global.Container.Resolve<IEmailSender>()
  • מגדירים עבור ה IoC Container מה יהיה המימוש לכל interface שנרצה
  • מוודאים שכל קבצי האסמבלי והתלויות שלהם נמצאים בתיקיית ה bin של הפרויקט ה"ראשי", אחרת מקבלים אקספשן…

עוד כמה מילים על IoC Container

השימוש הבסיסי ביותר ל IoC Container (להלן IoCC) הוא ליצור instance של מחלקה לפי interface, או לפי כל type אחר. הרעיון המרכזי הוא להימנע מ new בקוד.

קונפיגורציה – בקובץ או דרך קוד?

הקונפיגורציה של ה IoCC יכולה להיות בקובץ חיצוני או בקוד עצמו. היתרונות של קובץ קונפיגורציה הם ברורים – לא צריך לבנות מחדש את האפליקציה וכו'. עם זאת, הקונפיגורציה יכולה להיות מעיקה וארוכה, ולכן יש לא מעט אנשים שמעדיפים לקנפג את ה IoC Container בקוד עצמו. למשל, מה שמציע Ninject זה קונפיגורציה רק דרך קוד. זה מאוד נקי, ואפשר להעביר לקונפיגורציה רק מה שצריך באמת.

אתחול של אובייקטים מורכבים

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

IoCC as a Factory

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

ועוד הרחבות

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

לסיכום

בפוסט הזה הראיתי איך אפשר להשתמש ב IoC Container (ובקיצור – IoCC).

הרעיון המרכזי – להעביר את הלוגיקה של איתחול של אובייקט מהקוד שלנו אל קובץ קונפיגורציה. במילים אחרות: השימוש ב IoCC מאפשר לנו להימנע מ new בקוד.

למה זה טוב?

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

למה זה לא טוב?

-כי לפעמים זה מורכב מדי למתכנתים מתחילים;

-כי זה לא כל כך פשוט להרכיב את הפאזל הזה: גם לקנפג וגם לוודא שהקבצים יגיעו לתיקיית bin ונגזרותיה… קצת כאב ראש;

-באופן כללי, קצת קשה לעשות back-tracking לקוד ולהבין מה מתבצע בפועל

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

ובשורה התחתונה, חייבים לעבוד ככה?

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

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

 

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

הינדוס נעים!

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

Regex מיתולוגי

מתי כדאי להשתמש ב Regex כדי לוודא קלט?
מתי זה לא מומלץ?
ואיפה עובר הגבול?
ואולי יש אלטרנטיבות?

תור הזהב של ה Regex

בשנים האחרונות יש הייפ משמעותי ל Regex (או בעברית, "ביטוי רגולרי", אבל אני אשתמש ב"רגקס"), בעיקר סביב וידוא קלט. נראה לי שהסיבה העיקרית לזה היא שמיקרוסופט שיבצו מספר פקדים (Controls) לוידוא קלט כחלק ממערכת ה UI הבסיסית של ה WebForms כבר מויז'ואל סטודיו 2002 (או אולי 2003). הפקדים האלה הם די פשוטים ומוגבלים: הם יכולים לוודא שערך נמצא בטווח כלשהו, או שהוא קיים וכו'. רק שניים מהם נותנים אפשרויות מתקדמות: פקד CustomValidator, שמאפשר פונקציות בדיקה מוגדרות ע"י המפתח עצמו (כחלק מהאפליקציה), ופקד RegularExpressionValidator, שמאפשר לבדוק מול רגקס את הקלט.

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

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

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

דוגמה – כתובת מייל

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

^((?>[a-zA-Z\d!#$%&'*+\-/=?^_`{|}~]+\x20*|"((?=[\x01-\x7f])[^"\\]|\\[\x01-\x7f])*"\x20*)*(?<angle><))?((?!\.)(?>\.?[a-zA-Z\d!#$%&'*+\-/=?^_`{|}~]+)+|"((?=[\x01-\x7f])[^"\\]|\\[\x01-\x7f])*")@(((?!-)[a-zA-Z\d\-]+(?<!-)\.)+[a-zA-Z]{2,}|\[(((?(?<!\[)\.)(25[0-5]|2[0-4]\d|[01]?\d?\d)){4}|[a-zA-Z\d\-]*[a-zA-Z\d]:((?=[\x01-\x7f])[^\\\[\]]|\\[\x01-\x7f])+)\])(?(angle)>)$

לא הכי נעים לקרוא את זה. האמת, נראה לי שצריך תואר ברגקס כדי להבין את התבנית הזו (מתוך האתר RegExLib.com). אבל אם זה עובד, אז הבעיה פתורה וסגרנו עניין. (דגש על "אם"…)

דוגמה נוספת – מספר טלפון בישראל

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

^(([0]([2|3|4|8|9|72|73|74|76|77])))[2-9]\d{6,7}$

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

03 6382020

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

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

וידוא קלט בקוד

בואו נבדוק את האלטרנטיבה: קוד C# שבודק תקינות טלפון בישראל, בדיוק כמו התבנית שכתובה למעלה (בלי תמיכה ברווחים, מקפים, ושאר עניינים).

namespace Electron.Validation
{
  public static class CharExt
  {
    public static bool IsDigit(this char c)
    {
      return c >= '0' && c <= '9';
    }
  }
 
  public class PhoneNumber
  {
    public static bool IsValid(string s)
    {
      if (null == s)
        return false;

      var validPrefixes = new[] {
                    "02",
                    "03",
                    "04",
                    "08",
                    "09",
                    "072",
                    "073",
                    "074",
                    "076",
                    "077",
                    "050",
                    "052",
                    "054"
                    };
      int prefixLength = 0;
      var foundMatchingPrefix = false;
      foreach (var validPrefix in validPrefixes)
        if (s.StartsWith(validPrefix))
        {
          prefixLength = validPrefix.Length;
          foundMatchingPrefix = true;
          break;
        }

      if (!foundMatchingPrefix)
        return false;

      // remaining length should be between 6 and 7
      var remainingLength = s.Length - prefixLength;
      if (!((6 == remainingLength) || (7 == remainingLength)))
        return false;

      // check the rest of the string to be digits
      for (int i = prefixLength; i < s.Length; i++)
        if (!s[i].IsDigit())
          return false;
     
      return true;
    }
  }
}

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

public class PhoneNumber
{
  public static bool IsValid(string s)
  {
    if (null == s)
      return false;

    var validPrefixes = new[] {
                  "02",
                  "03",
                  "04",
                  "08",
                  "09",
                  "072",
                  "073",
                  "074",
                  "076",
                  "077",
                  "050",
                  "052",
                  "054"
                  };
    int prefixLength = 0;
    var foundMatchingPrefix = false;
    foreach (var validPrefix in validPrefixes)
      if (s.StartsWith(validPrefix))
      {
        prefixLength = validPrefix.Length;
        foundMatchingPrefix = true;
        break;
      }

    if (!foundMatchingPrefix)
      return false;

    if (s.Length == prefixLength)
      return false;

    int nextDigitIndex;
    if (s[prefixLength] == ' ')
      nextDigitIndex = prefixLength + 1;
    else
      nextDigitIndex = prefixLength;

    // remaining length should be between 6 and 7
    var remainingLength = s.Length - nextDigitIndex;
    if (!((6 == remainingLength) || (7 == remainingLength)))
      return false;

    // check the rest of the string to be digits
    for (int i = nextDigitIndex; i < s.Length; i++)
      if (!s[i].IsDigit())
        return false;
     
    return true;
  }
}

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

קוד כפול

האמת היא שאם אנחנו חוזרים להקשר של WebForms, אז צריך לכתוב קוד שיבצע את הוידוא הזה גם ב JavaScript. כך שמבחינת תחזוקת קוד, נצטרך לתחזק שתי פונקציות: אחת ב C# ואחת ב JavaScript, וזה חסרון עצום, כי אם הלוגיקה משתנה, צריך לשנות את הקוד בשני חלקים שונים לחלוטין של אותה המערכת, וקיימת האפשרות ששני צוותים שונים לגמרי ינהלו את החלקים האלה. לכן הפתרון של פקד רגקס הוא יותר נוח לתחזוקה, כי רגקס זו פעולה שהיא חלק מדוט נט (C# או vb.net או whatever.net) וגם חלק מ JavaScript.

פתרון אפשרי: המרה אוטומטית

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

1. SharpKit, שלמדתי להכיר אותו במפגש של ALT.NET. זה כלי מסחרי, עם תמחור משתנה (חינם לשימוש אישי). לדבריהם:

When you work with SharpKit, you write C# instead of JavaScript

מעניין.

2. Script#, כלי שכבר קיים מספר שנים. העקרון די דומה. המימוש קצת שונה.

אז מתי להשתמש ב Regex?

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

  1. הקלט סטנדרטי (כמו כתובת מייל או URL), או שאינו צפוי להשתנות גם בעתיד הרחוק
  2. קל למצוא (או לכתוב) תבנית רגקס לקלט
  3. הרגקס נותן מענה ביותר מסביבה אחת. למשל, אם הוא משותף גם ל client וגם ל server (כפי שמתקיים בפיתוח אפליקציות web)
  4. ברור לי (ולצוות הפיתוח) שאם התבנית לא תקינה – מחליפים אותה באחרת ולא מנסים לדבג אותה

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

אלטרנטיבות – תוכנות עזר

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

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

Some people, when confronted with a problem, think "I know, I'll use regular expressions."
Now they have two problems.

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

אלטרנטיבות – DSL

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

עם תקווה בעיניים ובאצבעות רועדות, גיגלתי קצת regex dsl, ועל פניו נראה שיש משהו שפיתח אחד בשם Joshua Flanagan: הוא מציע DSL די בסיסי שמחליף את הסינטקס של הרגקס. בדקתי ושיחקתי קצת, והצלחתי להגיע למשהו לא רע. הנה קוד לבניית התבנית שבודקת תקינות של מספר טלפון בישראל, לפני התוספת של רווחים ומקף:

Pattern phoneNumber = Pattern.With
  .AtBeginning.Choice.Either(
    Pattern.With.Literal("02"),
    Pattern.With.Literal("03"),
    Pattern.With.Literal("04"),
    Pattern.With.Literal("08"),
    Pattern.With.Literal("09"),
    Pattern.With.Literal("072"),
    Pattern.With.Literal("073"),
    Pattern.With.Literal("074"),
    Pattern.With.Literal("076"),
    Pattern.With.Literal("077"),
    Pattern.With.Literal("050"),
    Pattern.With.Literal("052"),
    Pattern.With.Literal("054")
    )
  .Digit.Repeat.InRange(6,7).AtEnd;

והנה הקוד שמוודא טלפון בישראל עם אפשרות לרווחים ולמקף בין הקידומת למספר עצמו:

Pattern phoneNumber = Pattern.With
  .AtBeginning.Choice.Either(
    Pattern.With.Literal("02"),
    Pattern.With.Literal("03"),
    Pattern.With.Literal("04"),
    Pattern.With.Literal("08"),
    Pattern.With.Literal("09"),
    Pattern.With.Literal("072"),
    Pattern.With.Literal("073"),
    Pattern.With.Literal("074"),
    Pattern.With.Literal("076"),
    Pattern.With.Literal("077"),
    Pattern.With.Literal("050"),
    Pattern.With.Literal("052"),
    Pattern.With.Literal("054")
    )
  .WhiteSpace.Repeat.Optional
  .Literal("-").Repeat.Optional
  .WhiteSpace.Repeat.Optional
  .Digit.Repeat.InRange(6,7).AtEnd;

הקוד הרבה יותר מובן, ולכן קל לתחזוקה ולשינוי. עם זאת, הסינטקס של ה DSL הזה מורכב מדי לטעמי (או שפשוט צריך להתרגל אליו?). אגב, כדי להגיע לתוצאה הזו הוספתי מתודה ושמתי ב pastebin.
בכל מקרה, אם אני מבין נכון, הפרויקט הזה של Flanagan, כפי הוא עצמו מסביר, הוא בגדר DSL נסיוני בלבד, והוא לא הפך את הנסיון הזה לפרויקט שיתופי, ומעולם לא ניסה את הקוד בפרודקשן. חבל, דווקא יש לזה פוטנציאל.
מי שדווקא כן המשיך עם הרעיון של Flanagan ולקח אותו לכיוון של Linq to Regex הוא רועי אושרוב, מדטנט ידוע.

אז מה נסגר?

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

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

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

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

ועד אז, עזבו אותי מוידוא קלט עם רג-אקס, מבחינתי זה די דרעק-אקס :-)

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

מנשקים לפנים

8 פברואר, 2011 תגובה אחת

"מִנְשַק", לפי האקדמיה ללשון, הוא התרגום התקני למונח interface. עם כל ההצדקות שבעולם, אני תמיד אמרתי "ממשק", וכנראה אמשיך ואומר "ממשק" וזה מה יש. התרגום התקני מוצג בפוסט הזה רק פעמיים עד עכשיו, ואני חושב שבזה זה יסתכם :-)

אז ממשקים.

למה צריך ממשקים? מה הקטע שלהם?

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

נתחיל מ:

הגדרה לא פורמלית של "מה זה ממשק בדוט נט"

בדוט נט, הקוד שלנו כתוב כמעט תמיד במחלקות (class-ים). פה ושם אנחנו אולי נכתוב enum או struct למיטיבי לכת. פה ושם נכתוב אולי איזה delegate. אבל עיקר הקוד שלנו הוא במחלקות.

וכידוע, למחלקות יכולים להיות: שדות (fields), מאפיינים (properties), מתודות (methods) ואירועים (events). אני מעדיף לא להיכנס כאן לפינות של הקטע הפורמלי, אז סליחה מראש על אי הדיוקים.

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

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

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

public interface IShape
{
  double GetArea();
}

ואז נוכל להצהיר על שתי צורות גיאומטריות. לקחתי שתיים יחסית "קלות" – ריבוע ועיגול:

public class Rectangle : IShape
{
  private readonly double a;
  private readonly double b;

  public Rectangle(double a, double b)
  {
    this.a = a;
    this.b = b;
  }

  public double GetArea()
  {
    return a*b;
  }
}

public class Circle : IShape
{
  private readonly double radius;

  public Circle(double radius)
  {
    this.radius = radius;
  }

  public double GetArea()
  {
    const double pi = Math.PI; // ?
    return pi * radius * radius;
  }
}

כפי שניתן לראות, אלו מחלקות שמממשות את הממשק IShape.

ומה עושים עם זה?

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

public static void DoSomething(IShape shape)
{
  Console.WriteLine("the area is {0:00}", shape.GetArea());
}

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

דוגמה קונקרטית

(כן, דוגמה אמיתית מהחיים האמיתיים!)

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

אז יש לנו כאן שתי משימות:

  1. למצוא את כל הלקוחות האלה
  2. לשלוח להם מייל

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

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

זה יראה בערך כך:

public class CustomersReminder
{
  public void Remind()
  {
    var connectionString = "…";
    using (var connection = new OleDbConnection(connectionString))
    {
      connection.Open();
      var command = new OleDbCommand("select * from Customers where LastPurchasedAt < ?", connection);
      var todayMinus30 = DateTime.Today.AddDays(-30);
      var oleDbParameter = new OleDbParameter {OleDbType = OleDbType.Date, Value = todayMinus30};
      command.Parameters.Add(oleDbParameter);
      using (command)
      using (var reader = command.ExecuteReader(CommandBehavior.CloseConnection))
      {
        while (reader.Read())
        {
          var firstName = (string) reader["FirstName"];
          var lastName = (string) reader["LastName"];
          var email = (string)reader["EmailAddress"];
          SendMailTo(firstName, lastName, email);
        }
      }
    }
  }

  private void SendMailTo(string firstName, string lastName, string email)
  {
    var mailMessage = new MailMessage("do-not-reply@canberra-shopping.com", email);
    mailMessage.Subject = "It's been a while…";
    mailMessage.Body = string.Format("Hi there, {0} {1}, please try our new sales!", firstName, lastName);
    var smtpClient = new SmtpClient();
    try
    {
      smtpClient.Send(mailMessage);
    }
    catch (Exception ex)
    {
      // do something here, like logging or alerting..
    }
  }
}

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

במקום הקוד הזה, בואו נניח שיש לנו שתי מחלקות: CustomersFetcher ו EmailSender. נעביר את הקוד הרלוונטי למחלקות האלו. בואו נראה את המחלקה CustomersFetcher:

public class CustomersFetcher
{
  public IEnumerable<CustomerDetails> GetSleepingCustomers()
  {
    List<CustomerDetails> result = new List<CustomerDetails>();
    var connectionString = "…";
    using (var connection = new OleDbConnection(connectionString))
    {
      connection.Open();
      var command = new OleDbCommand("select * from Customers where LastPurchasedAt < ?", connection);
      var todayMinus30 = DateTime.Today.AddDays(-30);
      var oleDbParameter = new OleDbParameter { OleDbType = OleDbType.Date, Value = todayMinus30 };
      command.Parameters.Add(oleDbParameter);
      using (command)
      using (var reader = command.ExecuteReader(CommandBehavior.CloseConnection))
      {
        while (reader.Read())
        {
          var customerDetails = new CustomerDetails
                        {
                          FirstName = (string) reader["FirstName"],
                          LastName = (string) reader["LastName"],
                          EmailAddress = (string) reader["EmailAddress"]
                        };
          result.Add(customerDetails);
        }
      }
    }
    return result;
  }
}

public class CustomerDetails
{
  public string FirstName;
  public string LastName;
  public string EmailAddress;
}

המחלקה הזו מבצעת שתי פעולות: שליפת הנתונים מה DB והמרה שלהם ל class דוט נטי "טהור", מה שנקרא גם POCO.

ועכשיו, בואו נחזור אל ה Remind שלנו:

public class CustomersReminder
{
  public void Remind()
  {
    var customersFetcher = new CustomersFetcher();
    var emailSender = new EmailSender();
    foreach (var customer in customersFetcher.GetSleepingCustomers())
    {
      emailSender.SendRemindMessage(customer.FirstName, customer.LastName, customer.EmailAddress);
    }
  }
}

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

האחריות של המתודה Remind היא עדיין לא פשוטה: המתודה הזו מייצרת instance (ובעברית "מופע") של המחלקות CustomersFetcher ו EmailSender. כלומר יש במתודה את שתי השורות הראשונות שבהן יש new. בואו נחסוך את ה new הזה. בואו נקבל את ה instance-ים האלה כפרמטרים:

public class CustomersReminder
{
  public void Remind(CustomersFetcher customersFetcher, EmailSender emailSender)
  {
    foreach (var customer in customersFetcher.GetSleepingCustomers())
    {
      emailSender.SendRemindMessage(customer.FirstName, customer.LastName, customer.EmailAddress);
    }
  }
}

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

var customersReminder = new CustomersReminder();
customersReminder.Remind(new CustomersFetcher(), new EmailSender());

ועכשיו, עוד פעם, בואו ניקח את הפתרון הזה צעד נוסף קדימה. בואו נתבונן בקוד של המתודה Remind. שימו לב, המתודה Remind מתייחסת רק להתנהגות של CustomersFetcher ושל EmailSender: היא רק משתמשת במתודות שלהן, בלי לדעת מה קורה מאחורי הקלעים. כלומר לא ממש אכפת למתודה Remind איך ה CustomerFetcher מבצע את המתודה שלו. רק חשוב שתהיה מתודה כזו ושהיא תחזיר תוצאות. אותו הדבר גם לגבי ה EmailSender.

אם כך, נוכל ליצור שני ממשקים, ICustomersFetcher ו IEmailSender, שמתארים את ההתנהגות הזו:

public interface ICustomersFetcher
{
  IEnumerable<CustomerDetails> GetSleepingCustomers();
}

public interface IEmailSender
{
  void SendRemindMessage(string firstName, string lastName, string email);
}

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

public class CustomersFetcher : ICustomersFetcher
{}

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

public void Remind(ICustomersFetcher customersFetcher, IEmailSender emailSender)
{}

מה קבלנו? -עכשיו המתודה Remind בכלל לא תלויה במימוש, אלא רק בהתנהגות של שליפת הלקוחות ושל שליחת המיילים.

יופי, אבל מה זה נותן?

כאשר אנחנו מתכנתים מול ממשק, אנחנו נהנים ממספר יתרונות:

  1. אנחנו יכולים בכל שלב שהוא לשנות את המימוש, והקוד של המתודה Remind ישאר כפי שהוא. למשל, אם יום אחד נרצה לשלוח מייל דרך איזה WebService ששולח מיילים, אז נכתוב מימוש חדש ונשלח את המימוש הזה כפרמטר למתודה Remind, והקוד של המתודה Remind ימשיך לעבוד כמו שצריך. זה יתרון תחזוקתי, כי אז שינוי במימוש (נוספה מחלקה) לא גורם לשינוי בחתימה של המתודה Remind ולא בקוד שלה. במילים אחרות, ממשק הוא סוג של פרוטוקול של האפליקציה שלנו. כל עוד הפרוטוקול נשאר זהה, נוכל להחליף את המחלקות שמממשות אותו באחרות, ובקלות רבה.
  2. הקטנת הצימוד: המחלקה שלנו תלויה אך ורק בממשקים שהוגדרו. אם היא היתה תלויה במימוש, אז כל שינוי במימוש היה יכול להשפיע על המחלקה שלנו.
  3. אנחנו יכולים למקבל את הפיתוח: מתכנת א' יצור את המימוש של ICustomersFetcher ומתכנת ב' יצור את המימוש של IEmailSender. אף אחד לא מפריע לשני, וזה מקדם את הפרויקט מהר יותר לפרודקשן. למי שעובד בצוות, זה יתרון אדיר.
  4. הקוד שלנו הופך להיות יותר טסטבילי, כלומר יותר מוכן לבדיקות. בהקשר הזה, הקוד של המתודה Remind יכול להיבדק ע"י unit testing באמצעות mocking (עוד על mocking בפוסט עתידי כלשהו).

וכמובן, לא הכל דבש. עבודה עם ממשקים מייצרת עוד קבצים לתחזק, ועלולה להביא ל"הינדוס יתר" (over-engineering).

עוד קצת מוטיבציה

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

בואו נראה דוגמה שממחישה את הנקודה האחרונה.

נניח שאנחנו רוצים לכתוב מתודה שמקבלת URL של תמונה, והיא צריכה:

  1. להוריד את התמונה מהאינטרנט
  2. לשנות את הגודל של התמונה כך שהרוחב והגובה לא יעלו על 100 פיקסלים (ולשמור על פרופורציית גובה-רוחב)
  3. לשמור את התמונה החדשה בפורמט PNG בתיקיה מסויימת

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

public class ImageProcessing
{
  public void ProcessImage(Uri imageUrl, IDownloader downloader, IImageResizer imageResizer, IFileSystem fileSystem)
  {
    byte[] imageBytes = downloader.Get(imageUrl);
    Image bitmap = Image.FromStream(new MemoryStream(imageBytes));
    Image afterResize = imageResizer.ResizeToMax(bitmap, 100);
    fileSystem.StoreImage(afterResize);
  }
}

public interface IFileSystem
{
  void StoreImage(Image image);
}

public interface IImageResizer
{
  Image ResizeToMax(Image bitmap, int maxWidthOrHeight);
}

public interface IDownloader
{
  byte[] Get(Uri imageUrl);
}

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

למה זה טוב?

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

למה לא לכתוב פונקציות?

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

עדיין לא השתכנעתם?

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

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

וסיומת בפרפאזה על הסיומת הקבועה של משה קפלן: ממשקים לפתח!

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

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

אבטחת מידע בקול גדול

18 ינואר, 2011 4 תגובות

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

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

הלכתי ל"אודות", ושם ראיתי הודעה מעניינת:

אז מה דעתכם, להירשם? :-D

קטגוריות:אבטחת מידע תגיות:

To(Decimal)String

28 דצמבר, 2010 9 תגובות

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

בקיצור: "קליין! מַמֵּש ToString של int חיובי! יש לך 2 דקות, זוז!!"

פתרתי, אלא מה. אומרים לי, אני עושה.

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

אז בואו נראה איך אפשר להפוך קוד "כללי" לקוד תלוי משימה ספציפית.

ToDecimalString – גירסה ראשונה

בואו ניזכר רגע בדרישות: נתון int שהוא חיובי, ועלינו להמיר אותו ל string מבלי להיעזר במתודת ToString שמגיעה באופן מובנה עם הפריימוורק.

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

public class V1
{
  public static string ToDecimalString(int value)
  {
    if (value == 0)
      return "0";
    var list = new List<char>(1);
    while (value != 0)
    {
      var digitAsInt = value%10;
      var digitAsChar = (char) ('0' + digitAsInt);
      list.Add(digitAsChar);
      value /= 10;
    }
    list.Reverse();
     
    return new string(list.ToArray());
  }
}

אוקיי, עכשיו בואו נבדוק מה הולך כאן: מכיוון שאנחנו לא יודעים מראש מה אורכו של המספר, אנחנו משתמשים ב List של תוים, שמאפשר לנו בכל פעם מחדש להוסיף char למערך פנימי משלו. בכל איטרציה אנחנו בודקים מהי השארית של חלוקה ב 10 (זו פעולת המודולו), ואח"כ מחלקים את המספר ב 10 (חלוקת int ב int היא ללא שארית). ממשיכים כך עד שמה שנשאר מכל החלוקות האלו הוא אפס (0).

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

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

טוב ויפה, הקוד עובד ותקין. אלא שמבחינת ביצועים, אפעס, הקוד לא משהו. למה?

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

קצת מספרים: ה ToString הסטנדרטי לוקח בממוצע 7 טיקים (Ticks) והמימוש שיש כאן עכשיו לוקח 12 (!) טיקים. זה הפרש של עשרות אחוזים, כך שיש לנו עוד עבודה לעשות.

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

ToDecimalString – גירסה שניה

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

כאן באה לעזרתנו האלגברה. זוכרים את פונקציית ה Log? זאת של המתמטיקה, לא של log4net! אה, כן, Log, ויש גם ln ויש איזה e שקיים שם. טוב, בלי שאני אסחף כאן, בדוט נט יש את הפונקציה Log10 מובנית בפריימוורק. התוצאה שלה היא מספר לא שלם (double), ואם נעגל את המספר שיצא כלפי מעלה, נקבל את מספר הספרות שהמספר הנתון יצטרך בייצוג העשרוני שלו.

כלומר, הביטוי הבא:

var digitCount = (int)Math.Ceiling(Math.Log10(value));

יתן לנו את מספר הספרות הנדרשות.

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

public static string ToDecimalString(int value)
{
  if (value == 0)
    return "0";
  var log10 = Math.Log10(value);
  var digitCount = (int) Math.Ceiling(log10);
  //fix if needed for the values of 10^n (10, 100, etc.)
  if (digitCount == (int)log10)
    ++digitCount;
  var chars = new char[digitCount];
  for (var k = chars.Length - 1; k >= 0 ; k)
  {
    var mod = value%10;
    chars[k] = (char) ('0' + mod);
    value /= 10;
  }
  return new string(chars);
}

[לצערי הביטוי "מינוס מינוס" מומר אוטומטית ב WordPress ל "–"]
שימו לב שכבר תוך כדי הלולאה, סדר הכתיבה למערך התוים הוא הפוך, כדי שלא נצטרך להפוך אותו אח"כ.

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

ToDecimalString – גירסה שלישית

אני מסתכל על הקוד, והקוד מסתכל עלי.

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

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

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

0-9

10-99

100-999

וכו'.

למעשה, נוכל לבנות מערך עם הערכים הבאים: 1, 10, 100 וכו' (המערך יהיה ממויין). כדי לדעת כמה ספרות יש במספר הנתון, נחפש אותו במערך בחיפוש בינארי. יש בדוט נט פונקציה מובנית לחיפוש בינארי, שלא סתם אומרת "מצאתי" או "לא מצאתי". בגדול, אפשר לדעת מהו האינדקס של הערך הקרוב ביותר למה שאנחנו מחפשים, גם אם לא נמצא. אני לא נכנס כאן לפרטים של המתודה Array.BinarySearch, מי שרוצה שיבדוק ב MSDN. אבל הנה הקוד:

private static int[] powersOf10 = new int[]
                                        {
                                            1, 10, 100, 1000, 10000, 100000, 1000000,
                                            10000000, 100000000, 1000000000
                                        };
public static string ToDecimalString(int value)
{
    if (value == 0)
        return "0";
    var index = Array.BinarySearch(powersOf10, value);
    if (index < 0)
        index = ~index; // not found, get closest index
    else
        ++index;

    var digitCount = index;
    var chars = new char[digitCount];
    for (var k = chars.Length - 1; k >= 0; k)
    {
        var mod = value % 10;
        chars[k] = (char) ('0' + mod);
        value /= 10;
    }
    return new string(chars);
}

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

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

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

ToDecimalString – גירסה רביעית

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

איך?

נוכל לדעת מהי הסיבית שהיא ה MSB, שזה קיצור של Most Significant Bit. זוהי הסיבית שמבטאת פחות או יותר את המשפט הבא:

אם נתון לנו מספר x, כמה ספרות נדרשות לייצוג בינארי של x?

למשל, אם x=123, אז הייצוג הבינארי שלו הוא 1111011, ונדרשנו ל 7 ספרות בינאריות. כלומר MSB של 123 זה 7.

באותו אופן, אם x = 9 אז ה MSB שלו הוא 4, כי 9 בייצוג בינארי הוא 1001.

ולמה אני כותב את כל זה? כי למצוא MSB של מספר זה תהליך יחסית מהיר.

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

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

אני לא נכנס כאן לתיאור מלא של הקוד, תצטרכו לסמוך עלי שזה עובד:

public class V4
{
  private static readonly int[] powersOf10 = new int[]
                      {
                        1, 10, 100, 1000, 10000, 100000, 1000000,
                        10000000, 100000000, 1000000000
                      };

  private static readonly int[] powersOf2 = new int[]
                      {
                        1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024,
                        2048, 4096, 8192, 16384, 32768, 65536, 131072,
                        262144, 524288, 1048576, 2097152, 4194304,
                        8388608, 16777216, 33554432, 67108864,
                        134217728, 268435456, 536870912, 1073741824
                      };

  private static int[] decimalDigitsByMsb;

  static V4()
  {
    decimalDigitsByMsb = new int[powersOf2.Length];
    for (int k = 1; k < powersOf2.Length; k++)
    {
      decimalDigitsByMsb[k] = powersOf2[k - 1].ToString().Length;
    }
  }

  public static string ToDecimalString(int value)
  {
    var mostSignificantBit = 0;
    for (int k = powersOf2.Length - 1; k >= 0; k)
    {
      var bit = (value & powersOf2[k]) != 0;
      if (bit)
      {
        mostSignificantBit = k + 1;
        break;
      }
    }
    if (mostSignificantBit == 0)
      return "0";

    int index;
    if (mostSignificantBit >= decimalDigitsByMsb.Length)
      index = decimalDigitsByMsb[decimalDigitsByMsb.Length - 1];
    else
      index = decimalDigitsByMsb[mostSignificantBit];
    if (value >= powersOf10[index])
      index++;
    var digitCount = index;
    var chars = new char[digitCount];
    for (var k = chars.Length - 1; k >= 0; k)
    {
      var mod = value%10;
      chars[k] = (char) ('0' + mod);
      value /= 10;
    }
    return new string(chars);
  }
}

והביצועים?

סוף-סוף, המדידות מראות באופן מובהק יתרון קל לגירסה הרביעית: הגענו לממוצע של 6 טיקים! שיפור של 14% בממוצע!

זה לא נגמר

יש עוד כמה שיפורים שאפשר להכניס לכל גירסה:

  • תנאי הלולאה בלולאות for לא חייב להיות "גדול מ-", מספיק לראות אם "שונה מ-".
    מכאן והלאה הקוד יקבל את השידרוג הזה.
  • במקום לחשב בכל פעם מחדש את הביטוי '0' + mod, נוכל להשתמש (שוב) בטבלת מיפוי.

כלומר נוכל לכתוב פונקציה סטאטית באופן הבא:

private static char[] digits = new[]
  {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };

public static char ToChar(int digit)
{
    return digits[digit];
}

אלא שזה יכול קצת לקלקל לנו אם נשאיר את הפונקציה הזו כמו שהיא. למה?

כי עצם הקריאה והביצוע של פונקציה נוספת על ה stack יוצר overhead די רציני. בשפות קרובות יותר למעבד, כמו C, אפשר להנחות את הקומפיילר שיכניס את הפונקציה כמו שהיא לתוך הקוד שקורא לה. זה נקרא inline. זה נותן יתרון בביצועים, לצד תחזוקתיות גבוהה. מצד שני, זה יכול להגדיל קצת את גודל ה executable כולו. בכל מקרה, ב C# אין לנו את הלוקסוס הזה (וטוב שכך. רק למה השאירו לנו את ה volatile?). אז נצטרך לעקוף את זה ע"י הטמעת הקוד של הפונקציה ב class באופן ישיר.
גם את השידרוג הזה נוסיף לגירסאות השונות של המימוש שלנו.

נו, ומה עוד?

ToDecimalString – גירסה חמישית

כן, תמיד יש עוד. והפעם – פעולת החילוק.

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

ואנחנו צריכים למשימה שלנו רק חילוק ב 10.

אז אולי יש איזה אלגוריתם של חילוק שלמים ב 10 בלבד?

חיפוש קצר העלה את הקוד הבא (מתוך stackoverflow, והנה לינק למקור):

public static int div10(int n)
{
    int q, r;
    q = (n >> 1) + (n >> 2);
    q = q + (q >> 4);
    q = q + (q >> 8);
    q = q + (q >> 16);
    q = q >> 3;
    r = n - q * 10;
    return q + ((r + 6) >> 4);
}

בדקתי את הקוד הזה על כל מרחב ה int32 החיובי. עובד.

ועכשיו נכניס אותו פנימה, כ inline כמובן:

public static string ToDecimalString(int value)
{
  // …
  // old code remains the same
  // …
  var chars = new char[digitCount];
  for (var k = chars.Length - 1; k != (-1); k)
  {
    // inline the div10 operation
    int q, r;
    q = (value >> 1) + (value >> 2);
    q = q + (q >> 4);
    q = q + (q >> 8);
    q = q + (q >> 16);
    q = q >> 3;
    r = value - q * 10;
    var valueDiv10 = q + ((r + 6) >> 4);
    var mod = value - (valueDiv10 * 10);
    chars[k] = digits[mod];
    value = valueDiv10;
  }
  return new string(chars);
}

והצלחנו להרוויח עוד קצת: קבלנו ממוצע של 5 טיקים! שיפור של 28% בממוצע!

התוצאות עד כה

הגענו בינתיים לחמש גירסאות שונות, והנה הגרף המתבקש:

העמודה משמאל – מייצגת את ה ToString הסטנדרטי. העמודות שמימינה מייצגות את המימושים שמוצגים כאן.

יש עוד! כנסו כנסו!

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

  • הקוד כאן בונה את הטקסט המתקבל ספרה אחרי ספרה, מה שגורם גם להעתקות זכרון של תו אחד בכל פעם. כלומר הזכרון משוכפל בכל איטרציה בגודל של תו אחד. אפשר, למשל, לבצע העתקת זכרון של יותר מתו אחד בכל פעם. נניח בזוגות של תוים. או ברביעיות של תוים. סביר להניח שזה ישפר במשהו את הביצועים, כי אז תתבצע העתקה של מספר תוים בבת אחת. סביר להניח שזה גם יסבך קצת את הקוד. וגם יגדיל את צריכת הזכרון הסטאטי.
  • ברמת המתמטיקה, אפשר לעשות קצת אנליזה נומרית, ולמצוא פולינום "קל" יותר מהמימוש של Log10, שיהיה קירוב מספיק טוב לדרישות שלנו מ Log10, אבל יהיה יותר קל לביצוע.
  • אפשר להקצות מראש מערך באורך 10 תוים, שזה המקסימום שיכול להיות כאן (כי int.MaxValue זה 2147483647, וזה לוקח 10 ספרות. לא יכול להיות יותר מזה). וכך "ניצלנו" מכל העניין של לדעת מראש כמה ספרות יש למספר הנתון. מצד שני, נצטרך להפוך את הסדר של התוים במערך, כך שנוצרה לנו עוד קצת עבודה.

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

רגע, מה לגבי שלמים שליליים?

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

סיכום

בפוסט הזה הראיתי איך משימה יחסית פשוטה יכולה להיות מורכבת ככל שרוצים להפיק ממנה ביצועים טובים יותר. הראיתי איך פעולות יחסית כלליות יכולות להיות ספציפיות למשימה ואיך אפשר להפוך את זה ליתרון בביצועים. בנוסף הראיתי איך הידיעה של "איך הדברים עובדים מתחת למנוע" יכולה להיות מומרת לביצועים. הדוגמה כאן היתה ToString שמגיע מובנה בפריימוורק, שמזכיר קצת את פונקציית itoa משפת C. רוב השימושים שהיו לי אי פעם ב ToString של int היו פשוט להציג את המספר בייצוג העשרוני שלו. הפריימוורק מציע הרבה יותר מזה: אפשר להשתמש ב Formatter אחר, נניח כזה שגם מציג מפריד אלפים. המעבר מקוד כללי לקוד ספציפי, שיציג את ה int בצורה הפשוטה ביותר, הוא מה שמאפשר להגיע לביצועים טובים יותר במימושים השונים שהצעתי כאן.

המעבר מהכללי לספציפי מביא אותי למסקנה הבאה: אפשר לבצע אופטימיזציות ברמת הקוד. חשוב רק לדעת לא לבצע אותן מוקדם מדי (Premature Optimization) וגם לדעת לא לעבור את הגבול בין קוד קריא ובר תחזוקה לקוד שעובד טיפה'לה יותר מהר. ברוב המקרים השיקולים יהיו לטובת תחזוקתיות גבוהה לעומת שיפורים של אחוזים בודדים בביצועים.

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

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

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

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

Parallels and Internals Shminternals

25 אוקטובר, 2010 6 תגובות

הלכתי לאחרונה למפגש במיקרוסופט רעננה בנושא Parallel Programming in Visual Studio 2010. המרצה היה סשה גולדשטיין. אז קודם כל קצת מחמאות: למיקרוסופט על האירועים האלה שמאורגנים היטב, מהחניה ועד המשובים; לסשה גולדשטיין על הרצאה ברמה גבוהה, בשפה ברורה ועם הרבה סבלנות.

ועכשיו לעניינים הטכניים עצמם.

נעים להכיר – System.Threading.Tasks

נתחיל מהקדמונת: החל מגירסה 4.0 של הדוט נט פריימוורק, מיקרוסופט הכניסו פנימה פיצ'רים חדשים בתחום ה Multi-Threading. הפיצ'רים האלה מאפשרים פיתוח נוח יותר ל Multi-Threading, ולמעשה כדאי יותר לקרוא לזה מיקבול משימות, מתוך הנחת יסוד שמשימה ו Thread זה לא אותו הדבר, למרות שיש קשר הדוק. את הפיצ'רים והמחלקות למיניהם אפשר למצוא ב namespace שנקרא System.Threading.Tasks. הרעיון הכללי של הפיצ'רים האלה הוא שהמפתחים לא יטריחו את עצמם יותר מדי סביב הפרטים הקטנים של המיקבול עצמו, ויוכלו למקבל משימות בקלות יחסית.

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

namespace Bronze.Lib
{
  public class SimpleAggregator
  {
    public int Sum(int[,] matrix)
    {
      var result = 0;
      for (var i = 0; i < matrix.GetLength(0); i++)
        for (var j = 0; j < matrix.GetLength(1); j++)
          result += matrix[i, j];
      return result;
    }
  }
}

פשוט וקל.

מקבילית המוחות

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

עכשיו, יש שתי גישות עיקריות לביצוע של הפתרון הזה:

1

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

הנה קצת פסודו-קוד לעניין זה:

1. common_sum = 0
2. Parallel: for each row in matrix:
  2.1 temp = sum(row)
  2.2 append temp to common_sum

הנקודה הכואבת יחסית בקוד הזה היא סעיף 2.2, שבו כל משימה מעדכנת את המשאב המשותף. מכיוון שזו סביבה של ריבוי משימות וככל הנראה גם ריבוי threads, הרי שיש לנו משאב משותף שיכול להתעדכן ע"י מספר threads במקביל. מקור לא אכזב ל data corruption. כדי להתגבר על זה, צריך לכתוב קוד של פעולת העדכון בצורה שתהיה thread-safe. ניתן להשיג את זה ע"י lock, ובמקרה הספציפי הזה, נוכל להשתמש גם ב Interlocked שיכול בפעולה אטומית להוסיף ערך למשתנה int.

בואו נראה את הקוד שמבצע את הגישה הראשונה, עם Interlocked:

using System.Threading;
using System.Threading.Tasks;

namespace Bronze.Lib
{
  public class ParallelWithInterlock
  {
    public int Sum(int[,] matrix)
    {
      var outerUpperBound = matrix.GetLength(0);
      var commonSum = 0;

      var innerUpperBound = matrix.GetLength(1);
      Parallel.For(0, outerUpperBound, i =>
      {
        var localSum = 0;
        for (var j = 0; j < innerUpperBound; j++)
          localSum += matrix[i, j];
        Interlocked.Add(ref commonSum, localSum);
      });

      return commonSum;
    }
  }
}

2

במקום להגדיר משתנה משותף ולהגן עליו באמצעי סנכרון שונים, נוכל להגדיר מערך שבו לכל משימה מוקצה תא משלה. הגישה לאיברי המערך היא thread-safe לפי הגדרה (אגב, יצא לי לחפור על זה פעם ב stackoverflow), ולכן כל משימה יכולה לעדכן את התא "שלה" במערך הנ"ל, מבלי לחשוש מ data corruption. בסוף התהליך סוכמים את כל איברי המערך, ובא לציון גואל. שימו לב, עיקר התחכום כאן הוא שהפקודה Parallel.For יוצרת משתנה אינדקס, ושולחת אותו כפרמטר ל delegate של המשימה המבצעת, וזה בדיוק האינדקס של התא ה"פרטי" של המשימה במערך המשותף. הנה הקוד שממחיש את זה:

using System.Linq;
using System.Threading.Tasks;

namespace Bronze.Lib
{
  public class ParallelAggregator
  {
    public int Sum(int[,] matrix)
    {
      var outerUpperBound = matrix.GetLength(0);
      var partialSums = new int[outerUpperBound];

      var innerUpperBound = matrix.GetLength(1);
      Parallel.For(0, outerUpperBound, i =>
      {
        partialSums[i] = 0;
        for (var j = 0; j < innerUpperBound; j++)
          partialSums[i] += matrix[i, j];
      });

      var result = partialSums.Sum();
      return result;
    }
  }
}

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

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

using System.Linq;
using System.Threading.Tasks;

namespace Bronze.Lib
{
  public class ParallelAggregatorWithLocalSum
  {
    public int Sum(int[,] matrix)
    {
      var outerUpperBound = matrix.GetLength(0);
      var partialSums = new int[outerUpperBound];

      var innerUpperBound = matrix.GetLength(1);
      Parallel.For(0, outerUpperBound, i =>
      {
        var localSum = 0;
        for (var j = 0; j < innerUpperBound; j++)
          localSum += matrix[i, j];
        partialSums[i] = localSum;
      });

      var result = partialSums.Sum();
      return result;
    }
  }
}

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

של מי ארוך יותר?

כתבתי תוכנית קטנה שמשווה את הביצועים, ויש יתרון מובהק בביצועים של ParallelAggregatorWithLocalSum (בוצע ב Release על הלפטופ שלי). היתרון אצלי במחשב הוא של 40%, כלומר המחלקה ParallelAggregatorWithLocalSum רצה ב 40% בממוצע מהר יותר מהמחלקה ParallelAggregator. לא יאמן. באותה תוכנית ניתן גם לראות שהביצועים של ParallelAggregatorWithLocalSum ושל ParallelWithInterlock ממש קרובים אחד לשני (לפעמים האחד מהיר יותר באחוז או שניים ולפעמים השני). את הקוד המלא אפשר להוריד מכאן. הנה גרף שממחיש את התוצאות של סכימת מטריצה עם 99 שורות ו 31841 עמודות:

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

סיכום ומסקנות

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

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

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

ומסקנה נוספת: כל מי שחשב שדוט נט זו סביבה שבה אין צורך לדעת את ה internals – צודק רק ברוב המקרים. בחלק קטן (אבל משמעותי) מהמקרים כדאי מאוד לדעת מה קורה under-the-hood, רק כדי להפיק יותר מהסביבה הזו. יש עוד הרבה דוגמאות שממחישות כמה טוב לדעת את ה internals, והפוסט הזה מציג רק דוגמה אחת, כמובן. עם זאת, התחושה האישית שלי היא שבמיקרוסופט ניסו לכוון לשפה שהיא יותר high-level ממה שהתקבל בסופו של דבר. במילים אחרות, הציפיה של המפתחים שרק מגיעים לדוט נט היא שהם לא יצטרכו להתעסק עם tweakים כדי לכתוב קוד יעיל, ובסוף הם נאלצים להיכנס לפינות שהם לא חלמו עליהם, ושמרחיקות אותם מכתיבת קוד "נטו".
במילים פשוטות ובוטות יותר: פייר, התאכזבתי :-)

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

מיקבול נעים!

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

Looking for my next great job

3 אוגוסט, 2010 אין תגובות

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

ב Xsights הייתי כבר משלב ההקמה. יחד עם ארנון רותם-גל-עוז (כן, השלישיה הזו זה שם המשפחה שלו) בתפקיד VP R&D בנינו מערכת מדהימה בצד השרת, עם דגש על scalability, availability ועוד כמה ilities שאפשר להוסיף.

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

אז כמו שאומרים נושאי המגבעת: "תודה לעולם ותודה לכולם".

כיום אני מחפש עבודה כראש צוות. אם תרצו, הנה ה CV שלי (RTF), ויש גם פרופיל בלינקדין. אם מישהו שומע על סטארטאפ בהקמה – תנו לי פינג, אני אשמח לבוא ולהיות חלק מהאתגר הבא!
המייל שלי הוא solution-at-ronklein-dot-co-dot-il

סבבה? סבבה!

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

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

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

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