דף הבית > תכנות > Don't enum Data

Don't enum Data

מכירים את enum?

-מכירים.

אז מתי כדאי להשתמש בו וליצור enum בקוד שלנו?

-הממממם… כשיש כמה קבועים מאותה "קבוצה".

ופיסת הקוד הזו:

public class Util
{
  public static void SaveAs(Image image, ImageFormat format, string path)
  {
    switch (format)
    {
      case ImageFormat.Bitmap:
        SaveAsBitmap(image, path);
        break;
      case ImageFormat.Jpeg:
        SaveAsJpeg(image, path);
        break;
      case ImageFormat.Png:
        SaveAsPng(image, path);
        break;
      default:
        throw new ApplicationException(
          string.Format("enum '{0}' changed", typeof (ImageFormat).FullName)
          );
    }
  }

  // implementations here...
}

נראית הגיונית?

-כן. נראה בסדר גמור.

טעות. הקוד הזה מפר את עקרון OCP, קיצור של Open/closed principle.

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

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

using System;
namespace System.Data
{
  public enum ConnectionState
  {
    Closed = 0,
    Open = 1,
    Connecting = 2,
    Executing = 4,
    Fetching = 8,
    Broken = 16
  }
}

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

ועכשיו נחזור לדוגמה הראשונה. אם בעוד כמה חודשים נצטרך לתמוך בשמירת האובייקט Image לפורמט אחר, נניח לפורמט WebP, אז נצטרך להוסיף עוד ערך לקבוצת הערכים של ה enum שלנו. ולקמפל מחדש. ולבדוק בכל מיני מקומות מה ההשפעה של זה. וכו' וכו'. כמה שינויים בקוד בשביל להוסיף תמיכה בפורמט נוסף!

למה זה קורה?

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

בקיצור, יצרנו enum סביב data, וזו הטעות.

זאת טעות נפוצה מאוד. גם אני הייתי שם, בעיקר בגלל שזה נתן לי תחושה של type safety – יש לי השלמה אוטומטית של הויז'ואל סטודיו, יש Resharper שיכול לצעוק עלי שאני לא בודק את כל הערכים האפשריים – משמע אני לא יכול לטעות. זה אולי נכון במיקרו, אבל מרוב תחושה של "הנה אני כותב קוד עם מינימום אפשרות לטעות" – לא ראיתי את הטעות במאקרו.

איך בכל זאת כדאי לעצב ולכתוב קוד עם רעיון דומה?

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

בדוגמה שלנו, הממשק הוא כזה ששומר תמונה לדיסק:

using System.Drawing;

namespace Silicate.Lib
{
  public interface IImagePersister
  {
    void SaveAs(Image image, string path);
  }
}

וכל מימוש יוכל לכתוב את הקובץ בפורמט אחר. ניקח לדוגמה מימוש של png:

using System.Drawing;

namespace Silicate.Lib
{
  public class PngPersister : IImagePersister
  {
    public void SaveAs(Image image, string path)
    {
      image.Save(path, System.Drawing.Imaging.ImageFormat.Png);
    }
  }
}

ובאותו אופן נוכל לממש עבור כל פורמט שנרצה, כולל WebP, שלא נתמך בפריימוורק.

אוקיי, אבל איך הכל מתחבר ביחד?

הרעיון המרכזי הוא לוותר על ה switch, כי אז הוספה (או גריעה) של פורמט לשמירת תמונה תגרום לקימפול מחדש. במקום ערכים של enum נעבוד עם string, שיכול להחזיק ערכים בצורה גמישה יותר. האלטרנטיבה ל switch היא לטעון באופן דינמי את ה plugin הרלוונטי ולהשתמש בו. טעינה דינמית שכזו יכולה להתבצע ע"י Reflection, או ע"י IoC Container (ובקיצור IoCC). אני אראה בפוסט הזה את הדרך של IoCC. אם מישהו יבקש (נניח בהערות לפוסט) – אז אני אוסיף גם קוד שמדגים ע"י Reflection.

נסכם את המהלך בטבלה קטנה ואז נמשיך:

לפני אחרי
סוג המזהה ערך מתוך enum string
סוג הטעינה סטטית ע"י switch + new דינמית ע"י Reflection או IoCC
הרחבה גורמת ל… שינויים בקוד + rebuild 🙁 שינויים בקונפיגורציה בלבד 🙂

נחבר את הכל עם Unity

ה IoCC שאדגים באמצעותו את השינויים בקוד הוא Unity. קודם כל – הקונפיגורציה:

<unity>
  <namespace name="Silicate.Lib" />
  <assembly name="Silicate.Lib" />
  <container>
    <register type="IImagePersister" mapTo="BitmapPersister" name="bitmap" />
    <register type="IImagePersister" mapTo="JpegPersister" name="jpeg" />
    <register type="IImagePersister" mapTo="PngPersister" name="png" />      
  </container>
</unity>

שימו לב שיש יותר ממימוש אחד לממשק IImagePersister, ולכן יש גם התייחסות לשם (או מזהה) של המימוש.

מרגע שהכל מקונפג כמו שצריך, נוכל לוותר על ה switch ולכתוב את הקוד באופן הבא:

public class Util
{
  private static readonly UnityContainer Container = BuildContainer();

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

    return container;
  }

  public static void SaveAs(Image image, string format, string path)
  {
    var persister = Container.Resolve<IImagePersister>(format.ToLowerInvariant());
    persister.SaveAs(image, path);
  }
}

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

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

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

עניין נוסף – מי שמבצע את הטעינה (בפועל) של המחלקה הרלוונטית הוא ה Unity. העברנו אליו את האחריות לבצע את זה. [אנחנו, ראש קטן אנחנו, כמה שפחות אחריות – יותר טוב ;-)]

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

יש מספר יתרונות וחסרונות בצורה שבה הקוד מעוצב כרגע. נתחיל מהחסרונות:

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

ונעבור ליתרונות:

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

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

ולשאלה המתבקשת…

השאלה המתבקשת שעולה עכשיו, והיא שאלת מיליון הדולר: מתי כדאי להעדיף את הצורה הראשונה (enum+switch) ומתי נעדיף את הצורה השניה?

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

תלוי כמה הפרויקט גדול.

תלוי כמה מפתחים בוחשים בקדרה הזו.

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

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

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

תכנות נעים!

קבצי הקוד: גירסה ראשונה – עם enum וגירסה שניה – בצורה של plugins.

[התמונה מתוך אתר flickr, שם משתמש tj scenes, לינק ישיר לתמונה, תחת רשיון CC BY 2.0]

קטגוריות:תכנות תגיות:, , ,
  1. 16 אוקטובר, 2011 מתוך 16:37 | #1

    אני לא כל כך רואה את הבעיה בדוגמא שנתת היות וגם בקוד החדש שלך מי שקורא לפונקציה אמור להכיר את הtype ולכן לעשות איתו משהו ולכן להשתנות כאשר נוספים סוגים ולכן אין כנראה בעיה בקומפילציה שלו
    בנוסף נראה לי שניתן ליצר מצב שבו הenum והsave as נטענים דינאמית כplugin ואז צרכנים לא יהיו חייבים להשתנות (למרות שהמימוש של enum ב .net אולי עושה את זה לא נח כל כך – תעבור לscala 🙂 )
    כתיבה מחדש היתה אותי מסתירה את כל הנושא של מה סוג הקובץ מהמשתמשים – נאמר היה עובר איזה אוביקט עוטף קובץ שבו saveAs הוא מימוש פנימי וכן גם הידיעה על הסוג – מה ששוב כנראה לא מהווה בעיה לשימוש בenum מפני שכל תמיכה בפורמט חדש היתה מצריכה שינוי באוביקט הזה בהרבה מקומות

    ארנון

  2. 17 אוקטובר, 2011 מתוך 09:54 | #2

    @ארנון
    אני אסביר: הצרכנים הקיימים נשארים כפי שהם (תאימות לשני הכיוונים).
    הרכיב ששומר את התמונה – הוא זה שכל פורמט חדש אצלו יגרום ל rebuild ובדיקות וכו'.
    אם נניח שהרכיב הנ"ל הוא בצד שרת, אז אנחנו בשינוי של {קונפגורציה בלבד + הוספת DLL לתיקיית ההרצה} נוכל להוסיף פורמט חדש בלי rebuild.
    אמנם, הצרכנים ה"חדשים" יצטרכו לדעת את המזהה הטקסטואלי לפורמט החדש, אבל בזה מסתכם השינוי שלהם.

  3. 24 אוקטובר, 2011 מתוך 02:10 | #3

    ברגע שהשיפור הזה דורש בצידו DLL נוסף בתיקיית ההרצה, כבר יצאת מה-scope של נגיעה בקובץ קונפיגורציה בלבד ונכנסת לממלכת קוד המקור והקימפול (אלא אם אנחנו בעולם אוטופי שבו "זמינות לנו על המדף" שלל ספריות מוכנות שבמקרה משתמשות בדיוק באינטרפייסים שלנו…), ואם נכנסת כבר לממלכת קוד המקור והקימפול, תישאר כבר עם enum שנותן לך כפי שציינת strongly-typing יפה וטוב.

    בלי קשר לזה, אם כבר לאמץ IoC, נראה לי פרקטיקה טובה להכניס באפליקציה, בעת עלייתה, קוד שרץ על כל הטיפוסים המוגדרים בקובץ הקונפיגורציה ובודק אם הם אכן קיימים (ב-reflection או ע"י הכלים הקיימים של ה-IoCC), ואם לא – שמיד יזרוק exception ו/או יכבה את האפליקציה, לפי סוג האפליקציה וההקשר. כלומר, שאם נעשתה טעות באחד ה-magic strings שבקובץ הקונפיגורציה, היא תוצף מיד ולא רק אחרי שהקוד רץ ב-production כך שיתפוצץ בפנים של יוזר מסכן.

  4. 24 אוקטובר, 2011 מתוך 07:05 | #4

    @עופר זליג
    אני מבין את הבעייתיות שאתה מעלה לעניין הראשון, בעיקר לגבי הוספה של DLL (כי שינוי קונפיגורציה הוא די מקובל כיום). עם זאת, מרגע שהחלטנו להשתמש ב IoCC, *תמיד* נצטרך לדאוג ש DLLים של ספריות חיצוניות שהן לא referenced בפרויקט עצמו אכן יהיו ב bin שלנו (או בתיקיית ההרצה עצמה, תלוי בסוג הפרויקט). זה חלק מהמשחק עם IoCC, מה לעשות. האם זה נוח יותר או נוח פחות מהאלטרנטיבה של enum+rebuild? זאת שאלה טובה, ואין עליה תשובה חד משמעית.

    מה שאתה מתאר לגבי העניין השני אכן מוטמע בחלק מהפרוייקטים שאני לוקח בהם חלק בתקופה האחרונה: אנחנו מבצעים בדיקות sanity מיד עם עליית האפליקציה, מוודאים שיש DB ושהחיבור נפתח, מוודאים שיש Queue מסויים וכותבים אליו הודעת dummy, וכו'. התוספת שלך של לוודא שכל ה"פלאגינים" שזמינים דרך ה IoCC – אכן עובדים – זה משהו שלא חשבתי עליו, ונראה לי שנטמיע גם את זה עם עליית האפליקציה. תודה על הרעיון!

  5. אלעד כץ
    25 אוקטובר, 2011 מתוך 12:03 | #5

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

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

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

  6. 25 אוקטובר, 2011 מתוך 21:50 | #6

    @אלעד כץ
    תודה על התגובה. ניסיתי להמחיש את הנקודה (של כתיבה נכונה של קוד) על ידי משהו פרקטי כמו המרת תמונה לפורמט כלשהו ושמירה בדיסק. היה לי חשוב לחבר את העקרון לפרקטיקה. אולי אני אחשוב על דוגמאות נוספות (טובות יותר? :-)) בהזדמנות.

  7. מעין ח.
    30 אוקטובר, 2011 מתוך 17:46 | #7

    אחלה פוסט!
    לצערי נפלתי לבור הזה מספר פעמים, ובדיוק כמו שאתה מתאר – זה נתן לי תחושה של type safety.
    בפועל, האבחנה שלך מדוייקת לדעתי – אינומרציה של קבוצה סגורה וסופית לעומת קבוצה שאינה סגורה \ אינה סופית.
    לדעתי אולי קצת סיבכת את הקוראים כששילבת את ה-Unity בדוגמא, אבל לא נורא 🙂

  8. 30 אוקטובר, 2011 מתוך 22:28 | #8

    @מעין ח.
    תודה!
    [וכמו שציינת, אולי החיבור ל Unity לא סגור עד הסוף, אבל שיהיה כבר ככה :-)]

  9. רן יאיר
    21 דצמבר, 2011 מתוך 10:41 | #9

    טוב אף אחד לא שאל אז אני אשאל:
    יש מצב לתת דוגמא עם reflection? 🙂
    תודה מראש!

  10. 21 דצמבר, 2011 מתוך 11:17 | #10

    אני אכתוב פוסט נוסף כדי לתת דוגמה מבוססת Reflection, בכיף.

  11. רועי
    2 ינואר, 2012 מתוך 14:58 | #11

    שלום, תודה על הפוסט המצויין.

    הרעיון הובהר יפה, המימוש שלא פחות הובן לי.
    אשמח להרחבה בנושא ה-IoCC \ reflection. בנוסף, לא ברור השימוש במחלקות ה-unity. מאיפה הן לקוחות?

  12. 2 ינואר, 2012 מתוך 17:53 | #12

    @רועי
    תודה!

    אני מציע בתור התחלה שתוריד את שתי הגרסאות של הפוסט הזה, ו"תשחק" איתן קצת.
    באופן כללי Unity זה IoCC. פרטים נוספים תוכל לקרוא גם בפוסט אחר שלי:
    http://heblog.ronklein.co.il/2011/07/ioc-container-explained

  1. אין הפניות עדיין.

Quantcast