Open Close Principle czyli jak zarobić ale się nie narobić.

Wyobraźmy sobie taką sytuację: jest sklep internetowy, podczas składania zamówienia system wylicza rabat – przy zamówieniach 500-1000 zł 5%, powyżej 1000 zł 10%, powyżej 5000 dodatkowo darmowa przesyłka. Brzmi znajomo?

public double CalculateDiscount(IOrder order)
{
    if (order.Total <= 500 && order.Total > 1000)
    {
        return order.Total*0.05;
    }
    if (order.Total > 1000)
    {
        return order.Total*0.1;
    }
    if (order.Total > 5000)
    {
        order.SippingFee = 0;
        return order.Total*0.1;
    }
    return 0;
}

Napisane, przetestowane, zapłacone. Ale teraz klient (zleceniodawca/kierownik/project owner/właściciel tego sklepu) mówi, że chce dodać jeszcze jedną opcję: 30tego listopada darmowa przesyłka dla wszystkich (taki dzień darmowej dostawy). Nic trudnego dodamy trochę kodu:

public double CalculateDiscount(IOrder order)
{
    if (order.Total >= 500 && order.Total < 1000)
    {
        return order.Total*0.05;
    }
    if (order.Total > 1000)
    {
        return order.Total*0.1;
    }
    if (DateTime.UtcNow.Month == 11 && DateTime.UtcNow.Day == 30)
    {
        order.SippingFee = 0;
    }
    if (order.Total > 5000)
    {
        order.SippingFee = 0;
        return order.Total*0.1;
    }
    return 0;
}

I miesiąc później nasz klient chce mieć obsługę kuponów rabatowych:

...
if (order.HasCoupon && CouponIsValid(order.Coupon))
{
    return order.Total*order.Coupon.Discount;
}
...

Kolejny miesiąc i kolejny pomysł – jakaś promocja w zakupach grupowych:

// miejmy nadzieję że wcześniejszy kod pozwoli korzystać z wielu różnych kuponów

…. widać w jakim kierunku to idzie. Kod rośnie szybciej niż ciasto drożdżowe na kaloryferze i równie szybko co ryba na kaloryferze zaczyna pachnieć.

Na takie bolączki przychodzi Open Close Principle. Zamiast małymi kroczkami psuć kod i z każdą iteracją zwiększać złożoność przypadkową , czyli tą złą, możemy zastosować Open Close Principle, która mówi: Klasa powinna być zamknięta na modyfikacje ale jednocześnie otwarta na rozszerzanie.

A gdyby tak nasza metoda wyglądała mniej więcej tak ?

public double CalculateDiscount(IOrder order)
{
    var discounts = GetAllAvaliableDiscountsInPropperOrder();
    foreach (var discount in discounts)
    {
        discount.Recalculate(order);
    }
    return order.Discount;
}

Taki kod jak powyżej jest zamknięty na modyfikacje ale otwarty na rozszerzanie. Dodawanie kolejnego sposobu obliczania zniżki nie wymaga modyfikowania powyższego kodu. Co więcej, możemy zniżki ładować dynamicznie np. za pomocą MEF-a a przez to w zależności od klienta dystrybuować aplikacje z różnymi modelami obliczania zniżki – bez rekompilacji czy dziwnych, skomplikowanych konfiguratoró. Prawie pewne jest, że po 5-10 klientach zbuduje nam się pokaźna biblioteka wymysłów pomysłów, która zaspokoi 90% potrzeb przyszłych klientów.  Czyż nie jest to piękne? Każdy kolejny klient nie kosztuje nas praktycznie nic.