Liskov Yer Değiştirme Prensibi (LSP – Liskov Substitution Principle)

Sıra geldi yazılım tasarım prensipleri (SOLID) makalesinde kısaca değindiğim liskov yer değiştirme prensibini (liskov substitution principle) bir örnekle açıklamaya. Öncesinde bu substitution kelimesini yazmakta baya bir zorlandığımı ve bazı yerlerde kopyala yapıştır ile durumu kurtarmaya çalıştığımı itiraf edeyim. Kendi yazdığım ender yerlerde de yazım yanlışı yapmışsam şimdiden söyleyeyim, kusuruma bakmayın. Yazım açısından bir hayli kafa karıştırıcı kelime, en azından benim için.

Bu prensip ismini vakti zamanında MIT’de (Massachusetts Institute of Technology) profesör olan Amerikalı bir bilim insanı, Barbara Liskov’dan almaktadır. Barbara Liskov, 1987 yılında basılmış olan Data Abstraction and Hierarchy isimli kitabında bu prensiple ilgili tanımı ortaya koymuş ve yazılım dünyasına hiyerarşi ve kalıtım ile ilgili konularda yepyeni bir bakış açısı getirmiştir (Kaynak: wikipedi).

Şimdi Liskov yer değiştirme prensibi (liskov substitution principle) ile ilgili bir örnek yaparak konuyu anlamaya çalışalım. Burada temel olarak yapacağımız şey; bir metodumuz olacak ve bu metod ana sınıf tipinde bir parametre alacak. Bu metoda, ana sınıftan örneklenmiş bir nesne de göndersek, ana sınıftan türetilmiş olan çocuk sınıflardan örneklenmiş bir nesne de göndersek metodun çalışmasında herhangi bir sıkıntı olmamalı. Ayrıca bu metoda da bazı nesneler için istisnai kod yazmamalıyız veya try-catch bloklarını kullanarak hata yakalama işlemlerinde bulunmamalıyız. Sonrasında metodun doğru yazılımının Interface kullanılarak yapılışına ait kodları da vererek bitireceğiz. Tabi bu şekilde sözel anlatım ile konuyu anlayabilmek zor çünkü sözel olarak anlatabilmek de gayet zordu ve ne derecede başarılı olduk bilinmez. Örneğe geçtiğimizde daha anlaşılır bir hale geleceğini düşünüyorum.

Senaryo 1 – Yazılımın Klasik Anlayışa Göre Kodlanması

İlk önce üretim yapan bir işletme olarak aşağıdaki yapıda sınıflarımız olduğunu düşünelim. Ürünlere ait tüm sınıflar (Urun1..N) Urun isimli ana bir sınıftan türetiliyor. Urun isimli ana sınıfımızda da Uret isimli sanal (virtual) bir sınıf tanımlanmış. Türetilen her sınıf bu metodu ezerek (override) kendine ait özel üretim metotlarını işletmektedir.

Aşağıda bir de UrunA isimli bir sınıf daha görülmektedir. O da Urun sınıfından türetilmektedir fakat Uret isimli metodu ezmediğini (override) görüyoruz. Bu ürünü de firmamızda üretilmeyen, başka bir tedarikçiden satın alıp direk olarak kullandığımız veya sattığımız bir ürün olarak düşünebiliriz. Yani bu ürün için üretim yapmıyoruz.

namespace UretimSenaryosu
{
    public class Urun
    {
        public virtual void Uret()
        {
            throw new NotImplementedException();
        }
    }
    
    public class Urun1: Urun
    {
        public override void Uret()
        {
            //Burada ürün 1'e özel üretim metotları çalıştırılıyor.
            Console.WriteLine("Ürün 1'e özel üretim yapıldı.");
        }
    }

    public class Urun2: Urun
    {
        public override void Uret()
        {
            //Burada ürün 2'ye özel üretim metotları çalıştırılıyor.
            Console.WriteLine("Ürün 2'e özel üretim yapıldı.");
        }
    }

    public class UrunA: Urun
    {
    }
}

Tüm sınıflar tanımlandıktan sonra ana projede aşağıdaki gibi bir toplu üretim yapan metodumuz olduğunu düşünelim.

namespace UretimSenaryosu
{
    public class Uretim
    {
        public static void Main(string[] args)
        {
            List<Urun> urunler = UrunListesiGetir();
            foreach (Urun urun in urunler)
            {
                //Normalde burada urun.Uret(); komutu ile işlemi apabiliriz.
                //Fakat ben konunun anlaşılması için üretim işlemlerini yapan bir başka bir sınıfa yönlendirdim.
                UretimYap(urun);
            }
        }

        public static void UretimYap(Urun urun)
        {
            try
            {
                //Burada proje niteliğine göre üretim ile ilgili başka metotlar da çalıştırılabilir.
                urun.Uret();
            }
            catch(Exception hata)
            {
                Console.WriteLine(urun.ToString() + " isimli ürün için üretim yapılamadı.");
            }
        }

        private List<Urun> UrunListesiGetir()
        {
            //Bu metot içerisinde veritabanından ürünlerin bir listesi alındığı düşünülebilir.
            //Ben kolaylık açısından aşağıdaki gibi bir liste döndürerek örnek bir ürün listesi oluşturdum.
            List<Urun> urunler = new List<Urun>();
            urunler.Add(new Urun1());
            urunler.Add(new Urun2());
            urunler.Add(new UrunA());
            return urunler;
        }
    }
}

Proje çalıştırıldıktan sonra konsol ekranında sırası ile aşağıdaki çıktılar görülecektir.

Ürün 1e özel üretim yapıldı.
Ürün 2e özel üretim yapıldı.
UrunA isimli ürün için üretim yapılamadı.

Toplu üretim metodu çalışırken hata veren ürünler için try-catch bloğu kullanarak hata yakalama işlemi yaptık ve uygun bir mesaj verdik. Projenin niteliğine göre başka işlemler veya loglama da yapılabilir. Yada try-catch metodu yerine nesnelerin tiplerine göre bir switch ifadesi kullanılarak uygun işlemler switch blokları içerisinde de yaptırılabilir. Basitlik açısından ben yukarıdaki gibi kullandım.

Bu durumda programın işleyişi açısından belki bir sorun olmadı veya çıkabilecek sorunları bir şekilde yönetebildik fakat mevcut tasarım liskov yer değiştirme prensibine (liskov substitution principle) uygun olmadı. Bu prensip özetle bir ana sınıf, kendisinden türetilen başka bir sınıfla yer değiştirebilir diyordu. Yani Urun1, Urun2 veya UrunA sınıflarından örneklenen nesneler Urun tipinde parametre kabul eden bir metoda gönderildiğinde (bu aşamada üst sınıf ile alt sınıftan örneklenen nesneleri yer değiştirmiş olduk) parametreyi kabul ediyor ve gerekli işlemi yapıyor (Uret metodunu çağırıyor). Bu durumda bizim “UretimYap” metoduna ürünleri gönderdiğimizde hiçbir şekilde “… isimli ürün için üretim yapılamadı.” hatası almamamız gerekiyor.

Senaryo 2 – Yazılım Tasarımının LSP’a Uygun Kodlanması

Peki doğru tasarım nasıl olmalıydı konusuna gelirsek elbette bunun birkaç değişik yolu olabilir. Ben Interface kullanarak yapmayı tercih ettiğimden çözümü de Interface ile yapacağım.

namespace UretimSenaryosu
{
    public Interface IUretimiYapilabilenUrunler
    {
        void Uret();
    }

    public class Urun
    {
    }
    
    public class Urun1: Urun, IUretimiYapilabilenUrunler
    {
        public void Uret()
        {
            //Burada ürün 1'e özel üretim metotları çalıştırılıyor.
            Console.WriteLine("Ürün 1'e özel üretim yapıldı.");
        }
    }

    public class Urun2: Urun, IUretimiYapilabilenUrunler
    {
        public void Uret()
        {
            //Burada ürün 2'ye özel üretim metotları çalıştırılıyor.
            Console.WriteLine("Ürün 2'e özel üretim yapıldı.");
        }
    }

    public class UrunA: Urun
    {
    }
}

Urünlerin tanımlandığı sayfaya “IUretimiYapilabilenUrunler” isimli bir Interface ekledim ve Urunlerin tanımlarında da küçük bir değişiklik yaptım. Ana sınıfımız olan Urun sınıfındaki Uret metodu kaldırıldı ve onun yerine Interface içerisinde Uret isimli bir metot tanımlandı. Alt kısımdaki Urun1 ve Urun2 isimli ürünlere ait sınıflar, bu sınıflardan örneklenen nesneler üretim yapılan ürünler olduğundan ve aynı zamanda da ürün olduğundan hem Urun sınıfından örneklendi hem de yeni arayüzü (interface) uyguladı. UrunA isimli ürüne ait sınıf ise bu arayüzü (interface) uygulamadı ve sadece Urun sınfından örneklendi.

namespace UretimSenaryosu
{
    public class Uretim
    {
        public static void Main(string[] args)
        {
            List<Urun> urunler = UrunListesiGetir();
            foreach (IUretimiYapilabilenUrunler urun in urunler.FindAll(s => s is IUretimiYapilabilenUrunler))
            {
                //Normalde burada urun.Uret(); komutu ile işlemi apabiliriz.
                //Fakat ben konunun anlaşılması için üretim işlemlerini yapan bir başka bir sınıfa yönlendirdim.
                UretimYap(urun);
            }
        }

        public static void UretimYap(IUretimiYapilabilenUrunler urun)
        {
            //Burada proje niteliğine göre üretim ile ilgili başka metotlar da çalıştırılabilir.
            urun.Uret();
        }

        private List<Urun> UrunListesiGetir()
        {
            //Bu metot içerisinde veritabanından ürünlerin bir listesi alındığı düşünülebilir.
            //Ben kolaylık açısından aşağıdaki gibi bir liste döndürerek örnek bir ürün listesi oluşturdum.
            List<Urun> urunler = new List<Urun>();
            urunler.Add(new Urun1());
            urunler.Add(new Urun2());
            urunler.Add(new UrunA());
            return urunler;
        }
    }
}

Programın ana bloğunda yer alan UretimYap isimli metodumuzun parametresini Urun tipinden IUretimiYapilabilenUrunler tipine çevrildi. Main metodunda da foreach içerisinde ürün listesinden sadece IUretimiYapilabilenUrunler arayüzünü uygulayan nesneler seçildi ve UretimYap isimli metoda parametre olarak gönderildi. Bu durumda yazılıma eklenen ürünlerin tek bir merkezden yönetilmesi sağlandı. Bu yapılırken de uygulama, metotlarda ürün tiplerine göre hata yönetimi veya ürün tipine göre yönlendirme (switch) gibi bakımı zorlaştırıcı işlemlerden arındırılmış oldu.

Faydalı olması dileğiyle…

Açık – Kapalı Prensibi (OCP – Open Closed Principle)

Bu serinin ilk makalesi olan yazılım tasarım prensipleri (SOLID) içerisinde kısa tanım olarak bahsettiğim açık – kapalı prensibini (open – closed principle) örneklendirerek açıklamak istiyorum.

Yukarıdaki görselde müşterilerin genel davranışlarından biri olan projeden sürekli talepte bulunma isteği resmedilmiş. Müşteri açısından bakıldığında birçok talep yapıyor olmak gayet olağan bir davranıştır. Çünkü gerçek hayatta bir fabrika veya başka bir kurum müşteriniz olabilir ve bu kurumlar da insanlar gibi yaşayan birer organizma gibidir. İş yapış şekli, yöntemler, ürünler, tasarımlar ve daha sayamadığım bu gibi konular zaman içinde sürekli değişime uğrar. Yazılımın da bu talepleri hızlı ve esnek bir biçimde karşılıyor olması beklenir.

Bir uygulama senaryosu üzerinden devam edelim. Örneğimizde çeşitli ürünler üreten bir fabrikamız olduğunu düşünelim. Bu fabrikanın piyasa ve müşteri ihtiyaçlarını karşılamak amacı ile her ay onlarca yeni tip ürünü piyasaya sürdüğünü düşünelim. Şimdi senaryoyu açık – kapalı prensibine (open – closed principle) uymayan şekilde kodlayalım ve programın ilerleyen zamanlarda yaşayacağı problemleri görelim.

İlk aşamada fabrikada üretilen ürünlerin tanımlanması ile başlayalım. Aşağıdaki kodda 2 adet ürün tanımlandı fakat buradaki ürünlerin sayısı istenildiği kadar çoğaltılabilir.

namespace UretimSenaryosu
{
    public class Urun1
    {
        public void Uret()
        {
            //Burada ürün 1'e özel üretim metotları çalıştırılıyor.
            Console.WriteLine("Ürün 1'e özel üretim yapıldı.");
        }
    }

    public class Urun2
    {
        public void Uret()
        {
            //Burada ürün 2'ye özel üretim metotları çalıştırılıyor.
            Console.WriteLine("Ürün 2'ye özel üretim yapıldı.");
        }
    }
}

Yukarıda görülen “Uret” metodu sembolik bir metottur. Bu metot içerisinde sadece o ürüne özel bazı akışlar, o ürüne özel kurallar, varsa özel hesaplamalar ve/veya ürüne özel olarak şu an aklıma gelmeyen diğer her türlü işlevlerin bulunduğunu düşünebiliriz. Tabi ki bunlar sektöre göre değişim gösterecektir.

İkinci aşamada fabrikadaki ürünleri üreten Fabrika isimli bir sınıfımız olduğunu düşünelim ve bu sınıfı kodlayalım.

namespace UretimSenaryosu
{
    public class Fabrika
    {
        public void Urun1Uret(Urun1 urun)
        {
            //Ürün 1 üretiliyor.
            urun1.Uret();
        }

        public void Urun2Uret(Urun2 urun)
        {
            //Ürün 2 üretiliyor.
            urun2.Uret();
        }
    }
}

Son olarak da Program.cs içerisindeki kodları yazalım.

namespace UretimSenaryosu
{
    public class Uretim
    {
        public static void Main(string[] args)
        {
            Urun1 urun1 = new Urun1();
            Urun2 urun2 = new Urun2();

            Fabrika fabrikam = new Fabrika();

            fabrikam.Urun1Uret(urun1);
            fabrikam.Urun2Uret(urun2);
        }
    }
}

Senaryo 1 – Üretim Hattına Yeni Bir Ürün Eklendi.

Buraya kadar basit anlamda bir üretim senaryosu oluşturduk. Şimdi ise üretim hattımıza Ürün 3 isimli yeni bir ürün geldiğini düşünelim. Bu yeni ürüne göre programımız aşağıdaki gibi şekillenecek. İlk önce yeni ürüne ait sınıfımızı oluşturuyoruz.

namespace UretimSenaryosu
{
    public class Urun1
    {
        public void Uret()
        {
            //Burada ürün 1'e özel üretim metotları çalıştırılıyor.
            Console.WriteLine("Ürün 1'e özel üretim yapıldı.");
        }
    }

    public class Urun2
    {
        public void Uret()
        {
            //Burada ürün 2'ye özel üretim metotları çalıştırılıyor.
            Console.WriteLine("Ürün 2'ye özel üretim yapıldı.");
        }
    }

    public class Urun3
    {
        public void Uret()
        {
            //Burada ürün 3'e özel üretim metotları çalıştırılıyor.
            Console.WriteLine("Ürün 3'e özel üretim yapıldı.");
        }
    }
}

Sonrasında ise üretim işlemini yapan Fabrika sınıfımıza Ürün 3 için gerekli üretim kodlarını ekliyoruz.

namespace UretimSenaryosu
{
    public class Fabrika
    {
        public void Urun1Uret(Urun1 urun)
        {
            //Ürün 1 üretiliyor.
            urun1.Uret();
        }

        public void Urun2Uret(Urun2 urun)
        {
            //Ürün 2 üretiliyor.
            urun2.Uret();
        }

        public void Urun3Uret(Urun3 urun)
        {
            //Ürün 3 üretiliyor.
            urun3.Uret();
        }
    }
}

Son olarak da Program.cs içerisine Ürün 3 için üretim yapma komutunu veriyoruz.

namespace UretimSenaryosu
{
    public class Uretim
    {
        public static void Main(string[] args)
        {
            Urun1 urun1 = new Urun1();
            Urun2 urun2 = new Urun2();
            Urun3 urun3 = new Urun3();

            Fabrika fabrikam = new Fabrika();

            fabrikam.Urun1Uret(urun1);
            fabrikam.Urun2Uret(urun2);
            fabrikam.Urun3Uret(urun3);
        }
    }
}

Peki her ürün eklendiğinde Fabrika sınıfımızı bu şekilde değiştirerek her yeni ürün için bir üretim yapma kodu mu gireceğiz. Elbette işler bu şekilde de yürüyebilir fakat bu açık – kapalı prensibine (open – closed principle) oldukça ters bir mimari oluşturmaktadır. Her seferinde tüm kodların elden geçmesi belli bir zaman sonra programın bakımında zorluklar çıkaracak ve işler iyice karmaşık hale gelecektir. Oysa ki yeni bir ürün eklendiğinde sadece ilgili yeni ürüne ait sınıfımızı (model olarak da bilinir) eklesek ve üretim yapan Fabrika isimli sınıfa hiç dokunmasak nasıl olurdu?

Senaryo 2 – Aynı Kodun Açık – Kapalı Prensibine (Open – Closed Principle) Göre Yazılması

Açık – kapalı prensibine (open – closed principle) göre kodlama işlemi soyut sınıflar (abstract class) kullanılarak da yapılabilir fakat ben arayüz (interface) uygulamalarını daha çok sevdiğim için bu yöntemle yapacağım. İlk Aşama olarak tekrar iki adet ürün sınıfımızı yazalım. Bu sefer öncekinden farklı olarak bir üretim arayüzü yazacağız ve tüm sınıfların bu arayüzü uygulamasını sağlayacağız.

namespace UretimSenaryosu
{
    public interface IUrun
    {
        void Uret();
    }

    public class Urun1: IUrun
    {
        public void Uret()
        {
            //Burada ürün 1'e özel üretim metotları çalıştırılıyor.
            Console.WriteLine("Ürün 1'e özel üretim yapıldı.");
        }
    }

    public class Urun2: IUrun
    {
        public void Uret()
        {
            //Burada ürün 2'ye özel üretim metotları çalıştırılıyor.
            Console.WriteLine("Ürün 2'ye özel üretim yapıldı.");
        }
    }
}

Sınıflarımız bir arayüzü uygulayacak şekilde tanımladıktan sonra üretim işleminin yapıldığı Fabrika sınıfını da açık – kapalı prensibine (open – closed principle) göre yazalım. Farkedileceği üzere artık Fabrika sınıfının içerisinde sadece bir adet IUrun tipinde parametre kabul eden bir metodumuz var ve bu metot tüm ürünlerin üretimini yapabiliyor.

namespace UretimSenaryosu
{
    public class Fabrika
    {
        public void Uret(IUrun urun)
        {
            urun.Uret();
        }
    }
}

Son olarak da Program.cs içerisindeki kodları açık – kapalı prensibine (open – closed principle) göre düzenleyelim. Buradaki değişiklikte de görüleceği üzere üretilecek tüm ürünleri Fabrika sınıfı içerisindeki Uret metoduna gönderdik. Buradaki Uret metodu artık ürün bağımsız şekilde kendisine gönderilen her ürün için üretim yapabiliyor.

namespace UretimSenaryosu
{
    public class Uretim
    {
        public static void Main(string[] args)
        {
            Urun1 urun1 = new Urun1();
            Urun2 urun2 = new Urun2();

            Fabrika fabrikam = new Fabrika();

            fabrikam.Uret(urun1);
            fabrikam.Uret(urun2);
        }
    }
}

Senaryo 3 – Üretim Hattına Yeni Bir Ürün Eklendi.

Şimdi bu yeni kodlama yapısında fabrikadaki üretim hattına yeni bir ürün eklendiğinde yapılacak işlemleri aşağıda gösterelim. Her zamanki gibi ilk aşama olarak ürünümüzü ekliyoruz.

namespace UretimSenaryosu
{
    public interface IUrun
    {
        void Uret();
    }

    public class Urun1: IUrun
    {
        public void Uret()
        {
            //Burada ürün 1'e özel üretim metotları çalıştırılıyor.
            Console.WriteLine("Ürün 1'e özel üretim yapıldı.");
        }
    }

    public class Urun2: IUrun
    {
        public void Uret()
        {
            //Burada ürün 2'ye özel üretim metotları çalıştırılıyor.
            Console.WriteLine("Ürün 2'ye özel üretim yapıldı.");
        }
    }

    public class Urun3: IUrun
    {
        public void Uret()
        {
            //Burada ürün 3'e özel üretim metotları çalıştırılıyor.
            Console.WriteLine("Ürün 3'e özel üretim yapıldı.");
        }
    }
}

Fabrika sınıfımızda ve içerisindeki Uret metodunda herhangi bir değişiklik yapmıyoruz. Program.cs içerisine de ürün 3 için üretim yapma kodunu yazıyoruz ve projemizin içerisine yeni ürünümüzü başarı ile eklemiş oluyoruz.

namespace UretimSenaryosu
{
    public class Uretim
    {
        public static void Main(string[] args)
        {
            Urun1 urun1 = new Urun1();
            Urun2 urun2 = new Urun2();
            Urun3 urun3 = new Urun3();

            Fabrika fabrikam = new Fabrika();

            fabrikam.Uret(urun1);
            fabrikam.Uret(urun2);
            fabrikam.Uret(urun3);
        }
    }
}

Görüldüğü üzere projeye yeni ürünü eklemek için sadece ürün tanımlarını içeren sınıfın tanımlanması yeterli oldu. Projede ne kadar fazla karmaşık yapı olursa olsun, proje bu prensibe göre kodlandığında bizim için ileriki bakım aşamaları sancılı olmaktan çıkacaktır.

Faydalı olması dileğiyle…

Tek Sorumluluk Prensibi (SRP – Single Responsibility Principle)

Bugünkü yazımda daha önce bahsetmiş olduğum yazılım tasarım prensiplerinden (SOLID) tek sorumluluk prensibi (single responsibility principle) üzerinde duracağım. Yukarıdaki görselde görüleceği gibi bazı durumlarda içerisinde birçok işlev barındıran sınıflar (class) aynı bir isviçre çakısı gibi size pratiklik sağlayabilir. Fakat işler büyümeye ve daha karmaşık hale gelmeye başladığında bu işlevler güçsüz kalmaya ve sizi bazı noktalarda kilitlemeye başlar.

Bu konuyu basit bir şekilde örneklendirecek olursak loglama, mail gönderme ve kimlik doğrulama işlemlerini kendi üzerine toplamış bir sınıf kodlayabiliriz.

public class Siparis
{
    public void SiparisKaydet()
    {
        if (KimlikDogrula())
        {
            try
            {
                 // Sipariş kaydetme işlemleri burada yapılıyor

                 MailGonder("Siparişiniz başarı ile kaydedildi", ...);
            }
            catch (Exception e)
            {
                 Log("Sipariş kaydedilirken hata oluştu.", e, ...);
            }
        }
    }

    public bool KimlikDogrula()
    {
        // Kimlik doğrulama ile ilgili işlemler...
    }

    public void MailGonder(string mesaj, ...)
    {
        // Mail gönderimi ile ilgili işlemler...
    }

    public void Log(string mesaj, Exception e, ...)
    {
        // Loglama ile ilgili işlemler...
    }
}

Yukarıdaki örnekte görüleceği üzere sipariş sınıfımız, siparişi kaydetmenin yanı sıra siparişi kaydeden kişinin kimlik doğrulama işlemleri, sipariş kaydedildiğinde ilgililere mail gönderilmesi ve eğer bir hata ile karşılaşılırsa çıkan hatanın loglanması gibi fazladan bir takım görevler de üstlenmiş durumda. Bu fazladan görev üstlenme durumu bizim sipariş sınıfımızı olması gerekenden daha kalabalık bir hale getirdiği gibi, kimlik doğrulama, mail gönderimi ve loglama işlemlerinin diğer sınıflarda kullanılmasını engelleyerek kod tekrarlarına neden olmaktadır.

Şimdi bu örneği tek sorumluluk prensibine (single responsibility principle) göre böldüğümüzde oluşacak yeni halini aşağıya kodlayalım.

public class Siparis
{
    public void SiparisKaydet()
    {
        if (GuvenlikIslemleri.KimlikDogrula())
        {
            try
            {
                 // Sipariş kaydetme işlemleri burada yapılıyor

                 MailIslemleri.MailGonder("Siparişiniz başarı ile kaydedildi", ...);
            }
            catch (Exception e)
            {
                 LoglamaIslemleri.Log("Sipariş kaydedilirken hata oluştu.", e, ...);
            }
        }
    }
}

// Diğer sınıfları static olarak tanımladım. Projenin genel yapısına göre bu durum değişebilir.

public static class GuvenlikIslemleri
{
    public static bool KimlikDogrula()
    {
        // Kimlik doğrulama ile ilgili işlemler...
    }
}

public static class MailIslemleri
{
    public static void MailGonder(string mesaj, ...)
    {
        // Mail gönderimi ile ilgili işlemler...
    }
}

public static class LoglamaIslemleri
{
    public static void Log(string mesaj, Exception e, ...)
    {
        // Loglama ile ilgili işlemler...
    }
}

Bu örnekte görüldüğü üzere sipariş sınıfımız artık sipariş ile ilgili olmayan kimlik doğrulama, mail gönderme ve loglama işlemlerini kendi içinde barındırmıyor, bu işlemler için ilgili diğer sınıflardaki metotları çağırıyor. Bu şekilde sipariş sınıfımız sadece kendi işini yapan sade bir sınıf haline geldi, kod kalabalığından arındı. Ayrıca diğer işlemler de ayrı sınıflar içerisine taşındığından projenin her yerinde kullanılabilir duruma geldi ve böylece kod tekrarının da önüne geçilmiş oldu.

Faydalı olması dileğiyle…

Yazılım Tasarım Prensipleri

Hayatta başarılı olmak için nasıl ki bazı prensiplere sahip olmak gerekiyorsa, bana göre bir yazılım projesinin de bazı prensiplere dayalı olarak geliştirilmesi gerekiyor. Tabi söz konusu olan bir sektör ise her sektörde olduğu gibi yazılım sektöründe de bu prensiplerin belli standartlar kapsamında yapılması en doğal olanıdır. Bu yazının kapsamında daha önce araştırmış olduğum fakat unuttuğum bir konu olan yazılım tasarım prensipleri (SOLID) kavramına tekrardan bir göz atacağım ve notlarım arasına eklemiş olacağım.

İlk önce SOLID kelimesini oluşturan kavramları ve bunların kısa tanımlarını incelemek istiyorum. Sonrasında her bir konu için ayrı bir makale hazırlayarak konuları örnekler ile detaylandırmaya çalışacağım.

(S) Single Responsibility Principle

Özetle bir sınıfın (class) sadece bir işten sorumlu olmasıdır. Örneğin iş emri ile ilgili bir sınıf tanımladığımızı düşünürsek, bu sınıftaki metotlar sadece iş emri ile ilgili olmalıdır. Söz gelimi iş emrinin durumunun değiştirilmesi, açılması gibi işlemler bu sınıf tarafından yapılırken, bu hareketlerin loglanması, bildirimlerinin ilgili kişilere gönderilmesi gibi işler başka sınıflar tarafından yapılmalıdır. Konu ile ilgili detay makaleye buradan ulaşabilirsiniz.

(O) Open-Closed Principle

Genel kabul gören slogan tipinde bir tanımı vardır. Bir sınıfın genişletilmeye açık olması (Open) fakat değiştirilmeye kapalı olması (Closed) prensibidir. Bu prensibe göre projenin ileri safhalarında doğacak ihtiyaçlarının var olan sınıflardaki kodlara ekleme yapılarak veya kodlarda değişiklik yapılarak karşılanması bu prensibe aykırıdır. Var olan sınıf kendisinde hiçbir yazılımsal değişiklik yapılmadan, belli bir arayüzü (Interface) uygulayan ve sonradan eklenebilecek tüm diğer nesnelerle uyumlu çalışabilmelidir. Konu ile ilgili detay makaleye buradan ulaşabilirsiniz.

(L) Liskov Substitution Principle

Temel anlamda kalıtım ile türetilen nesneler (instance) ile tüm diğer nesnelerin türetildiği ana nesne birbiri ile yer değiştirildiğinde aynı davranışı sergilemeleri prensibine dayanır. Yani türetilen sınıftan örneklenmiş nesneyi ana sınıf tipinden bir parametre alan metoda gönderdiğimizde hata alıyorsak veya hata almamak için hata yakalama bloklarını kullanarak bazı sınıflara özel istisnai kodlar yazıyorsak, tasarımda bu prensibi göz ardı etmişiz demektir.

(I) Interface Segregation Principle

Uygulamamızda sınıfların uygulaması gereken arayüzler birbiri ile ilişkili özelliklerine göre gruplandırılmalıdır. Bu aşamada çok fazla üyesi olan bir arayüz tasarlamak yerine bir sorunu çözecek ve sadece o soruna odaklanmış özellik ve metotları içeren arayüzler oluşturulmalıdır.

(D) Dependency Inversion Principle

Projelerdeki üst seviye nesnelerin daha alt seviyede işlem yapan nesnelere olan bağımlılığının alt sınıftan üste doğru değil de üst sınıftan alta doğru ilerlemesi prensibine karşılık gelir. Bu tür bağımlılıklar sınıfın kendisi ile değil de arayüzlerle yapılmak sureti ile alt seviye sınıflar ile bağlantısı gevşek hale getirilmiş olur.

Bu makale konuya tanım boyutunda bir giriş makalesi formatında oldu. Bazı özellikleri örneklerle açıklamadığım için anlaşılması güç olmuş ve tanımlara boğulmuş bir makale havası yansıtmış olabilirim. Bu sorunu ileride her bir konuyu ayrı birer makale olarak ele alıp örneklendirerek çözmeyi planlıyorum. Ana hedefim makaleleri olabildiğince kısa tutmak. Bu yüzden tüm konuları detaylı olarak burada anlatmak yerine ayrı ayrı bölümlendirmeyi daha cazip buluyorum.

Faydalı olması dileğiyle…