OKUMA SÜRESİ 05:31

SOLID Prensibi

Nesne Yönelimli Programlama yapı taşı

Object Oriented Programming (OOP) yani Nesne Yönelimli Programlama’nın en temel prensibi olarak kabul edilir.

2015 yılında AslanobaLabs projesi bünyesinde yayınladığım yazımı proje iptal edildiği için kendi bloguma taşıdım. Biraz düzeltme ve güncellemeler de yaptım.

2000’lerin başında konuşulmaya başlanan, Michael Feathers tarafından tanıtılan ve İlk Beş Prensip diye Robert C. Martin tarafından adlandırılan Nesne Yönelimli Programlama’nın temel prensibidir S.O.L.I.D

S.O.L.I.D’deki her harf bir prensibe/kurala denk gelir:

Harf Açıklaması
S Single responsibility principle
O Open/closed principle
L Liskov substitution principle
I Interface segregation principle
D Dependency inversion principle

Bu prensipler, daha iyi program yazmanın yanı sıra, sürdürülebilir uygulama geliştirmeye yardımcı olur. Sandi Metz’in 2009’da düzenlenen GoRuCo konferansında yaptığı sunumun başında;

Uygulamanızı görmedim, ne yaptığınızı bilmiyorum ama kesin olarak bildiğim şey şu: uygulamanız değişecek!

Evet, Sandi Metz, bu konuda çok haklıydı. Çünkü gerçekten de yazdığımız uygulama, bir noktada mutlaka değişikliklere uğrayacaktı. İster hata düzeltme ister yeni özellikler ekleme olsun, uygulamalar bir şekilde hiçbir zaman ilk yazıldığı gün gibi kalmayacaktı.

Sağlam temelleri olan bir uygulama geliştirmek için ilk adımın Test Driven Development olduğunu biliyoruz, ama bir noktada ne yazıkki bu yöntem de yetersiz kalabiliyor.

Uygulama geliştirirken başımızın fazla ağrımaması için, uzmanlar SOLID prensipleri ortaya koydular. Şimdi tek tek bu prensiplere göz atalım.

[S]: Single responsibility

Bir sınıf (class) sadece tek bir işten sorumlu olmalıdır. Örneğin, bir web uygulaması içinde, veritabanı ile bağlantı yapacak olan sınıfın tek işi bağlantıyı açması ve kapatması olmalıdır. Sorgu yapmak, tablo silmek ya da oluşturmak işlerinden biri OLMAMALIDIR!

Kaynak, Kaynak


[O]: Open/closed

Bir sınıf, genişlemeye açık olmalı ama değişime kapalı olmalı. Yani kodu değiştirmeden, değişebilmeli. Modüller extend edilebilmeli ama asla ilgili işi yapabilmek adına, hali hazırda bulunan kod tekrardan yazılmamalı, değiştirilmemeli.

Örneğin, Daire ve Kare çizen bir uygulama olsun. Çizdirme işini yaparken, eğer tipi daire ise şöyle çiz, kare ise böyle çiz şeklinde bir akış kullanırsak, ileride üçgen çizdirmemiz gerektiğinde bu prensibi bozmak zorunda kalırız. Kodu değiştirip eğer tipi üçgen ise koşulunu eklememiz gerekir.

Halbuki, Şekil adında bir sınıf olsa, Daire ve Kare bu sınıftan türese, her iki şeklinde kendi çizim metodu olsa. En sonda da ŞekliÇiz diye ayrı bir sınıf olsa, bu sınıfı oluştururken ilgili şekli parametre olarak geçsek ve çizme işlemi için ilgili şeklin çizim metodunu çağırsak?

Aşağıdaki örnek bu prensibi ihlal eder. Yarın json çıktı almak yerine pdf ya da xml çıktı gerekse kodu değiştirmek gerekecek…

# Ruby kodu
class Rapor
  def dokuman
    dokumani_uret
  end

  def yazdir
    dokuman.to_json # json çıktı alır
  end
end

Yukarıdaki sınıf sadece json çıktı almak üzerine planlanmış. pdf çıktı almak gerektiği noktada bu kod’u değiştirmek gerekecek… Halbuki aşağıdaki kod parçası bu durumu düşünüp kurala uymuş;

# Ruby kodu
class Rapor
  def dokuman
    dokumani_uret
  end

  def yazdir(cikti_formati: JSONFormatter.new)
    cikti_formati.format dokuman
  end
end

aylik_rapor = Rapor.new
aylik_rapor.yazdir(cikti_formati: XMLFormatter.new) # xml olarak aldık...

Kaynak


[L]: Liskov substitution

Alt sınıf (türeyen sınıf - sub class), üst sınıfın (base class) yerini alabilecek şekilde olmalıdır. Şimdi iki tane sınıfımız olsun. Dörtgen ve Kare. Kare, Dörtgen’den türemiş olsun. Dörtgen sınıfının genislik ve yukseklik adında iki tane accessor’ü (getter-setter) var, Ruby’den örnekliyoruz:

# Ruby kodu
class Dortgen
  attr_accessor :genislik, :yukseklik
end 

ve Dortgen’den türemiş Kare:

# Ruby kodu
class Kare < Dortgen
  def yukseklik=(yukseklik)
    @degisken = yukseklik
  end
  def genislik=(genislik)
    @degisken = genislik
  end
  def genislik
    @degisken
  end
  def yukseklik
    @degisken
  end
end 

Ruby’de = ile biten metod setter anlamına geliyor. Yani yukseklik= setter, yukseklik ise getter.

Şimdi her iki şeklinde alanını hesap edelim:

alan = genislik * yukseklik
# Ruby kodu
dortgen = Dortgen.new
kare = Kare.new

dortgen.genislik = 4
dortgen.yukseklik = 5
dortgen.genislik * dortgen.yukseklik # => 20

kare.genislik = 4
kare.yukseklik = 5
kare.genislik * kare.yukseklik       # => 25 ???

Kare sınıfı, Liskov değişimi kuralını ihlal edip, üst sınıftan gelen özellikleri modifiye etmiştir.

Başka bir örnek;

# Ruby kodu
class Hayvan
  def yuru
     yurume_islemi
  end
end

class Kedi < Hayvan
  def kos
    kosma_islemi
  end
end

Verdiğim örnek Ruby’den ve Ruby’de interface mantığı yok. Yukarıdaki kod Liskov değişimi prensibini ihlal ediyor. Neden? Kedi sınıfı Hayvandan türedi ve Hayvan sınıfının kos diye bir metodu yok… Bu durumda, Hayvan sınıfını bir interface gibi düşünmeli ve interface’de olan metodları implement etmeliyiz. Bu bakımdan Hayvan sınıfı aşağıdaki gibi olmalı:

# Ruby kodu
class Hayvan
  def yuru
    yurume_islemi
  end
  def kos                          # koşma özelliği olmasa bile
    raise NotImplementedError      # prensibe göre çapraz
  end                              # eşitlik olmalı...
end

Kaynak, Kaynak


[I] Interface segregation

Genel amaca hizmet eden tek bir arayüz yapmak yerine, istemciye uygun farklı farklı arayüzler yapmak daha iyidir. Yani bir sınıf, bir interface’den türerken sadece kendi işine yarayacak metodları almalıdır.

Bu sayede daha uyumlu kod yazma ve less coupling yani başka kütphane/kod’a bağlı kalmama özelliğini arttırmış/sağlamış oluruz.

Kaynak, Kaynak


[D] Dependency inversion

Bağımlılığın tersine dönmesi. Tek parça monolit/devasa bir sınıf olmak yerine kendi işlerini yapan küçük parçalardan oluşan sınıflar haline gelmek. Bağımlılıkları minimale indirmek, hatta başka bir değişle Dependency Injection yapmak.

Özellikle TDD yaparken sık kullandığımız Mock, Stub, Test Double gibi kavramlar bu prensip üzerine kuruludur.

Bir sınıf oluşturuken parametre olarak Hash / Dictionary yani key-value tutan nesne geçmek ya da ilgili başka bir sınıfı geçmek araya bağımlılık enjekte etmek anlamına gelir. Buradaki bağımlılık aslında bize esneklik sağlar.

Fonksiyonu ya da metodu çağırken sabit parametre yerine kullanılan Hash, hem bize parametre sırası zorunluluğundan kurtatır hem de ilgili fonksiyon içinde bağımsız hareket edebilme özgürlüğünü sağlar.

# Ruby kodu
DEFAULTS = {
  a: 1,
  b: 2,
  c: "c"
}

def test_func(args={})
  args = DEFAULTS.merge(args)
end

test_func
# => {:a=>1, :b=>2, :c=>"c"}

test_func({a: "ali", b: "veli", c: nil})
# => {:a=>"ali", :b=>"veli", :c=>nil}

Test yazarken, henüz nasıl çalışacağına karar vermediğimiz ama tahminen sonucunu ne olması gerektiğini bildiğimiz durumlara, bu metodu varmış gibi taklit etmek, gerçek kodda kullanmak da tam bir Dependency Injection örneğidir.

# Ruby, Rspec örnek...
f = "./test_file.txt"

file_downloader = double("Fetcher")
# Henüz Fetcher sınıfını yazmadık ama 

file_downloader.stud(:download).and_return(f)
# download metodu olacağını ve bir text dosyası döneceğini biliyoruz!

Tüm bu prensipler aslında daha kullanışlı, daha rahat yönetilebilen ve sürdürülebilir yazılım geliştirmemizi sağlamak amacıyla uzmanların ortaya koyduğu kurallardır.

Başta da belirttiğim gibi, usta Sandi Metz bu konuyla ilgili çok güzel bir sunum yapmıştı. Bu sunumu da linkten indirebilirsiniz.