Değişen ve artan ihtiyaçlarla paralel şekilde evrimini sürdüren teknoloji alanında da geçmişten günümüze gelişim devam etmektedir. Bu çerçevede insanoğlu sorunlarını elektronik aygıtlar kullanarak çözemeye çalışma konusunda da epey yol aldı. Elektronik aygıtların ihtiyaçlara çözüm üretecek şekilde yönetilmesini, haberleşmesini ve yapması gereken işleri yerine getirmesini sağlamak üzere oluşturulmuş komutlar dizilerinin oluşturulmasını yazılım veya programlama olarak tanımlayabiliriz.
1960’lı yılların sonlarına doğru yazılım dünyasında, karmaşıklığın ve boyutların sürekli artmasıyla beraber geliştirilen yazılımlardaki niteliğin düzeyini korumak, bakım maliyetleri ve bunlara bağlı olarak harcanan zaman ve emek hızla artmaktaydı. Bu sorunlara çözüm olarak veri soyutlama (data abstraction), çok biçimlilik (polymorphism), bilgi gizleme (information hiding) ve kalıtım (postance) gibi geliştirilen yazılımın üzerinde ekip çalışmasını kolaylaştıran, bakım ve yönetim maliyetlerini düşüren ve birimselliği (modularity) benimseyen programlama yaklaşımı olan nesne yönelimli programlama (object – oriented programming) (OOP) yaklaşımı ortaya çıkmış oldu.
Günümüzde bir çok yazılım iş ilanında da rastlayacağınız üzere nesne yönelimli programlaya uygun başta C#, Java, PHP gibi dilleri kullanan firmaların adaylarda aradığı özelliklerden birisi nesne yönelimli programlama konusuna hakim olması, nesne yönelimli programlamanın günümüzde de yaygın olarak kullanıldığını ve geniş çaplı yazılım projelerinde benimsendiğini kanıtlar niteliktedir.
Tabiki nesne yönelimli programlama yaklaşımının tek başına yeterli ve standartlara uygun yazılım geliştirmek için yeterli olduğunu varsaymak biraz hayali olur, bu nedenle KISS, DRY, YAGNI ve SOLID gibi prensipler ortaya çıkmıştır.
Bu yazıda ele alacağımız SOLID prensipleri temel OOP problemlerine çözüm olması amacıyla 2000’li yılların başlarında yazılımın Bob Amcası (Uncle Bob) olarak bilinen Robert C. Martin tarafından ortaya konulan ve yazılım camiasında benimsenerek en dikkat edilerek uygulanmaya çalışılan 5 temel prensibin baş harflerinin birleşimiyle isimlendirilen yöntemler bütünüdür. Bu kadar uzun bir girişten sonra zaman kaybetmeden ilk prensibimiz olan single responsibility ile başlayalım.
Single Responsibility Principle
Türkçeye tek sorumluluk prensibi olarak çevirisini yapabileceğimiz bu prensip temelde, geliştirdiğimiz yazılımlardaki sınıf ve metodların yapması için planlandığı/oluşturulduğu tek işi/görevi yerine getirmesi olarak tanımlayabiliriz. Küçük bir gerçek hayat örneğiyle açıklamaya çalışırsak; bir çalışan olduğunu düşünün ve bu çalışanın birden fazla işi yapıyor olsun. Çalışanımız belirli bir süre sonra iş yoğunluğu, fazlalığı, iş birliği yapması gereken birim ve işe ilişkin bilgisi artacağından ve buna paralel olarak tecrübesinin bu yönde gelişeceğinden bu çalışanı işten ayrıldığında veya değiştirilmek istendiğinde aynı nitelikte işi yapacak olan tecrübeye sahip başka bir çalışanı bulmak oldukça zor hatta imkansız hale gelecektir. Tersi durumda düşündüğümüzde ise eğer çalışanın görevleri belli bir alana yönelik olsaydı bu eleman işten ayrıldığında veya yerine başkası alınması düşünüldüğünde aynı nitelikte işi yapabilecek olan personeli bulmak çok daha kolay olacaktı. Sonuç olarak personel gerektiği/planlandığı gibi çalışmak yerine kurumun özel ihtiyaçlarına göre geliştiği ve tecrübesi şekillendiği için olası bir durumda sorun yaşıyor hale geldik.
Single responsibility prensibinide böyle düşünebiliriz, proje ilerledikçe ve genişledikçe sınıfları ve metodlar içinde görevi olmayan veya oluşturulma amacına hitap etmeyen başka işlerinde yapılması ilerleyen zamanlarda kodun tekrar kullanılmasını zorlaştıracak ve bağımlılığı artıracağından projeye katılacak yeni ekip üyeleri için başta adaptasyon, mevcut ekip üyeleri için ise bir takım belirsizlik sorunlarını ve zorlukları ortaya çıkaracaktı, sonuç olarak projenin geliştirilmesi için gereken maliyet artacak ve standartlara uygun olmayan geliştirilmesi ve bakımı zor bir yazılım ve karmaşık bir yazılım süreci ortaya çıkmış olacaktı.
Kısacası: single responsibility prensibi yazdığımız koddaki sınıf ve metodların amaçlarından farklı işleri yerine getirip getirmediğini sorgulamamızı ve tek bir sınıf veya metod için tek bir işin tanımlanması gerektiğini öğütler.
Yaptığımız tanımlama ve gerçek hayat örneğinin üzerine C# ile örnek bir senaryo üzerinde ilerleyerek kod tarafında prensibin çalışma mantığını görelim.
Senaryo: Akıllı ev yönetim sistemi için bir uygulama geliştirdiğimizi varsayalım. Kombinin kontrol açılıp kapanmasını kontrol eden bir sınıfımız olsun;
public class CombiController
{
public DeviceStatus CombiStatusManagement(string deviceName){
if(CheckWaterPressure(deviceName)){
// LOGICS
return DeviceStatus.On;
}
return DeviceStatus.Off;
}
public bool CheckWaterPressure(string deviceName){
// LOGICS
return true;
}
}
CombiController sınıfı içindeki CombiStatusController metodu aldığı deviceName parametresine göre gerekli logicleri yürütüyor ve su basıncını kontrol ediyor ve su basıncının uygun olduğu durumda kombiyi açmış oluyor. Kodumuzda herhangi bir problem yok stabil çalışıyor gibi gözükse de CheckWaterPressure() metodunun CombiController sınıfı içinde yer alması single responsibility prensibine aykırı bir durum oluşturuyor.
Senaryomuza göre ilerleyen zamanda uygulamamız üzerinden su filtresini kontrol edecek modülü eklediğimizi düşünelim. İyi ihtimalle CombiController içindeki CheckWaterPressure() metodunu kullanacağız veya aynı işi yapan başka bir metodu WaterFilterController sınıfı içerisine de yazıyor olacağız ki buda tekrar kullanılabilirlik ve don’t repeat yourself prensibine aykırı durumlar oluşturuyor. Ayrıca uygulamanın testlerinin yazılması da başta olmak üzere süreçlerin zaman ve emek maliyetini artırıyor.
Yanlış yöntemi inceledikten sonra doğrusunu şu şekilde uygun bir şekilde oluşturabiliriz;
public class CombiController
{
public DeviceStatus DeviceStatusController(string DeviceName){
if(CheckWaterPressure(DeviceName)){
// LOGICS
return DeviceStatus.On;
}
return DeviceStatus.Off;
}
}
public static class WaterPressureController
{
public static bool CheckWaterPressure(string DeviceName){
// LOGICS
return true;
}
}
Sınıflarımızı ve metodlarımızı doğru şekilde oluşturduğumuz yeni örneğimizde artık sınıf ve metodlar yapması için planlanmış tek bir işi yerine getirmekte ve proje genişledikçe yazacağımız su filtresi veya kahve makinesi için basınç kontrolünü WaterPressureController sınıfı içindeki CheckWaterPressure() metoduyla gerçekleştirebiliyor hale geldik. Dolayısıyla kodun karmaşıklığının ve ilerleyen süreçlerde oluşabilecek ekstra yüklerin önüne geçmiş olduk.
Open-open Principle
Türkçeye açık-kapalı prensibi olarak çevirebileceğimiz bu prensip temelde kodumuzun (class, method vs.) gelişime açık ve değişime kapalı olmasını öğütler. Değişime kapalı gelişime açık olmanın mantığı projenin ilerleyen süreçlerinde yeni ihtiyaçlar sonucunda ortaya çıkan değişiklikleri en az eforla entegre etmemize yardımcı olur, bu sayede kodumuzun bağımlılığı düşük olacağı için yapılan değişiklikler başka bir yerleri etkilemeyecektir.
Bu prensibi açıklamak için sıklıkla kullanılan senaryo üzerinden devam inceleyelim. Projemizde loglama altyapısnı kuruyor olalım ve şimdiki şartlara göre database’e ve bir text file içine loglama yapmak ihtiyaçlarımızı karşılıyor olsun.
public enum LogType
{
DatabaseLogger,
FileLogger,
}
public class DatabaseLogger
{
public bool Log(string data){
// LOGICS
return true;
}
}
public class FileLogger
{
public bool Log(string data){
// LOGICS
return true;
}
}
public class Logger
{
private DatabaseLogger databaseLogger;
private FileLogger fileLogger;
public Logger(FileLogger fileLogger, DatabaseLogger databaseLogger)
{
this.databaseLogger = databaseLogger;
this.fileLogger = fileLogger;
}
public void Log(LogType logType, string data){
switch (logType)
{
case LogType.DatabaseLogger:
databaseLogger.Log(data);
break;
case LogType.FileLogger:
fileLogger.Log(data);
break;
}
}
}
Yazdığımız logging alt yapısına bakacak olursak projemizde sorunsuz bir şekilde kullanabiliriz gibi gözüküyor. Ancak proje ilerledi ve artık loglamayı json şeklinde Elasticsearch üzerinde de tutmaya ihtiyacımız oldu bu durumda LogType enum’una Elasticseach’ı type olarak ekleyip Elastiksearch için loglama logiclerinin yer aladığı ElasticsearchLogger sınıfımızı oluşturmak ve Log sınıfımız içine depency injectionla tanımlayıp switch case yapısına elasticsearhı eklememiz gerekiyor. Tada artık Elasticsearch üzerinde de loglama yapabilme kabileyitine sahip loglama altyapımızla mutlu mesut projemizi geliştirmeye devam ediyoruz ancak kısa bir zaman sonra loglamayı xml şeklinde de yapmaya ihtiyaç duyduk. bu senaryoda tekrar başa sarıp yukarıdakilerin aynısını xml içinde yapmamız gerekiyor.
Tüm bu yaptıklarımızı okurken bile karmaşık kulağa hoş gelmeyen bir şeyler olduğunu söylemek mümkün. Öncelikle DatabaseLogger ve FileLogger sınıflarımızın log metodları için bir standart bulunmuyor ve yeni bir yöntemle loglama yapmak istediğimizde her seferinde başa dönüp LogType’tan başlayarak gerekli değişiklikleri yapmak zorunda kalıyoruz ki bu durumda bir standart olmadan bağımlılığı ve karmaşayı artırarak kodlamaya neden oluyor, dahası yazılacak teslerin karmaşıklaşmasından bahsetmeye bile gerek yok.
Yanlış yöntemi inceledikten sonra open-open prensibine göre loglama alt yapımızı şöyle oluşturursak;
public interface ILogger
{
bool Log(string value);
}
public class DatabaseLogger : ILogger
{
public bool Log(string value)
{
// LOGICS
return true;
}
}
public class FileLogger : ILogger
{
public bool Log(string value)
{
// LOGICS
return true;
}
}
public class Logger
{
private ILogger logger;
public Logger(ILogger logger){
this.logger = logger;
}
public void Log(string value){
logger.Log(value);
}
}
Örneğimizde DatabaseLogger ve FileLogger sınıflarımız ILogger interface’inden kalıtım alarak oluşturduk bu sayede logger tiplerini her seferinde başa dönüp yazmak zorunda olduğumuz hiçte esnek olmayan bir yapı olan enum type sisteminden kurtulmuş olduk. Diğer taraftan DatabaseLogger ve FileLogger için Log işlemlerinin yapıldığı metod standart hale gelmiş oldu. (interface sayesinde bu standarta uymak zorunda kaldık.) Son olarak ise yeni bir Loglama mekanizması eklemek için sadece ILogger arayüzünden kalıtarak Log metodu içinde gerekli logicleri gerçekleştirmek yeterli hale geldi Logger sınıfının içerisine gidip herhangi bir değişiklik yapmak zorunda bile kalmadık.
Son olarak JsonLogger mekanizmasının entegre etmek istersek tüm yapmamız gereken;
public class JsonLogger : ILogger { public bool Log(string value) { // LOGICS return true; } }
örnekte olduğu gibi JsonLogger sınıfımızı oluşturarak ILogger yüzünden kalıttık ve artık loglamalarımızı Json nesneleri halinde de yapabiliriz.
Bu sayede kod tekrarını, karmaşıklığı en aza indirip bağımlılığı düşük, gelişime açık ve değişime kapalı bir loglama alt yapısına kavuşmuş olduk.
SOLID prensiplerinden single-responsibility ve open-open prensiplerine değindiğimiz bu yazının sonuna gelmiş bulunmaktayız. SOLID prensiplerinin devamını anlattığımız “SOLID Yazılım Geliştirme Prensipleri Bölüm 2” ye buradan ulaşabilirsiniz.
Bir sonraki yazıda görüşünceye dek sağlıcakla kalın.
Sources:
Be First to Comment