Herkese merhaba. Bir önceki yazımda SOLID prensiplerinden ve Single Responsibility ilkesinden bahsetmiştim. Bu yazıda ise Open Closed ilkesini inceleyeceğiz. Bu ilke, uygulamanın mevcut kodlarını değiştirmeden sistemin özelliklerinin genişletilmesini önerir. Bu sayede yeni bir özellik eklendiğinde var olan kodda değişiklik yapılmadığı için bug oluşma riskini de azaltmış oluruz. Bu yazıdaki örneklere github repomdan erişim sağlayabilirsiniz.

Open Closed Nedir?

Open closed ilkesi; metotlar, sınıflar ve modüller gibi yazılım parçalarının geliştirmeye açık ancak değiştirilmeye kapalı olması gerektiğini söyler. Yani mevcuttaki bir uygulamaya yeni bir özellik eklemek istediğimizde var olan kodu değiştirmeden ekleme yapabilmeliyiz. Çünkü mevcut kodu değiştirmek beklenmedik sonuçlara ve buglara neden olabilir. Tabii ki, sadece mevcuttaki bir bugı çözmek için yapıyorsak mevcut kod üzerinde değişiklik yapabiliriz.

Genellikle yeni bir özellik eklemek için interface ya da abstract sınıflar gibi sanal olarak oluşturulan yapılar kullanılır. Bu sayede mevcut kod yapısında değişiklik yapmamıza gerek kalmadan geliştirmelerimizi yapabiliriz.

Örnek olarak farklı ödeme seçenekleri olan bir e-ticaret sitesinin ödeme servislerini yazdığımızı düşünelim. Uygulamamızda kredi kartı ve havale ile ödeme seçenekleri olsun. Bu ödeme yöntemlerine ek olarak PayPal ile ödeme seçeneğini de uygulamamıza eklemek istiyoruz. Öncelikle mevcuttaki ödeme sistemlerimizin kodlamasını gerçekleştirelim.

internal class PaymentByCreditCard
{
    public string PaymentviaCreditCard(decimal amount)
    {
        return $" {amount} amount was paid by credit card.";
    }
}

internal class WirePayment
{
    public string PaymentviaWire(decimal amount)
    {
        return $" {amount} amount was paid by wire.";
    }
}

internal class PaymentService
{
    PaymentByCreditCard _paymentByCreditCard;
    WirePayment _wirePayment;

    public PaymentService()
    {
        _paymentByCreditCard = new PaymentByCreditCard();
        _wirePayment = new WirePayment();
    }

    public string MakePayment(decimal amount, PaymentMethod paymentMethod)
    {
        switch (paymentMethod)
        {
            case PaymentMethod.Wire:
                return _wirePayment.PaymentviaWire(amount);
            case PaymentMethod.CreditCard:
                return _paymentByCreditCard.PaymentviaCreditCard(amount);
            default:
                return "Payment was not made.";
        }
    }
}

enum PaymentMethod
{
    CreditCard = 1,
    Wire = 2
}

Console.WriteLine("Enter the amount: ");
decimal amount = Convert.ToDecimal(Console.ReadLine());

Console.WriteLine("For pay via Credit Card press 1");
Console.WriteLine("For pay via Wire press 2");
PaymentMethod paymentMethod = (PaymentMethod)Convert.ToInt32(Console.ReadLine());

#region Non-Refactored
PaymentService paymentService = new PaymentService();
string result = paymentService.MakePayment(amount, paymentMethod);
Console.WriteLine(result);
#endregion

PaymentService içerisindeki MakePayment metodu üzerinden ödeme işlemleri gerçekleştiriliyor. Bu kod içerisine PayPal ile ödeme seçeneğini eklememiz gerektiğinde MakePayment metodu üzerinde düzenleme yapmamız gerekiyor. PayPal ile ödeme işlemini gerçekleştiren servisi oluşturduktan sonra değişikliklerimizi yapalım.

internal class PaymentByPaypal
{
    public string PaymentviaPaypal(decimal amount)
    {
        return $" {amount} amount was paid by Paypal.";
    }
}

internal class PaymentService
{
    PaymentByCreditCard _paymentByCreditCard;
    PaymentByPaypal _paymentByPaypal;
    WirePayment _wirePayment;

    public PaymentService()
    {
        _paymentByCreditCard = new PaymentByCreditCard();
        _paymentByPaypal = new PaymentByPaypal();
        _wirePayment = new WirePayment();
    }

    public string MakePayment(decimal amount, PaymentMethod paymentMethod)
    {
        switch (paymentMethod)
        {
            case PaymentMethod.Paypal:
                return _paymentByPaypal.PaymentviaPaypal(amount);
            case PaymentMethod.Wire:
                return _wirePayment.PaymentviaWire(amount);
            case PaymentMethod.CreditCard:
                return _paymentByCreditCard.PaymentviaCreditCard(amount);
            default:
                return "Payment was not made.";
        }
    }
}

Örnekte gördüğümüz gibi PayPal ile ödeme seçeneğinin eklenebilmesi için MakePayment metodunda değişiklik yapmamız gerekti. Bu kod parçacığı için çok büyük bir değişiklik olmasa da, gerçek bir ödeme sisteminde böyle bir değişikliğin çok büyük etkileri olurdu. Ayrıca sistemin test maliyetlerini de inanılmaz derecede yükseltirdi. Program.cs içerisine de aşağıdaki eklemeleri yapıp kodun çıktısını inceleyelim.

Console.WriteLine("Enter the amount: ");
decimal amount = Convert.ToDecimal(Console.ReadLine());

Console.WriteLine("For pay via Credit Card press 1");
Console.WriteLine("For pay via Wire press 2");
Console.WriteLine("For pay via Paypal press 3");
PaymentMethod paymentMethod = (PaymentMethod)Convert.ToInt32(Console.ReadLine());

#region Non-Refactored
PaymentService paymentService = new PaymentService();
string result = paymentService.MakePayment(amount, paymentMethod);
Console.WriteLine(result);
#endregion

Örneklerde de görüldüğü üzere bütün ödeme sistemleri hatasız bir şekilde çalışıyor. Ancak ileride yeni ödeme sistemleri eklenebilir ya da var olan ödeme sistemlerin biri veya birkaçı kaldırılabilir. Yazılım geliştirirken bu şekilde ön göremediğimiz durumlar karşımıza çıkabilir. Bu nedenle code base oluştururken gelecekte bu yapının kolayca genişletilebilmesini sağlamalıyız. Var olan yapı, bozulmadan ve buglanmadan gelecekteki gereksinimlere kolayca adapte olmalıdır.

Refactor

Bu bağlamda, open closed ilkesini uygulamak için öncelikle IPaymentService isminde bir interface oluşturalım. Ödeme işlemini gerçekleştirmek için yazdığımız MakePayment metodunu da bu interface içerisinde oluşturalım. Daha sonrasında farklı ödeme metotları için oluşturduğumuz sınıflar bu interface’i implent edelim.

internal interface IPaymentService
{
    string MakePayment(decimal amount);
}

internal class PaymentByCreditCard : IPaymentService
{
    public string MakePayment(decimal amount)
    {
        return $" {amount} amount was paid by credit card.";
    }
}

internal class PaymentByPaypal : IPaymentService
{
    public string MakePayment(decimal amount)
    {
        return $" {amount} amount was paid by Paypal.";
    }
}

internal class WirePayment : IPaymentService
{
    public string MakePayment(decimal amount)
    {
        return $" {amount} amount was paid by wire.";
    }
}

Bu sayede ileride farklı bir ödeme yöntemi eklenmesi gerektiğinde IPaymentService implement edilerek, MakePayment metodu içerisinde değişiklik yapılmasına gerek kalmadan eklenebilecek. Henüz Dependency Injection konusunu bilmediğimizi varsaydığım için bir helper classa ihtiyacımız olacak. Bu sayede ödeme tipine göre ilgili olan class’ın objesini dönebileceğiz.

internal class PaymentHelper
{
    public static IPaymentService PaymentHelperMethod(PaymentMethod payment)
    {
        switch (payment)
        {
            case PaymentMethod.Paypal:
                return new PaymentByPaypal();
            case PaymentMethod.Wire:
                return new WirePayment();
            case PaymentMethod.CreditCard:
                return new PaymentByCreditCard();
            default:
                return null;
        }
    }
}

Ödeme yöntemlerimiz IPaymentService’ini implement ettiği için metodun dönüş tipini IPaymentService olarak verebiliriz. Şimdi PaymentService sınıfını refactor edelim.

internal class PaymentServiceRefactored
{
    IPaymentService _paymentService;

    public PaymentServiceRefactored(IPaymentService paymentService)
    {
        _paymentService = paymentService;
    }

    public string MakePayment(decimal amount)
    {
        return _paymentService.MakePayment(amount);
    }
}

MakePayment metodunu interface içerisine eklediğimiz için PaymentService sınıfında ekstra bir değişiklik yapmamıza gerek kalmadı. İleride de yeni bir ödeme servisi geldiğinde bu class içerisinde değişiklik yapılmayacak. Program.cs içerisinden ilgili işlemleri tamamlayalım.

Console.WriteLine("Enter the amount: ");
decimal amount = Convert.ToDecimal(Console.ReadLine());

Console.WriteLine("For pay via Credit Card press 1");
Console.WriteLine("For pay via Wire press 2");
Console.WriteLine("For pay via Paypal press 3");
PaymentMethod paymentMethod = (PaymentMethod)Convert.ToInt32(Console.ReadLine());

#region Refactored
PaymentServiceRefactored paymentService1 = new PaymentServiceRefactored(PaymentHelper.PaymentHelperMethod(paymentMethod));
string result1 = paymentService1.MakePayment(amount);
Console.WriteLine(result1);
#endregion

Console.ReadKey();

Bu örneği debug yaparak incelediğinizde interface’in çalışma mantığını daha iyi kavrayacaksınız. Sonuç olarak yazılım tasarımında open closed prensibi geliştirmeye açık, değişime kapalı olduğu için önem taşımaktadır. Kod temeli sağlam bir şekilde oluşturulmuşsa yazılımımızı çok kolay bir şekilde gelişime açabiliriz. Sonraki yazıda Liskov Substitution konusunu ele alacağım. Bir sonraki yazıya kadar kendinize çok iyi bakın! 😊