#C,  Design Patterns

תבנית Strategy בשפת #C: האם באמת צריך מחלקה לכל דבר?

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

public decimal CalculateShipping(Order order, ShippingType type)
{
    return type switch
    {
        ShippingType.Regular => 20 + order.WeightKg * 3,
        ShippingType.Express => 40 + order.WeightKg * 5,
        ShippingType.Pickup  => 0,
        _ => throw new ArgumentOutOfRangeException(nameof(type))
    };
}

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

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

לתת לכל דרך חישוב מקום משלה

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

נגדיר ממשק:

public interface IShippingStrategy
{
    decimal Calculate(Order order);
}

כל סוג משלוח יממש את החישוב שלו:

public sealed class RegularShipping : IShippingStrategy
{
    public decimal Calculate(Order order) =>
        20 + order.WeightKg * 3;
}

public sealed class ExpressShipping : IShippingStrategy
{
    public decimal Calculate(Order order) =>
        40 + order.WeightKg * 5;
}

public sealed class PickupShipping : IShippingStrategy
{
    public decimal Calculate(Order order) => 0;
}

המחלקה שמשתמשת בחישוב מקבלת את האסטרטגיה מבחוץ:

public sealed class ShippingCalculator
{
    private readonly IShippingStrategy _strategy;

    public ShippingCalculator(IShippingStrategy strategy)
    {
        _strategy = strategy;
    }

    public decimal Calculate(Order order) =>
        _strategy.Calculate(order);
}

כך נשתמש בה עבור משלוח מהיר:

var order = new Order(2.5m);
var calculator = new ShippingCalculator(new ExpressShipping());

decimal price = calculator.Calculate(order); // 52.5

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

public record Order(decimal WeightKg);

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

ומה אם כל החישוב הוא שורה אחת?

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

public sealed class ShippingCalculator
{
    private readonly Func<Order, decimal> _calculateShipping;

    public ShippingCalculator(Func<Order, decimal> calculateShipping)
    {
        _calculateShipping = calculateShipping;
    }

    public decimal Calculate(Order order) =>
        _calculateShipping(order);
}

ולהעביר לה את החישוב הרצוי:

var order = new Order(2.5m);

var calculator = new ShippingCalculator(
    order => 40 + order.WeightKg * 5);

decimal price = calculator.Calculate(order); // 52.5

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

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

אז מה עדיף?

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

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

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

כתיבת תגובה

האימייל לא יוצג באתר. שדות החובה מסומנים *