SOLID Prensipleri
Detaylı örnekler ile SOLID prensiplerini tekrar inceleyelim.
Tarihçesi
Ünlü bilgisayar bilimcisi Robert C. Martin’in (daha çok Uncle Bob lakabı ile tanınır) 2000 yılında kaleme almış olduğu “Design Principle and Design Patterns” isimli makalesinde bahsedilen prensipler; nesne yönelimli programlamanın temelini oluşturan 5 maddeden oluşur. 2004 yılında ise Micheal Feathers tarafından bu maddelerin baş harflerinden oluşan S-O- L-I-D kısaltılması ilk kez kullanılmıştır.
Bu prensipler, birden fazla yazılımcının ya da şirket içindeki farklı ekiplerin aynı proje üzerinde rahatça çalışabilmesi, rahat okunabilir ve kolay test edilebilir kod yazımını sağlamayı amaçlar. Maddeler ve açıklamaları şöyledir;
Tek Sorumluluk Prensibi
Single Responsibility Principle
Bu prensip her sınıf ya da fonksiyonun değişime uğramak için tek bir sebep olmasını başka bir değişle her bir parçanın tek bir görev üstlenmesini tanımlar.
Örneğin kullanıcının girdiği e-mail ve parola bilgisini kontrol eden ve yanlış olması durumunda kullanıcıyı bilgilendiren bir akışınız varsa bu 2 farklı görevi aynı sınıf ya da fonksiyon içinde yapmamanızı tavsiye eder.
public class KullaniciGirisi{
public static void main(String []args){
SifreKontrolVeBilgilendirme("b@b.com", "321");
}
public static void SifreKontrolVeBilgilendirme(String email, String parola) {
if (email == "a@a.com" && parola == "123"){
System.out.println("Parola doğru.");
} else {
System.out.println("Parola yanlış.");
// E-mail gönderme işlemleri.
System.out.println("E-mail gönderildi.");
}
}
}
Bu kod örneğinde “Parola yanlış” bilgisini veren kısım bir görev, kullanıcıya e-mail gönderen kısım ise başka bir görevdir. Bu iki görevden birinde ya da her ikisinde birden değişiklik yapmak istediğimizde SifreKontrolVeBilgilendirme() isimli fonksiyonu değiştirmemiz gerekir. Tek Sorumluluk prensibinin tanımını yaparken söylediğimiz gibi bir sınıf ya da fonksiyonu değiştirmek için en fazla bir sebebimiz olmalıdır. Dolayısıyla bu iki ayrı görevi tek yerde kullanmak yerine her biri için ayrı fonksiyon ya da sınıf oluşturmamız gerekir.
public class KullaniciGirisi {
public static void main(String []args) {
SifreKontrol.Kontrol("a@b.com", "1234");
}
}
class SifreKontrol {
public static void Kontrol(String email, String parola) {
if (email == "a@a.com" && parola == "123") {
LogMesaji.Goster("Bilgiler Doğru.");
} else {
LogMesaji.Goster("Bilgiler Yanlış!");
Bilgilendirme.EmailGonder();
}
}
}
class LogMesaji {
public static void Goster(String mesaj) {
System.out.println(mesaj);
}
}
public class Bilgilendirme {
public static void EmailGonder() {
// E-mail gönderme işlemleri.
LogMesaji.Goster("E-posta gönderildi.");
}
public static void SMSGonder() {
// SMS gönderme işlemleri.
LogMesaji.Goster("SMS gönderildi.");
}
}
Kullanıcının girdiği parolanın doğruluğunu kontrol eden kod parçası çalışıp görevini tamamladıktan sonra akışa devam etmek üzere bir sonraki görevi üstlenen parçaya son durumu bildirmeli bu şekilde zincirleme mantığıyla çalışan, birbirinden bağımsız ve farklı görevleri olan parçalar oluşturmalıyız.
Yukarıdaki örnekte aynı zamanda ekrana log mesajı basmakla görevli olan fonksiyonun da kendine ait bir sınıf içinde yer alacak şekilde ayrılmış olduğuna dikkat ediniz.
Açık-Kapalı Prensibi
Open-Closed Principle
Bir yazılım parçasının (modül, sınıf, fonksiyon vb.) var olan halinin değişime kapalı fakat genişletilmeye açık olması gerektiğini savunan prensiptir. Önceden yazılmış ve çalışır halde olan kodların değişime uğramaması gerektiğini eğer değişim gereksinimi olursa yeni kodlar eklenerek var olan parçanın davranışlarının genişletilmesi gerektiğini tanımlar.
Örnek olarak hazırlanmış şu kodları inceleyelim:
public class Ogrenci {
String isim;
String soyisim;
String tcKimlikNo;
public Ogrenci(String isim, String soyisim, String tcKimlikNo){
this.isim = isim;
this.soyisim = soyisim;
this.tcKimlikNo = tcKimlikNo;
}
}
class OgrenciIslemleri {
public static void Kayit(Ogrenci ogrenci){
System.out.println(ogrenci.isim + " isimli öğrenci kayıt edildi.");
}
}
class Main {
public static void main(String[] args) {
Ogrenci ogrenci1 = new Ogrenci("Ahmet", "Soytürk", "164…");
OgrenciIslemleri.Kayit(ogrenci1);
}
}
Bu kodlar isim, soy isim ve T.C. kimlik numarası ile sisteme öğrenci kayıt etmek için hazırlanmış ve çalışır durumdadır. Peki ya sisteme evlenmiş ve ikinci soy isme sahip bir öğrenci kayıt etmek istersek? Bu durumda Ogrenci isimli sınıfın içeriğini değiştirmemiz gerekirdi fakat eğer Açık-Kapalı prensibini uygularsak bu gibi değişim gereksinimleri halihazırda çalışan kodları değişime uğratmadan yapabiliriz.
Bunun için öncelikle Ogrenci sınıfını yeniden planlamak gerekir, bu sınıf akışın en üstünde olacağı için sisteme kayıt edilecek tüm öğrencilerde mutlaka olması gereken belirli özellikleri içermelidir. Örneğin Türk vatandaşı olmayan bir öğrencinin T.C. Kimlik numarası da olmaz bu sebeple Ogrenci sınıfımız sadece isim ve soy isimden oluşmalı, T.C. kimlik ve ikinci soy isim gibi ek özellikler için Ogrenci sınıfından türeyen farklı sınıflarımız olmalıdır.
public class Ogrenci {
String isim;
String soyisim;
public Ogrenci(String isim, String soyisim){
this.isim = isim;
this.soyisim = soyisim;
}
}
class TurkOgrenci extends Ogrenci {
String tcKimlikNo;
public TurkOgrenci(String isim, String soyisim, String tcKimlikNo) {
super(isim, soyisim);
this.tcKimlikNo = tcKimlikNo;
}
}
class EvliOgrenci extends TurkOgrenci {
String ikinciSoyIsim;
public EvliOgrenci(String isim, String soyisim, String tcKimlikNo, String ikinciSoyIsim) {
super(isim, soyisim, tcKimlikNo);
this.ikinciSoyIsim = ikinciSoyIsim;
}
}
Artık öğrenci kayıt işlemlerinde yabancı ve ikinci soy isme sahip öğrencilerin kayıt işlemlerini kolayca yapabiliriz.
class Main {
public static void main(String[] args) {
Ogrenci ogrenci1 = new Ogrenci("Clara", "SEAMAN");
OgrenciIslemleri.Kayit(ogrenci1);
Ogrenci ogrenci2 = new TurkOgrenci("Selim", "SARI", "974…");
OgrenciIslemleri.Kayit(ogrenci2);
Ogrenci ogrenci3 = new EvliOgrenci("Damla", "KAYALI", "YÜKSEKTEPE", "154…");
OgrenciIslemleri.Kayit(ogrenci3);
}
}
Liskov’un Yer Değiştirme Prensibi
Liskov Substitution Princible
Barbara Liskov tarafından 1987 yılında matematiksel bir durumu ifade etmek için ortaya atılan şu cümleden yola çıkılarak üretilmiş bir prensiptir;
φ(x), T türündeki x nesneleri hakkında ispatlanabilir bir özellik olsun. O halde φ(y), T’nin bir alt türü olduğu S türündeki y nesneleri için de doğru olmalıdır.
Bir üst sınıftan türetilmiş tüm alt sınıflar, üst sınıfın bütün özelliklerini kullanabilmeli ve kendine ait özellikler barındırabilmelidir tanımına sahip olan bu prensip daha önce görmüş olduğumuz kalıtım konusuyla ilgilidir ve iyi bir kalıtım hiyerarşisi kurmamıza yardımcı olur. Açık-Kapalı prensibine uymak Liskov’un Yer Değiştirme prensibini uygulamayı kolaylaştırmaktadır.
Bir örnekle açıklamak gerekirse, bir kare aslında her kenarı aynı uzunluğa sahip bir dörtgendir yani bir Kare sınıfını Dortgen sınıfından kalıtabiliriz.
class Dortgen {
int yukseklik, genislik;
public Dortgen(int yukseklik, int genislik) {
this.yukseklik = yukseklik;
this.genislik = genislik;
}
public int AlanHesapla() {
return yukseklik * genislik;
}
}
class Main {
public static void main(String[] args) {
Dortgen dortgen = new Dortgen(5,8);
System.out.println(dortgen.AlanHesapla());
Dortgen kare = new Dortgen(5,5);
System.out.println(kare.AlanHesapla());
}
}
Dortgen sınıfından kalıtarak oluşturduğumuz Kare sınıfı için alan hesaplaması yaparken yükseklik ve genişlik verilerini ayrı ayrı vermemiz gerekir fakat bir karenin alanını hesaplarken tek bir kenar uzunluğunu bilmemiz bize yeterli olacaktır.
Bu sebeple izlenecek en iyi yol Açık-Kapalı prensibini de uygulayarak hem Kare hem de Dortgen sınıfının üstünde yer alacak Sekil isimli bir arayüz daha oluşturmaktır. Bu sayede alan ve çevre hesaplamalarını kendine özgü şekilde Kare veya Dortgen sınıfları içinde ayrı ayrı yapabiliriz.
interface Sekil {
public int AlanHesapla();
}
class Dortgen implements Sekil {
int yukseklik, genislik;
public Dortgen(int yukseklik, int genislik) {
this.yukseklik = yukseklik;
this.genislik = genislik;
}
@Override
public int AlanHesapla() {
return yukseklik * genislik;
}
}
class Kare implements Sekil{
int kenarUzunlugu;
public Kare(int kenarUzunlugu) {
this.kenarUzunlugu = kenarUzunlugu;
}
@Override
public int AlanHesapla() {
return kenarUzunlugu * kenarUzunlugu;
}
}
class Main {
public static void main(String[] args) {
Dortgen dortgen = new Dortgen(5,8);
System.out.println(dortgen.AlanHesapla());
Kare kare = new Kare(9);
System.out.println(kare.AlanHesapla());
}
}
Arayüz Ayrımı Prensibi
Interface Segregation Princible
Tek Sorumluluk prensibine çok benzeyen bu prensip bizlere bir nesneye kendisiyle alakalı olmayan hiçbir arayüzün tanımlanmaması yani arayüzlerin tek bir yerde toplanması yerine olabilecek en küçük parçalara bölünmesi gerekliliğini anlatır.
Bilindiği üzere arayüzler bir sınıfın yapabileceği işlemleri tanımladığımız yapılardır. Eğer bir YetiskinInsan sınıfı oluşturmak istersek, içerisinde “koşmak”, “yürümek”, “konuşmak” ve “yemek yapmak” gibi fonksiyonları tanımladığımız arayüzler uygulayabilir ve işlemlerin hepsini yapabilmesini aynı anda sağlayabiliriz.
interface Insan {
public void Yurumek();
public void Kosmak();
public void Konusmak();
public void YemekYapmak();
}
class YetiskinInsan implements Insan {
public static void main(String[] args) {}
@Override
public void Kosmak() {}
@Override
public void Yurumek() {}
@Override
public void Konusmak() {}
@Override
public void YemekYapmak() {}
}
Fakat Insan arayüzünü uygulayacağımız sınıf bir YurumeEngelliInsan sınıfı olması gerekirse içerisinde otomatik olarak eklenmiş olan yürümek ve koşmak özellikleri gereksiz yere eklenmiş olacaktır. Bu sebeple arayüz ayrımı prensibi şişkin bir arayüz yapısı yerine arayüzleri olabilecek en uygun şekilde ayrıştırılması amacını güder.
interface YemekYapabilenInsan {
public void YemekYapmak();
}
interface KonusabilenInsan {
public void Konusmak();
}
interface KosabilenInsan {
public void Yurumek();
public void Kosmak();
}
class YurumeEngelliInsan implements KonusabilenInsan, YemekYapabilenInsan {
public static void main(String[] args) {}
@Override
public void Konusmak() {}
@Override
public void YemekYapmak() {}
}
YürümeEngelliInsan sınıfında kullanılamayacak olan fonksiyonları Arayüz Ayrımı Prensibini uygulayarak kaldırmış ve kod kalabalığını önlemiş olduk.
Bağımlılığı Tersine Çevirme Prensibi
Dependency Inversion Princible
Bu prensip bir programın doğru şekilde dizayn edilmesi ile ilgilenir. Üst seviye sınıfların, alt seviye sınıflara bağımlı olmaması gerektiğini savunur. Sınıflar arası bağımlılıkların en aza indirgenmesi için araya soyutlama yapılarını barındıran bir katman eklenmesi gerektiğini vurgular.
Bunu bir örnek ile anlamaya çalışalım; Bir şirket yönetim uygulaması yazdığımızı varsayalım, gönderileri yollamak için Otomobil ve Kamyon isminde 2 ayrı sınıfımız olsun. Bu sınıflar içerisinde YolaCik() isimli bir fonksiyon barındırsınlar.
public class Otomobil {
public void YolaCik() {
System.out.println("Otomobil yola çıktı");
}
}
public class Kamyon {
public void YolaCik() {
System.out.println("Kamyon yola çıktı");
}
}
Şimdi gönderileri yönetecek olan GonderiYoneticisi sınıfı içerisinde bu iki sınıfı çağıralım. GonderiYoneticisi sınıfı Otomobil ve Kamyon sınıflarını barındırdığı için üst sınıf durumundadır.
public class GonderiYoneticisi {
public void GonderiOlustur() {
Otomobil otomobil = new Otomobil();
Kamyon kamyon = new Kamyon();
otomobil.YolaCik();
kamyon.YolaCik();
}
}
Bu üst sınıf içerisinde yer alan GonderiOlustur() isimli fonksiyon, gönderiyi yola çıkartabilmek için Otomobil ya da Kamyon sınıflarından birine ihtiyaç duyar. Fakat hangisine? Bu kararı pekâlâ bazı kontrol komutları sonrasında yapabilir ve ilgili alt sınıfı çağırarak işleme devam edebiliriz ama bu doğru yöntem midir?
Üst sınıf olan GonderiYoneticisi, alt sınıf Kamyon’a olan bağımlılığı bu noktada çok belirgin şekilde hissedilmeyebilir fakat Kamyon ya da Otomobil içerisindeki değişiklikler direkt üst sınıfı etkileyecektir. Örneğin Kamyon içerisindeki YolaCik() isimli fonksiyonun isminin değişmesi ya da yeni bir fonksiyon eklenmesi gibi.
Bağımlılığı Tersine Çevirme prensibi tam olarak bu noktada devreye girmekte ve bizlere alt sınıf ile üst sınıf arasındaki bu bağımlılığı ortadan kaldırmak için araya bir soyutlama katmanı eklememiz gerektiğini söyler.
Şimdi Arac isimli bir arayüz oluşturalım ve içerisine YolaCik() isimli bir fonksiyon tanımlayalım.
interface Arac {
public void YolaCik();
}
Kamyon, Otomobil, Otobus, Ucak gibi sınıflarımıza bu arayüzü uygulayalım ve YolaCik() isimli fonksiyona otomatik olarak sahip olmalarını sağlayalım.
class Otomobil implements Arac {
@Override
public void YolaCik() {
System.out.println("Otomobil yola çıktı.");
}
}
Artık GonderiYoneticisi isimli sınıfımız içerisinde Arac tipinde tek bir parametre kabul eden ve içerisinde arac.YolaCik() fonksiyonunu tetikleyen KargoYolla() isimli bir fonksiyon oluşturabiliriz.
public class GonderiYoneticisi {
public static void main(String []args) {
Otomobil otomobil = new Otomobil();
KargoYolla(otomobil);
}
public static void KargoYolla(Arac arac) {
arac.YolaCik();
}
}
Bunu yapmak bize kargonun hangi araç ile gideceğini jenerik olarak seçme özgürlüğü tanımış aynı zamanda alt ve üst sınıfların arasındaki bağımlılığı koparıp ikisini de soyut bir arayüze bağımlı hale getirmiş olacaktır.