Yazılım geliştirmenin bir yaşam döngüsü vardır. İlk önce talebi ya da sorunu analiz eder ve gereksinimleri ortaya çıkarırız. Ardından analize göre sistemi tasarlarız. Tasarlama süreci bitirdikten sonra kodlama aşamasına geçeriz. Kodlama sürecinin ardından testlerimizi yapar ve en sonunda yazılımı canlıya alırız. Bu süreç kendini sürekli tekrar eder. Zaman ilerledikçe ihtiyaçları karşılamak için mevcut kod üzerinde eklemeler yapmak isteriz. Bu eklemeler basit olsa dahi mevcut kod üzerinde büyük değişikliklere neden oluyorsa, tasarım aşamasında büyük bir hata yapmışız demektir. Elimizde büyük bir legacy, birbirine sıkı bağımlı kod yapıları oluşmuş olabilir. Belki gerekli soyutlamaları ya doğru yapamadık ya da yanlış bir şekilde yaptık. Modüllerin, sınıfların, bileşenlerin birbirleri arasında sıkı bağımlı yapılar ortaya çıkardık. Aslında amacımız yazılım sistemlerini tasarlarken gevşek bağımlı(loose coupling), yüksek kohezyonlu(high cohesion) ve doğru soyutlamaların(abstractions) olduğu yapılar ortaya çıkarmaktır. Tüm bunları prensiplerden ve patternlerden yardım alarak yapabiliriz. DRY (Don’t Repeat Yourself), KISS (Keep It Simple, Stupid), YAGNI (You Ain’t Gonna Need It), Single Responsibility Principle, Open/Closed Principle, Liskov Substitution Principle, Interface Segregation Principle, Dependency Inversion Principle gibi bir çok prensip bulunuyor. Ayrıca Singleton Pattern, Factory Method Pattern, Observer Pattern, Decorator Pattern, Strategy Pattern, Adapter Pattern, Command Pattern, Composite Pattern, Template Method Pattern, State Pattern gibi bir çok pattern bulunuyor. Ben bu yazımda Open/Closed Prensibinden bahsetmeye çalışacağım.
Open-Closed prensibi, ben dahil bir çoğumuz için SOLID prensiplerini araştırırken karşısına çıkan 5 prensipten birisidir. “O” harfine karşılık gelir. OCP olarak kısaltırılır. Aslında SOLID arka arkaya bir kişi tarafından ortaya atılmış prensipler değildir. OCP 1988 yılında Bertrand Meyer tarafından ortaya atılmış bir prensiptir. Daha sonra Robert C. Martin OCP ile 4 diğer prensibi bir bütün halinde insanlara aktarmaya çalışmıştır. Open-Closed prensibini ilk ortaya atan Bertrand Meyer prensibi aşağıdaki gibi açıklamıştır.
A software artifact (classes, modules, functions, etc.) should be open for extension but closed for modification.
Türkçe’ye çevirmek istersek; Bir yazılım genişlemeye açık, fakat değişime kapalı olmalıdır. Başka bir söylemle, bir yazılımının yapısını ve davranışını değiştirmek zorunda kalmadan, genişletilebilir olmasıdır. Bertrand Meyer çözüm olarak kalıtımı (inheritance) önermiştir. Kalıtım ile sınıfları genişletmeyi amaçlamıştır. Eğer bir sınıfın genişlemeye ihtiyacı varsa ondan türeyen yeni bir alt sınıf oluşturmayı önermiştir. Bu oluşturulan yeni alt sınıfa, ihtiyaç duyulan yeni değişkenler, methodlar eklenerek genişlemenin sağlanabileceğini belirtmiştir.
Günümüzde bu yöntem OCP için çok uygulanmasada tarihteki ilk tanımı ve çözümü bu şekilde ortaya atılmıştır. Ancak bu çözümün bazı sıkıntıları vardır. Öncelikle kalıtım (inheritance) yoluyla oluşturulan alt sınıflar, üst sınıfın methodlarına, değişkenlerine bağlı ise iki sınıf arasında sıkı bir bağımlılık (tight coupling) oluşturur. Birbirine sıkı bağımlı yapılar esnemez ve kırılgan sistemler oluşturur. Bizim amacımız gevşek bağımlı (loose coupling) yapılar oluşturarak esnek sistemler geliştirmektir. Bir diğer problem ise c#, java, kotlin, swift gibi dillerde bir alt sınıfın sadece bir üst sınıftan türetilebilir olmasıdır. Bir sınıfa birden fazla yeni özellik eklemek istediğimizde, bunu tek bir kalıtım ile yapamayacağımız için devamlı birbirinden türeyen sınıflar oluşturmak zorunda kalacağız. Bu da yönetmemiz ve bakımını yapmamız gereken çok fazla sınıflar ortaya çıkaracaktır. Bir başka problem ise zaman içerisinde mevcut sınıflardan türetilen yeni sınıfların Liskov Substitution Prinsibini ihlal edebilir olmasıdır. Bu ihlal de bize başka teknik borçlara ve problemlere yol açabilir.
Robert C. Martin OCP’iyi tekrardan ele aldı ve yeni bir çözüm ortaya koydu. Bu çözümü oluştururken soyutlama (abstraction) ve çok biçimliliğin (polymorphism) güçlerinden faydalandı. Robert C. Martin üst sınıflar yerine arayüz(interface) kullanmayı tercih etti. Yeni gelen bir geliştirme talebi olduğunda, yeni bir sınıf oluşturdu ve ilgili arayüzü (interface) yeni sınıfa uyguladı**(implementation)** ve detayları oluşturduğu sınıf içerisinde tanımladı. Arayüzün kullanıldığı diğer sınıflarda herhangi bir değişiklik yapmadan yeni özellikleri ekledi. Yeni oluşturduğu sınıfları çok biçimlilik (polymorphism) sayesinde arayüzün kullandığı sınıflara parametre olarak verebilmiş oldu. Bu çözüm sayesinde gereksiz kalıtım (inheritance) işlemlerinden kurtuldu, soyutlama(abstraction) yaparak gevşek bağımlı(loose coupling) yapılar ortaya çıkardı ve esnek bir sistem kurmayı başardı.
Bu prensibi bir örnek ile anlatmaya çalışalım. Öncelikle Open-closed prensibini ihlal edebilecek bir kod yapısı üzerine konuşalım. Ardından bunu nasıl düzeltiriz ve nasıl Open-Closed prensibine uygun hale getirebiliriz bakalım. Örneğimizde müşterilerin türüne göre taşıma bedelini hesaplayalım. İş modelimize göre 2 farklı müşteri türümüz var. Bu müşteri türleri Default ve Silver olsun. Sistem içerisinde son 6 ayda yapılan taşıma sayısının 10 adedi geçmeyen müşterilere Default müşteri ve son 6 ayda yapılan taşıma sayısının 10 ve üzeri olduğu müşterilere de Silver müşteri diyelim. Default müşterilerimizden kilogram başına 50,00 TRY ve Silver müşterilerimizden kilogram başına 30,00 TRY ücret alınacak olsun. Örneğin gelen ağırlığımız 1.000 kg olursa, taşıma bedeli Default müşterilerimiz için 1.000 * 50,00 = 50.000 TRY ve Silver müşterilerimiz için 1.000 * 30,00 = 30.000 TRY olacaktır. Bu koşullara göre aşağıdaki kodu geliştirdiğimizi düşünelim.
public enum CustomerType
{
Default,
Silver
}
public class ShippingService
{
public double CalculateShippingCost(CustomerType customerType, double totalWeightAsKg)
{
return customerType switch
{
CustomerType.Default => totalWeightAsKg * 50.0,
CustomerType.Silver => totalWeightAsKg * 30.0,
_ => throw new NotSupportedException("Invalid customer type")
};
}
}
Elimizde CustomerType adında bir enum ve ShippingService adında bir sınıf var. Bu sınıf CalculateShippingCost adında bir method barındırıyor. Bu method parametre olarak CustomerType ve toplam taşınacak malın ağırlığını alıyor. CustomerType göre müşterinin kilogram başına ücreti belirleniyor ve toplam ağırlık ile çarpılarak taşıma bedeli ortaya çıkıyor. Bu istekleri karşılayan bir kod yazdık ve test ettik. Herşey doğru bir şekilde çalışıyor. Bir süre sonra iş modelimizin değiştiğini ve Gold türünde yeni bir müşteri türü daha eklendiğini öğrendik. Gold türündeki bir müşterinin fiyat hesaplaması yapılırken kilogram başına 15,00 TRY ücret belirlendiğini öğrendik. Bizim bu koşuluda karşılayacak geliştirmeyi yapmamız gerekiyor.
public enum CustomerType
{
Default,
Silver,
Gold
}
public class ShippingService
{
public double CalculateShippingCost(CustomerType customerType, double totalWeightAsKg) {
return customerType switch
{
CustomerType.Default => totalWeightAsKg * 50.0,
CustomerType.Silver => totalWeightAsKg * 30.0,
CustomerType.Gold => totalWeightAsKg * 15.0,
_ => throw new NotSupportedException("Invalid customer type")
};
}
}
Yukarıdaki kod örneğinde gerekli geliştirmeyi yaptık. Peki, bu yaptığımız geliştirme yöntemi doğru bir yöntem mi? Bir sonraki talepte Platinum, Diamond gibi farklı müşteri türleri daha eklenmek istenirse ne yapacağız? Burayı gelip tekrardan gerekli yerlere eklemeler yapmak mı isteyeceğiz? Tüm bunlar Open-Closed prensibini ihlal eden davranışlardır. Her yeni bir talepte var olan kodu değiştirmek, çalıştığına emin olduğumuz kod parçaların tutarlığının tekrardan sorgulanmasına sebep olabilir. Bir değişim gördüğü için test edilmesi gerekir. Yapılan değişimler bize başka noktalarda başka hatalara sebep olabilir. Peki bunu Open-Closed prensibine göre nasıl geliştirebiliriz?
public interface ICalculateShippingCostService
{
double Calculate(double totalWeightAsKg);
}
public sealed class DefaultCalculateShippingCostService : ICalculateShippingCostService
{
public double Calculate(double totalWeightAsKg)
{
const double pricePerWeightAsKg = 50.0;
var totalPrice = pricePerWeightAsKg * totalWeightAsKg;
return totalPrice;
}
}
public sealed class SilverCalculateShippingCostService : ICalculateShippingCostService
{
public double Calculate(double totalWeightAsKg)
{
const double pricePerWeightAsKg = 30.0;
var totalPrice = pricePerWeightAsKg * totalWeightAsKg;
return totalPrice;
}
}
public sealed class GoldCalculateShippingCostService : ICalculateShippingCostService
{
public double Calculate(double totalWeightAsKg)
{
const double pricePerWeightAsKg = 15.0;
var totalPrice = pricePerWeightAsKg * totalWeightAsKg;
return totalPrice;
}
}
public class ShippingService
{
private readonly ICalculateShippingCostService _calculateShippingCostService;
public ShippingService(ICalculateShippingCostService calculateShippingCostService)
{
_calculateShippingCostService = calculateShippingCostService;
}
public double CalculateShippingCost(double totalWeightAsKg)
{
return _calculateShippingCostService.Calculate(totalWeightAsKg);
}
}
Yukarıdaki geliştirdiğimiz koda baktığımızda ICalculateShippingCostService adında bir arayüz(interface) oluşturduk. Arayüz içerisinde Calculate adında bir method tanımladık. Daha sonra DefaultCalculateShippingCostService, SilverCalculateShippingCostService, GoldCalculateShippingCostService adında 3 adet sınıf oluşturduk. Hepsi ICalculateShippingCostService arayüzünü uyguladı ve her biri Calculate methodu içerisinde gerekli geliştirmeler yapıldı. ShippingService refactor edip yeniden düzenledik. Artık constructor içerisinde ICalculateShippingCostService arayüzünü parametre olarak alıyoruz ve CalculateShippingCost methodu içerisinde arayüzün Calculate methodunu çağırıyoruz. Bu şekilde Open-Closed prensibi uygulayan bir kod yapısına sahip olduk. İleride Platinum, Diamond gibi farklı müşteri türleri gelsede mevcut kodu değiştirmeden yeni sınıflar oluşturarak mevcut sistemimizi genişletebileceğiz.
Özetlemek gerekirse, Open-Closed prensibi sistemimize esneklik katar. Sistemi genişlemeye açık olacak şekilde tasarladığımız için mevcut kodu değiştirmeden yeni özellikler, işlevler ekleyebiliriz. Değişime karşı direnci düşük seviyede tutarız. Ayrıca bakım maliyetleri daha az olur. Open-Closed prensibi ile mevcut kodu değiştirmediğimiz için mevcut kodumuzda hata oluşma riskini azaltmış oluruz. Ölçeklenebilir sistemler kurabiliriz. Yeni gereksinimler ortaya çıktıkça mevcut sistemimizi değiştirmek zorunda kalmadan yeni sınıflar ekleyebiliriz. Böylece modüler bir sistem kurmuş oluruz. Yeniden kullanılabilen kod bloklarını geliştirmiş oluruz. OCP’ye uygun yaptığımız örnekte de gördüğünüz gibi GoldCalculateShippingCostService adında bir sınıfımız var. Bu sınıfımız ihtiyaç halinde başka noktalarda da kullanabiliriz. Örneğimizde ShippingService içerisindeki CalculateShippingCost methodu sadece Calculate adında bir methodu çağırıyor**.** Bunun dışında hesaplama ile ilgili hiçbir bilgiye sahip değil. OCP sayesinde kapsülleme (encapsulation) yaparak uygulama ayrıntılarını gizleyebiliriz.
Bu yazımda Open-Closed prensibini anlatmaya çalıştım. Umarım sana dokunmuş olabilirim. Merak ettiğin bir şey olursa benimle iletişime geçebilirsin.