SOLID PRENSIBI

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

Bu beş kural, aşağıdaki konulardan oluşur;

  • Single responsibility principle
  • Open/closed principle
  • Liskov substitution principle
  • Interface segregation principle
  • 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...

class Rapor
 def dokuman
   dokumani_uret
 end

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

Halbuki aşağıdaki durum bu kurala uyar;

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:

class Dortgen
 attr_accessor :genislik, :yukseklik
end

ve Dortgen’den türemiş Kare:

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
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;

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ı:

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ütüphane/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.

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.

Kategori : Programlama Dilleri