Sayfalar

29 Mayıs 2013 Çarşamba

SOLID Prensipleri (SOLID Principles)

Yazılım tarihi boyunca geliştirilen yazılımlarda istekler, gereksinimler ve ortaya çıkan sorunları çözmek için pek çok kod tasarımı yapılmıştır. Bu tasarımlar yazılım sosyetesinde paylaşıldıkça ve üzerinde yapılan tartışmalarla şekillenmiş ve belli standartlar haline gelmiştir. Daha sonra bu kalıplar sınıflandırılarak adına da tasarım kalıpları (Design Patterns) denilmiş ve yazılım dünyasınca tescil edilmiştir. Herkes tarafından kabul edilen bu kalıplar sayesinde kötü tasarımdan uzak durmak isteyen bir kişinin uygulaması gereken tasarım belirlenmiştir.

Bu kalıplar iş sürecine katılan parçaların yapılarının nasıl olması gerektiğini ve birbirleriyle nasıl etkileşim kurması gerektiğini kabaca öngörmektedir. Ancak bu yapıları oluşturacak sınıfların nasıl modelleneceği ile ilgili detaylı bir fikir vermemektedir. Dolayısıyla tasarım kalıplarına uyarak geliştirme yapılsa da bazı temel sorunların meydana gelebildiği görülmüştir. Bu temel sorunlar ise üç ana başlık altında gruplandırılmıştır:
  1. Esnemezlik (Rigidity): Kullanılan tasarımın geliştirilememesi ve ekleme yapılamaması
  2. Kırılganlık (Fragility): Bir yerde Yapılan değişikliğin başka bir yerde sorun çıkartması
  3. Sabitlik (Immobility): Geliştirilmiş modülün başka yerde tekrar kullanılabilir (reusable) olmaması
Yukarıda saydığımız üç temel durumun tasarımdaki sınıfların birbirleri ile sıkı sıkıya bağlı (strongly coupled) olduğu durumlarda, yani bir sınıfın diğer bir sınıfı direkt olarak kullandığı durumlarda ortaya çıktığı görülmüştür. Bu temel sorunları çözmek için sınıfların nasıl modellemesi gerektiğini öngören prensipler de ortaya atılmış ve buna da Sınıf Tasarımı Prensipleri (Principles of Class Design) denilmiştir. Bu prensipler ile yapılmaya çalışılan şey özünde sınıflar arasındaki bağımlılığı azaltmaya hatta yok etmeye çalışmaktır.
Öngörülen en ideal sistem birbirilerini hiç bilmeyen sınıfların iş yaptığı sistemdir. Bu aslında tam da ideal bir kurumsal şirket yapılanmasına benzemektedir.

İyi Örnek:


İdeal bir şirkette personelin kim olduğunun önemi yoktur, personelin bir sıfatı vardır ve bu sıfatın görev tanımı bellidir, ne bunun dışına çıkılır, ne de eksik görev yapılır. Personeller arasında sıkı sıkıya olmayan (loosely coupled) bir bağ vardır. Bir iş yapılacağı zaman bu işi hangi sıfattaki personelin yapacağı biliniyordur, o sıfattaki personelden birine iş verilir ve o işi yapması beklenir. Bir personel yerine gelen personele iş basit bir oryantasyonla "öğretilir".

Kötü Örnek:


Oysa ideal olmayan (kurumsallaşamayan) şirket kadrolarında işi yapan personelin sıfatının önemi yoktur, personel birbirleriyle sıkı sıkıya bağlıdır. İşler kişiler aralarında hususi olarak paylaşılır. Örneğin muhasebe uzmanı olarak çalışan 2 personelden biri olan Ali sadece vergiler işlemlerini, Veli ise sadece bordrolar işlemlerini yapıyordur. Vergi işi yaptırmak için hususi olarak Ali Bey aranır. Ali Bey kimbilir kimlerle iletişime geçer? Ali Bey'in yerine gelecek olan kişiye işi şirket tarafından öğretilemez, kişinin işi "öğrenmesi" beklenir.

Çözüm: SOLID!


İşte Bu bağımlılıkları azaltmak için öngörülen 5 temel prensibe SOLID prensipleri (SOLID Principles) denir. SOLID kelimesi bu prensiplerin baş harflerinden meydana gelir. Bu prensipler ve kısaca açıklamaları şunlardır:
  • (S)ingle Responsibility : Her modül tek bir sorumluluğa sahip olmalı, olası bir değişiklik te tek bir nedene dayanmalı
  • (O)pen/Closed : Genişlemeye açık, modifikasyona kapalı tasarım kullanılmalı
  • (L)iskov's Substitution : Türeyen sınıfın üyeleri, temel sınıfın üyeleri ile tamamen yer değiştirebilir olmalı
  • (I)nterface Segregation : Arayüzler birbirinden ayrıştırılmalı
  • (D)ependency Inversion : Yüksek seviye sınıfların, düşük seviye sınıflara direkt olarak bağımlı olmamalı, arada bir soyut sınıf (abstract class) veya arayüz (Interface) konulmalı

(S)ingle Responsibility Prensibi


Bu prensip ile anlatılmak istenen şey bir sınıfın ileride sistemin genişlemesi durumunda kendi sorumluluk alanını büyütmeden olası genişlemelere önceden hazırlıklı olacak şekilde tasarlanması gerektiğidir.

Kavramlar soyut olduğundan bu konuyu örneklerle incelemek yerinde olacaktır. Örneğin verilen yakıtı yakıp enerji üreten bir reaktör arayüzü (IReactor) aşağıdaki gibidir:

interface IReactor
{
    double Gain(string oil, double amount);
}

Gain metoduna parametre olarak kaç litre benzin yakılacağı verilmekte, bu metodun içinde de benzinin yakılma süreci tanımlanmaktadır. Bu sistem yazıldığında reaktör yakıt olarak sadece "benzin" ile çalışıyordu. Ancak ilerleyen zamanlarda yanma süreçleri ve fiziksel özellikleri farklı yeni yakıtların keşfedilmesiyle birlikte reaktörün de bu yakıtları da yakma ihtiyacı ortaya çıkmıştır. Örneğin bazı yakıtlar katı olduğu gibi bazıları da gaz olabilir. Bu sadece miktar parametresi bunu karşılamaz. Eğer kodumuz şu şekilde olsaydı muhtemel tüm durumlara hazır olurdu:

interface IReactor
{
    void Gain(IOil oil);
}
interface IOil
{
    string Name  {get; set; }
    OilType OilType { get; set; }
}

Böylece yakıtları aşağıdaki gibi ayrı ayrı tanımlayabilir, IOil arayüzünü implement eden her türlü yakıt sınıfını Gain metoduna parametre olarak verebilirdik. Böyle bir altyapı ile mevcut yakıtları ve ileride keşfedilecek yeni yakıtlar için de arayüzlerde herhangi bir değişiklik yapmadan sadece IOil sınıfını implement eden yeni bir sınıf tanımlamak yeterli olacaktı:

//Benzin
class Fuel : IOil
{
    public double Amount { get; set; }
    public string Name {get; set; }
    public double Density {get;set; }
    public VolumeUnit Unit { get; set; } 
}

//Kömür
class Coal : IOil
{
    public double Amount { get; set; }
    public string Name {get; set; }
    public int Quality {get; set; }
    public double Amount { get; set; }
    public WeightUnit Unit { get; set; }
}

(O)pen/Closed Prensibi


Bu prensibe göre uygulama üzerine yeni modüller ekleyebilmeliyiz ama bunu mevcut kodları değiştirmeden yapmalıyız. Örneğin yukarıdaki örneğimizde Gain metodunun içinde her tip yakıt için ayrı ayrı yakma metotlarına yönlendirildiğini düşünelim, reaktör sınıfımız aşağıdaki gibi olabilir:

class Reactor : IReactor
{
    public void Gain(IOil oil)
    {
        switch (oil.OilType)
        {
            case OilType.Fuel : BurnFuel(oil); break;
            case OilType.Coal : BurnCoal(oil); break;
            default: throw new NotImplementedException();
        }
    }

    void BurnFuel(IOil coal)
    {
        ...
    }
    void BurnCoal(IOil coal)
    {
        ...
    }
}

Burada görüldüğü gibi ileride sisteme yeni bir yakıt eklendiğinde Reactor sınıfımıza yeni metot eklemeli ve Gain metodundaki switch bloğuna da de yenibir durum (case) eklemeliyiz. Bu da Closed prensibine aykırı bir durumdur. Bunun yerine IOil arayüzümüze Burn metodunu ekleyip,

interface IOil
{
    string Name  {get; set; }
    OilType OilType { get; set; }
    void Burn();
    
}
class Fuel : IOil
{
    ...
    public void Burn()
    {
        ...
    }
}
class Coal : IOil
{
    ...
    public void Burn()
    {
        ...
    }
}

Reactor sınıfımızı da aşağıdaki gibi değiştirerek bu durumu düzeltebiliriz

class Reactor : IReactor
{
    public void Gain(IOil oil)
    {
        oil.Burn();
    }

}

Böylece modifikasyona kapalı olarak genişlemeye açık olan bir sistem elde ederiz. Çünkü genişletmek için sadece yeni sınıflar eklememiz yeterli olacaktır. Burada OOP temel prensiplerinden polimorfizm görülmektedir. Command tasarım kalıbı da bu prensibe iyi bir örnektir.

(L)iskov's Substitution Prensibi


Bu prensip aslında Open/Close prensibinin bir uzantısı olmakla beraber, bir sınıftan yeni bir sınıf türetirken türedikleri sınıfın işlevini bozmadığından emin olmalımız gerektiğini söyler. Hemen bir örnek verelim.

class Rectangle
{
    protected int _Width;
    protected int _Height;

    public virtual int Width
    {
        get { return _Width; }
        set { _Width = value; }
    }
    public virtual int Height
    {
        get { return _Height; }
        set { _Height = value; }
    }

    public int getArea(){
        return _width * _height;
    } 
}

class Square : Rectangle 
{
    public override int Width
    {
        get { return _Width; }
        set 
        { 
            _Width = value;
            _Height = value;
        }
    }
    public override int Height
    {
        get { return _Height; }
        set 
        { 
            _Width = value;
            _Height = value;
        }
    }
}

class ConsoleApplication
{
    private static Rectangle getNewRectangle()
    {
        return new Square();
    }
    public static void Main ()
    {
        Rectangle r = getNewRectangle(); //geliştirici Rectangle tipinden inherit eden ne olursa olsun aynı özelliklerde davranacağına güvenerek Rectangle üzerinden işlem yapıyor.
        r.Width = 5;
        r.Height = 10;
        // kullanıcı r nin dörtgen olduğunu biliyor ve alanın 5 * 10 = 50 olmasını bekliyor 
        Console.WriteLine(r.getArea());
        // ama 50 yerine 100 görüyor ve şaşırıyor
}
}

Burada açıkça görülüyor ki prensip ihlal ediliyor.

(I)nterface Segregation Prensibi


En basit örnekle aşağıdaki gibi bir işçi arayüzü ile işçinin çalıştığını ve yemek yediğini öngörüyoruz.

public interface IWorker
{
    void Work(); //Çalış
    void Eat(); //Yemek ye
}
public class HumanWorker : IWorker
{
    public void Work() { ... }
    public void Eat() { ... }
}
public class RobotWorker : IWorker
{
    public void Work() { ... }
    public void Eat() { //Nasıl?! }
}

Evet adı üstünde her işçinin çalıştığı kesin ama örneğimizde görüldüğü gibi yemek yemeyen işçiler de var. İşte bu Interface Segregation prensibi der ki; Bunun gibi farklı ortak kavramları tek arayüz altına toplamak yerine farklı arayüzlere bölmeli, gereken arayüzü gereken sınıfa OOP temel prensiplerinden multi-inheritance kullanarak implement etmeliyiz. O halde çözümümüz şöyle olmalı:

public interface IWorkable
{
    void Work(); //Çalış
}
public interface IEatable
{
    void Eat(); //Yemek ye
}
public class HumanWorker : IWorkable, IEatable
{
    public void Work() { ... }
    public void Eat() { ... }
}
public class RobotWorker : IWorkable
{
    public void Work() { ... }
}

(D)ependency Inversion Prensibi


Son olarak bu prensip ile bir nesneyi bir yerde kullanacaksak o sınıfın tipini bodozlama kullanmak yerine o nesneyi türediği veya implement ettiği ara bir sınıf aracılığıyla kullanmalıyız.

Örneğin

public class Worker
{
    public void Work()
    {
        ...
    }
}
public class Manager
{
    public void Manage(Worker w)
    {
        w.Work();
    }
}

gibi bir yapımız varsa ve sisteme SuperWorker diye bir sınıf eklemek istersek Manager sınıfında 3 temel sorunumuz var demektir:
  1. SuperWorker sınıfını da yönetecek şekilde modifiye etmek zorundayız.
  2. Mevcut temel işlevlerin bozulma riski vardır.
  3. Birim testlerini yeniden yazmamız gerekir.

    Bunun yerine araya IWorker gibi bir arayüz koyup işlerimizi bunun üzerinden yapmak suretiyle aşağıdaki gibi bir yapıyı tercih etmeliyiz:

    public interface IWorker
    {
        void Work();
    }
    public class Worker : IWorker
    {
        public void Work()
        {
            ...
        }
    }
    public class SuperWorker : IWorker
    {
        public void Work()
        {
            ...
        }
    }
    public class Manager
    {
        public void Manage(IWorker w)
        {
            w.Work();
        }
    }

    Böylece Manager sınıfı diğer sınıflarla sıkı sıkıya bağlı olmayan (loosely coupled) bir hale gelecek ve tüm problemlerimizden kurtulacağız.

    Referanslar:


    SOLID (object-oriented design) @ Wikipedia
    Object-Oriented Design @ Wikipedia
    OO Design

    3 yorum: