S.O.L.I.D: Nesne Yönelimli Programlamanın 5 Prensibi

S.O.L.I.D ilk olarak Robert C. Martin (Uncle Bob) tarafından ortaya atılan ve Nesne Yönelimli Programlamanın prensiplerini içeren bir akronimdir.

Yazılım dizaynı yapılırken, bu 5 prensip bir arada kullanıldığında, geliştiriciler tarafından yazılımın daha kolay geliştirilmesi, bakımının daha kolay yapılması ve kodun daha kolay genişlemesi hedeflenir.

Ayrıca bu prensipler Code Smell dediğimiz yazılım tasarımı kusurlarını en aza indirir ve kodun daha rahat biçimde refactor edilmesini sağlar. Ayrıca bu 5 kural Agile ve Adaptive Software Development konularının bir parçasıdır.

S.O.L.I.D Prensipleri

Öncelikle akronim içindeki tüm harflerin anlamlarını öğrenerek başlayalım.

  • S - Single-responsiblity principle (Tek sorumluluk prensibi)
  • O - Open-closed principle (Açıklık-kapalılık prensibi)
  • L - Liskov substitution principle (Liskov'un yer değiştirme prensibi)
  • I - Interface segregation principle (Arayüz Ayrımı prensibi)
  • D - Dependency invertion principle (Bağlılığı Tersine Çevirme prensibi)

Şimdi her prensibe kısaca tek tek göz atıp S.O.L.I.D. prensiplerini öğrenmenin bizi neden daha iyi geliştiriciler yapacağını anlayacağız.

Single-responsiblity Principle (Tek sorumluluk Prensibi)

Kısaca S.R.P. şunlardan bahseder:

Bir sınıfı değiştirmek için sadece tek bir sebep olması gerekir, kısaca bir sınıf sadece tek bir iş için var olabilir.

Örnek olarak bazı şekillerimiz olsun ve biz bu şekillerin alanlarını toplamak isteyelim, Pekala bu çok kolay değil mi?

class Circle {  
    public $radius;

    public function __construct($radius) {
        $this->radius = $radius;
    }
}

class Square {  
    public $length;

    public function __construct($length) {
        $this->length = $length;
    }
}

Öncelikle Circle ve Square adında şekil sınıflarımızı oluşturduk ve sınıfların constructor metodlarında gerekli parametrelerimizi ayarladık.

Bir sonraki adımda, AreaCalculator sınıfımızı oluşturuyoruz ve tüm şekillerinin alanını toplayan sum metodumuzu yazıyoruz.

class AreaCalculator {

    protected $shapes;

    public function __construct($shapes = array()) {
        $this->shapes = $shapes;
    }

    public function sum() {
        // Tüm alanların toplanma kısmı
    }

    public function output() {
        return implode('', array(
            "<h1>",
                "Verilen şekillerin alanlarının toplamı: ",
                $this->sum(),
            "</h1>"
        ));
    }
}

AreaCalculator sınıfını kullanmak için, AreaCalculator sınıfını instantiate edip, tüm şekillerini içine array olarak veriyoruz, ve sonucu sayfanın en altında gösteriyoruz.

$shapes = array(
    new Circle(2),
    new Square(5),
    new Square(6)
);

$areas = new AreaCalculator($shapes);

echo $areas->output();  

Şimdi bu noktada bir problem bulunuyor, AreaCalculator sınıfı aslında başka bir sınıfta olması gereken bir iş yapıyor ve çıktı işini de üsleniyor. Peki, eğer olur da bir gün farklı bir formatda çıktı vermek isteseydik mesela JSON yada XML, mecburen şu anki kod ile girip AreaCalculator sınıfını düzenlemek zorunda kalacaktık.

Şuan Tek Sorumluluk Prensibini ihlal ettik, peki çözüm nedir?

SumCalculatorOutputter isminde bir sınıf yaratıp çıktıyı nasıl oluşturmak istiyorsak çıktı verecek olan kodlarımızı bu sınıfta bulundurmamız gerekli.

$shapes = array(
    new Circle(2),
    new Square(5),
    new Square(6)
);

$areas = new AreaCalculator($shapes);
$output = new SumCalculatorOutputter($areas);

echo $output->JSON();  
echo $output->HAML();  
echo $output->HTML();  
echo $output->JADE();  

Şimdi, sonuç çıktısını almak için nasıl bir yol izlersek izleyelim, çıktı işi sadece SumCalculatorOutputter sınıfı tarafından yapılacak.

Open-closed principle (Açıklık-kapalılık prensibi)

Nesneler genişlemeye açık fakat değişikliğe kapalı olmalıdır.

Kısaca bunun anlamı sınıfın kendi içinde değişiklik yapmadan sınıfın kolay bir biçimde geliştirilebilmesidir. AreaCalculator sınıfımıza, özellikle sum metoduna bakalım.

public function sum() {  
    foreach($this->shapes as $shape) {
        if(is_a($shape, 'Square')) {
            $area[] = pow($shape->length, 2);
        } else if(is_a($shape, 'Circle')) {
            $area[] = pi() * pow($shape->radius, 2);
        }
    }

    return array_sum($area);
}

Eğer sum metodumuzun daha fazla şekli toplamasını isteseydik daha fazla if-else bloğu eklemek zorunda kalıp Açıklık-kapalılık prensibine karşı gelecektik.

AreaCalculator sınıfının içindeki sum metodunu iyileştirmenin yolu alan bulma kodunu tüm Shape sınıflarının kendi içine koymak olurdu.

class Square {  
    public $length;

    public function __construct($length) {
        $this->length = $length;
    }

    public function area() {
        return pow($this->length, 2);
    }
}

Square sınıfı için yaptığımız alan hesaplama metodunun aynısı Circle sınıfı içinde yapılmalıdır. Şimdi alanları toplamak AreaCalculator sınıfımız için çok daha kolay bir hal aldı.

public function sum() {  
    foreach($this->shapes as $shape) {
        $area[] = $shape->area;
    }

    return array_sum($area);
}

Şimdi başka bir Shape sınıfı yaratıp ve kodumuzda değişiklik yapmadan onu sum metoduna gönderebiliriz. Ancak, şimdi başka bir problem meydana geldi. AreaCalculator sınıfına gönderilen nesnenin gerçek bir Şekil olup area metoduna sahip olduğuna nasıl emin olabileceğiz?

Arayüzler kullanmak S.O.L.I.D. prensiplerinin ayrılmaz bir parçasıdır. Küçük bir örnekle Arayüz yaratıp, tüm Şekillerde bu arayüzü uygulayacağız.

interface ShapeInterface {  
    public function area();
}

class Circle implements ShapeInterface {  
    public $radius;

    public function __construct($radius) {
        $this->radius = $radius;
    }

    public function area() {
        return pi() * pow($this->radius, 2);
    }
}

AreaCalculator sınıfında buraya gönderilen nesnelerin gerçekten ShapeInterface'i implement etmiş nesneler olup olmadığını kontrol edeceğiz. Eğer değilse Exception fırlatacağız.

public function sum() {  
    foreach($this->shapes as $shape) {
        if(is_a($shape, 'ShapeInterface')) {
            $area[] = $shape->area();
            continue;
        }

        throw new AreaCalculatorInvalidShapeException;
    }

    return array_sum($area);
}
Liskov substitution principle (Liskov'un yer değiştirme prensibi)

Her türetilmiş sınıf türedildiği sınıf ile yer değiştirilebilir olmalı.

Hala AreaCalculator örneğimiz üstünden devam ediyoruz, AreaCalculator sınıfımızdan türeyen bir VolumeCalculator sınıfımız olsun:

class VolumeCalculator extends AreaCalulator {  
    public function __construct($shapes = array()) {
        parent::__construct($shapes);
    }

    public function sum() {
        // burada hacim hesaplanıyor ve bir dizi olarak geri döndürülüyor
        return array($summedData);
    }
}

SumCalculatorOutputter sınıfımız:

class SumCalculatorOutputter {  
    protected $calculator;

    public function __constructor(AreaCalculator $calculator) {
        $this->calculator = $calculator;
    }

    public function JSON() {
        $data = array(
            'sum' => $this->calculator->sum();
        );

        return json_encode($data);
    }

    public function HTML() {
        return implode('', array(
            '<h1>',
                'Verilen şekillerin alanlarının toplamı: ',
                $this->calculator->sum(),
            '</h1>'
        ));
    }
}

Eğer örneğimizi aşağıdaki şekilde çalıştırmaya çalışırsak:

$areas = new AreaCalculator($shapes);
$volumes = new AreaCalculator($solidShapes);

$output = new SumCalculatorOutputter($areas);
$output2 = new SumCalculatorOutputter($volumes);

Kod çalışacaktır fakat, $output2 objesinden HTML metodunu çalıştırdığımızda E_NOTICE hatası alıp array'i string gibi kullanmaya çalıştığımız hakkında bir uyarı alacağız :)

Bunu çözmek için, VolumeCalculator sınıfından array döndürmek yerine, şöyle yapacağız:

public function sum() {  
    return $summedData;
}

Toplanmış veri float, double veya integer olabilir.

Interface segregation principle (Arayüz Ayrımı prensibi)

Bir sınıf asla bir arayüzü veya kullanmasına gerek olmayan bir metodu uygulamaya zorlanmamalı.

Hala şekil örneğimizden devam ediyoruz. Şekillerimizin hacimlerini hesaplıyoruz bundan dolayı ShapeInterface arayüzümüze volume şeklinde uygulanması zorunlu bir metod daha ekleyebiliriz.

interface ShapeInterface {  
    public function area();
    public function volume();
}

Şimdi yarattığımız her şekil volume metodunu uygulamak zorunda kalacak. Fakat kare düz bir şekildir yani aslında hacmi yoktur ve bundan dolayı volume metodunu uygulamamalıyız, Fakat şimdi düzenlediğimiz arayüz volume metodunu uygulamak konusunda bizi zorluyor, ne yapacağız?

Arayüz Ayrımı prensibini ihlal ediyoruz, böyle yapmaktansa SolidShapeInterface isminde bir arayüz yaratıp ekstra olarak volume metodunu buraya ekleyeceğiz.

interface ShapeInterface {  
    public function area();
}

interface SolidShapeInterface {  
    public function volume();
}

class Cuboid implements ShapeInterface, SolidShapeInterface {  
    public function area() {
        // Küboid'in yüzey alanını hesaplayan kod
    }

    public function volume() {
        // Küboid'in hacmini hesaplayan kod
    }
}

Bu çok daha iyi bir yaklaşım olacaktır. Fakat burada type-hinting konusunda küçük bir mantıksal problem ortaya çıkıyor oluşturduğumuz objemiz ShapeInterface mi yoksa SolidShapeInterface mi?

Bu durumla başa çıkmak için bir arayüz daha oluşturabiliriz, belki ManageShapeInterface bunu tüm şekillere uygularız ve bu şekilde tek bir noktadan tüm şekillerimizi yönetebiliriz. Örnek olarak:

interface ManageShapeInterface {  
    public function calculate();
}

class Square implements ShapeInterface, ManageShapeInterface {  
    public function area() { /* Alan hesaplayan kodlar */ }

    public function calculate() {
        return $this->area();
    }
}

class Cuboid implements ShapeInterface, SolidShapeInterface, ManageShapeInterface {  
    public function area() { /* Alan hesaplayan kodlar */ }
    public function volume() { /* Hacim hesaplayan kodlar */ }

    public function calculate() {
        return $this->area() + $this->volume();
    }
}
Dependency invertion principle (Bağlılığı Tersine Çevirme prensibi)

Objeler somut objelere değil soyutlamalara bağımlı olmalıdırlar, Kısaca yüksek seviyedeki modüller daha düşük seviyedeki modüllere bağımlı olmamalı, soyutlamalara bağlı olmalıdır.

Kulağa biraz karışık gelmiş olabilir fakat, bunu anlamak bir örnekle oldukça kolaydır.

class PasswordReminder {  
    private $dbConnection;

    public function __construct(MySQLConnection $dbConnection) {
        $this->dbConnection = $dbConnection;
    }
}

İlk olarak MySQLConnection düşük seviyeli bir modüldür, PasswordReminder ise yüksek seviyeli. Fakat bağlılığı tersine çevirme prensibine göre soyutlamalara bağımlı olunmalıdır. Bu kod parçası bu kuralı ihlal etmektedir. PasswordReminder sınıfı MySQLConnection sınıfına bağımlı olmaya zorlanmaktadır.

Örneğin proje geliştirilme aşamasındayken Database Engine üstünde bir değişiklik yapıldı ve PostgreSQL kullanılma kararı alındı ve PostgreSQLConnection sınıfı geliştirildi, şimdi girip PasswordReminder sınıfı dahil her yerde değişiklik yapmamız gerekecek. Bu şekilde 2. prensibimiz olan Açıklık-kapalılık prensibini ihlal ediyoruz.

PasswordReminder sınıfının veri kaynağı olarak ne kullandığımız hakkında bir bilgisi olmamalıdır, bunun için bir arayüz yaratacağız:

interface DBConnectionInterface {  
    public function connect();
}

Arayüzümüzün içinde connect metodu var ve MySQLConnection sınıfı bu arayüzü uygulamalıdır.

Artık sınıfımızın constructor metodunda MySQLConnection sınıfına bağımlı olmayıp DBConnectionInterface arayüzüne bağımlı olacağız, böylece bu kontratı uygulayan tüm sınıflar kullanılabilir.

Böylece PasswordReminder sınıfımız somut MySQLConnection sınıfına bağımlı olmayıp, soyut DBConnectionInterface arayüzüne bağımlı oldu.

Bu şekilde bağlılığı tersine çevirme prensibini ihlal etmemiş olduk.

class MySQLConnection implements DBConnectionInterface {  
    public function connect() {
        return "Veritabanı bağlantısı";
    }
}

class PasswordReminder {  
    private $dbConnection;

    public function __construct(DBConnectionInterface $dbConnection) {
        $this->dbConnection = $dbConnection;
    }
}

Yukarıdaki küçük kod snippetinde hem yüksek seviyeli hem düşük seviyeli modüller soyutlamaya bağımlı olmuşlardır.


Bu makale çoğunlukla çok sevdiğim bir makalenin çevirisidir. Orjinal içeriğe referanslar kısmındaki ilk referanstan gidebilirsiniz.

Zaman ayırdığınız için teşekkürler.

Batıkan Senemoğlu

Read more posts by this author.

İstanbul, Turkey https://batikansenemoglu.com