Design Patterns,  SOLID

Decorator

בפוסט זה אדגים שני שימושים נפוצים בתבנית העיצוב Decorator.

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

אפשרות זו מונעת ירושה מיותרת או פותרת מקרים בהם אין אפשרות לירושה בכלל (Sealed).

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

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

ה Decorator מורכב מארבעה חלקים עיקריים (אפשר לצמצם את חלקם בהתאם למקרה הספציפי):

א. החלק הראשון הוא ה Component – זהו abstract class או Interface שאת אחד או כמה ממימושיו נרצה לשנות.

ב. החלק השני הוא ה Concrete Component – זהו Class (אחד או כמה) שיורשים או מממשים את ה Component.

ג. החלק השלישי הוא ה Decorator – זהו abstract class שגם הוא כמו ה Concrete Component יורש מה Component . המאפיין את ה Decorator הוא שיש לו Protected Member מהסוג של הComponent ושב Constructor שלו הוא מקבל מופע של Component כלשהו ומאתחל את ה Member (תיכף תבוא הדוגמא).

ד. החלק הרביעי הוא Concrete Decorator – Class (אחד או כמה) אשר יורשים מה Decorator, מוסיפים פונקציונליות משל עצמם וכמובן מממשים את הפונקציונליות הבסיסית, וזאת על ידי הפעלת הפונקציונליות של המופע שהתקבל בConstructor.

דוגמא א

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

כרגע המצב הוא כזה – בתוכנה שלהם יש שלושה קלאסים: חומוס פרטי, חומוס זוגי, וחומוס משפחתי – שלושתם יורשים מקלאס “חומוס” כמובן ושלושתם ממשים GetDescription ו CalculateCost.

הקוד נראה כך:

public abstract class Humus
{
   public string Description { get; set; }
   public abstract string GetDescriotion();
   public abstract double CalculateCost();
}

public class PrivateHumus : Humus
{
  public PrivateHumus()
  {
     Description = "Private Humus";
  }
  public override string GetDescriotion()
  {
      return Description;
  }

  public override double CalculateCost()
  {
     return 18.00;
 }
}

public class DualHumus : Humus
{
   public DualHumus()
   {
     Description = "Dual Humus";
   }
   public override string GetDescriotion()
   {
    return Description;
   }
   public override double CalculateCost()
   {
      return 30.00;
   }
}

public class FamilyHumus : Humus
{
   public FamilyHumus()
   {
      Description = "Family Humus";
   }
   public override string GetDescriotion()
   {
       return Description;
   }

   public override double CalculateCost()
   {
       return 45.00;
   }
}

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

אם כך מה צריך לשנות בתוכנה?

אם נתחיל להוסיף קלאס לכל תוספת בנפרד וזה בהתאמה לגודל המנה, זה יראה כך FamilyHumusWithTehini, DualHumusWithTehini, PrivateHumusWithTehini וכו’ וכו’… וכך הלאה לכל תוספת בנפרד.

ומה אם אני רוצה לשלב (מנה משולשת כזכור)?

ומה אם מחר הם ירצו להוסיף גם פול?

תהיה לנו התפוצצות קלאסים וקוד ספגטי.

אם כך נממש Decorator.

השלב הראשון הוא ליצור Abstract Class שיורש מ Humus




public abstract class HumusDecorator : Humus

כעת ניצור שדה Protected של Humus




protected  Humus Humus;

ניצור Constructor שמקבל Humus ונאתחל את השדה שלנו.

protected HumusDecorator(Humus humus)
{
   Humus = humus;
}

כעת נממש את הפונקציות מהקלאס הראשי – Humus , אבל למעשה המימוש יהיה רק בהפעלת הפונקציות של המופע של Humus שקיבלנו.

ה Class בשלמותו נראה כך:

public abstract class HumusDecorator : Humus
{
     protected  Humus Humus;
	 
     protected HumusDecorator(Humus humus)
     {
         Humus = humus;
     }

     public override string GetDescriotion()
     {
         return Humus.GetDescriotion();
     }

     public override double CalculateCost()
     {
         return Humus.CalculateCost();
     }
}

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

ההבדל ביניהם יהיה במימוש של הפונקציות.

הקוד יראה כך (אין לי אחריות על המחירים):

 public class Granules : HumusDecorator
{
    public Granules(Humus humus) : base(humus)
    {
        Description = "Granules";
    }

    public override string GetDescriotion()
    {
       return string.Format("{0}, {1}", Humus.GetDescriotion(), Description);
    }

    public override double CalculateCost()
    {
        return Humus.CalculateCost() + 3.00;
    }
}

public class Tehina : HumusDecorator
{

    public Tehina(Humus humus)
        : base(humus)
    {
        Description = "Tehina";
    }

    public override string GetDescriotion()
    {
        return string.Format("{0}, {1}", Humus.GetDescriotion(), Description);
    }

    public override double CalculateCost()
    {
        return Humus.CalculateCost() + 2.00;
    }
}

public class Msabbaha : HumusDecorator
{
    public Msabbaha(Humus humus)
        : base(humus)
    {
        Description = "Msabbaha";
    }

    public override string GetDescriotion()
    {
        return string.Format("{0}, {1}", Humus.GetDescriotion(), Description);
    }

    public override double CalculateCost()
    {
        return Humus.CalculateCost() + 4.00;
    }

}

תכלס זהו.

אז איך משתמשים בזה?

ניצור מופע של הHumus הרצוי לנו (נגיד PrivateHumes) אך הוא יהיה כללי (כלומר מ type Humus)

ולאחר מכן ניצור מופע חדש של התוספת הרצויה לנו המקבל ב Constructor את ה PrivateHumus שיצרנו והמופע יחליף את MyHumus.

כעת נוכל להדפיס מה שנרצה.

הקוד יראה כך:

Humus myHumus = new PrivateHumus();

myHumus = new Granules(myHumus);

Console.WriteLine(myHumus.GetDescriotion());

Console.WriteLine(myHumus.CalculateCost());

התוצאה כמובן תהיה כמצופה:

דוגמא ב’ – Sealed Class

נניח שיש לי תוכנה שיש לי Sealed Class שמממש איזשהו Interface.

הקוד נראה כך:

public interface IDbHandler
{
   void HandelDb(string sql);
}

public sealed class SqlHandler : IDbHandler
{
   public void HandelDb(string sql)
   {
        //Do Some Work
    }
}

והשימוש בקוד נראה כך:

public void Main()
{
    var db = new SqlHandler();
    DoSomething(db);
}

public void DoSomething(IDbHandler db)
{
    db.HandelDb("query");
}

נשים לב שהמתודה DoSomthing לא מקבלת דווקא את הקלאס הספציפי שלנו אלא מימוש של הInterface.

ובכן, גילינו שיש לנו דליפת זיכרון בתוכנה.

השתמשנו באיזשהו Profiler וגילינו שהבעיה היא בקלאס שלנו – SqlHandler.

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

ניצור Decorator באופן הזה:

ניצור Class חדש אשר אף הוא מממש את IDbHandler ונדאג שיהיה לו שדה Protected מסוג IDbHandler וששדה זה מאותחל ב Constructor .

במתודה HandelDb נעשה שני דברים

א. התוספת שלנו – הדפסת ה sql.

ב. הפעלת המתודה המקורית מתוך הקלאס המקורי.

הקוד יראה כך:

public class SqlDecorator : IDbHandler
{
    protected IDbHandler DbHandler { get; set; }
    public SqlDecorator(IDbHandler db)
    {
        DbHandler = db;
    }

    public void HandelDb(string sql)
    {
        Console.WriteLine(sql);
        DbHandler.HandelDb(sql);
    }
}

כעת השימוש בקוד יהיה כמעט אותו דבר ויראה כך –

public void Main()
{
    IDbHandler db = new SqlHandler();
    var decorator = new SqlDecorator(db);

    DoSomething(decorator);
}

public void DoSomething(IDbHandler db)
{
    db.HandelDb("query");
}

נרוויח הדפסה של כל sql שקיבלנו מבלי להפריע לעבודה הסדירה של התוכנה על פי המהלך המקורי.

  • טיפ: במקרה כזה ובדומים לו ישנה דרך נוספת להוסיף פונקציונליות לקלאס קיים וזה על ידי Extension Methods