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!
[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...
[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ı Hayvan
dan
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
[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.
[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.