SOLID: Daha İyi Yazılım Tasarımının Beş Temel Taşı
Nesne Yönelimli Programlama (OOP), modern yazılım geliştirmenin temelini oluşturan güçlü bir paradigmadır. Ancak OOP'nin sunduğu sınıf, nesne, kalıtım gibi araçları etkili bir şekilde kullanmak, zamanla karmaşıklaşan, bakımı zorlaşan ve hatalara açık hale gelen "spagetti kod" yığınları oluşturmaktan kaçınmak için belirli tasarım prensiplerine uymayı gerektirir. İşte bu noktada, Robert C. Martin (genellikle "Uncle Bob" olarak bilinir) tarafından bir araya getirilen ve popülerleştirilen SOLID prensipleri devreye girer. SOLID, daha anlaşılır, esnek, sürdürülebilir ve test edilebilir nesne yönelimli tasarımlar oluşturmak için beş temel prensibin baş harflerinden oluşan bir akronimdir.
SOLID prensipleri, yazılımın zaman içindeki değişimlere karşı daha dayanıklı olmasını hedefler. Bir sistemin gereksinimleri değiştikçe veya yeni özellikler eklenmesi gerektiğinde, iyi tasarlanmış bir sistemde bu değişikliklerin minimum eforla, mevcut kodu kırmadan ve beklenmedik yan etkiler yaratmadan yapılabilmesi gerekir. SOLID, bu hedefe ulaşmak için somut rehberlik sunar. Prensipler şunlardır:
- Single Responsibility Principle (Tek Sorumluluk Prensibi)
- Open/Closed Principle (Açık/Kapalı Prensibi)
- Liskov Substitution Principle (Liskov Yerine Geçme Prensibi)
- Interface Segregation Principle (Arayüz Ayırma Prensibi)
- Dependency Inversion Principle (Bağımlılıkların Tersine Çevrilmesi Prensibi)
Bu beş prensip birbirini tamamlar ve birlikte uygulandığında, yazılımın kalitesini önemli ölçüde artırır. Daha gevşek bağlı (loosely coupled) ve yüksek uyumlu (highly cohesive) modüller oluşturmayı teşvik ederler. Gevşek bağlılık, sistemin bir parçasındaki değişikliğin diğer parçaları minimum düzeyde etkilemesi anlamına gelirken, yüksek uyum, bir modülün veya sınıfın iyi tanımlanmış tek bir amaca odaklanması demektir. Bu rehber, SOLID prensiplerinin her birini detaylı bir şekilde inceleyecek, ne anlama geldiklerini, neden önemli olduklarını ve pratik kod örnekleriyle (C# ve Python odaklı) nasıl uygulanabileceklerini açıklayacaktır. SOLID'i anlamak ve uygulamak, sadece daha iyi kod yazmanızı sağlamakla kalmaz, aynı zamanda yazılım tasarımı ve mimarisi konusundaki anlayışınızı da derinleştirir.
S: Tek Sorumluluk Prensibi (Single Responsibility Principle - SRP)
SOLID prensiplerinin ilki ve belki de en temel olanı Tek Sorumluluk Prensibi'dir (SRP). Bu prensip oldukça basit bir fikre dayanır: Bir sınıfın değişmek için sadece tek bir nedeni olmalıdır. Başka bir deyişle, bir sınıfın yalnızca tek bir sorumluluğu, tek bir işlevi veya tek bir amacı olmalıdır.
SRP'nin Amacı Nedir?
SRP'nin temel amacı şunlardır:
- Yüksek Uyum (High Cohesion): Sınıfın üyeleri (metotlar, özellikler) mantıksal olarak birbiriyle yakından ilişkili olur ve tek bir konsepte odaklanır.
- Düşük Bağlılık (Low Coupling - Dolaylı Etki): Bir sınıfın birden fazla sorumluluğu olduğunda, bu sorumluluklardan birindeki değişiklik diğer sorumlulukları etkileyebilir veya o sorumluluklara bağımlı olan diğer sınıfları gereksiz yere etkileyebilir. SRP, sorumlulukları ayırarak bu bağlılığı azaltır.
- Anlaşılabilirlik ve Bakım Kolaylığı: Tek bir işe odaklanan sınıfları anlamak, test etmek ve bakımını yapmak daha kolaydır.
- Değişiklik Etkisini Sınırlama: Bir sorumlulukla ilgili bir değişiklik gerektiğinde, sadece o sorumluluğu taşıyan sınıfın değiştirilmesi yeterli olur. Diğer sınıfların etkilenme riski azalır.
- Yeniden Kullanılabilirlik: Tek bir sorumluluğu olan sınıfların farklı bağlamlarda yeniden kullanılma olasılığı daha yüksektir.
Bir sınıfın birden fazla sorumluluğu varsa, bu sorumluluklar zamanla farklı hızlarda veya farklı nedenlerle değişebilir. Bu durum, sınıfın sık sık ve beklenmedik şekillerde değiştirilmesine, testlerinin karmaşıklaşmasına ve hatalara daha açık hale gelmesine neden olabilir.
SRP İhlali Örneği
Klasik bir SRP ihlali örneği, hem kullanıcı verilerini yöneten, hem bu verilerle rapor oluşturan hem de bu raporu e-posta ile gönderen tek bir KullaniciYonetimi
sınıfıdır.
// SRP İHLALİ ÖRNEĞİ (C#)
public class KullaniciYonetimi_SRP_Ihlali
{
private List kullanicilar = new List();
// Sorumluluk 1: Kullanıcı Veri Yönetimi
public void KullaniciEkle(string kullaniciAdi)
{
if (!kullanicilar.Contains(kullaniciAdi))
{
kullanicilar.Add(kullaniciAdi);
Console.WriteLine($"{kullaniciAdi} eklendi.");
}
}
public void KullaniciSil(string kullaniciAdi)
{
if (kullanicilar.Contains(kullaniciAdi))
{
kullanicilar.Remove(kullaniciAdi);
Console.WriteLine($"{kullaniciAdi} silindi.");
}
}
// Sorumluluk 2: Raporlama
public void RaporOlustur()
{
Console.WriteLine("\n--- Kullanıcı Raporu ---");
foreach (var k in kullanicilar)
{
Console.WriteLine($"- {k}");
}
Console.WriteLine("--- Rapor Sonu ---");
}
// Sorumluluk 3: E-posta Gönderme
public void RaporuEpostaGonder(string aliciEposta)
{
// E-posta gönderme mantığı (detaylar basitleştirildi)
string raporIcerigi = string.Join("\n", kullanicilar);
Console.WriteLine($"'{aliciEposta}' adresine rapor gönderiliyor:\n{raporIcerigi}");
// ... Gerçek e-posta gönderme kodu ...
Console.WriteLine("E-posta gönderildi.");
}
}
// Kullanım:
// KullaniciYonetimi_SRP_Ihlali yonetim = new KullaniciYonetimi_SRP_Ihlali();
// yonetim.KullaniciEkle("Ahmet");
// yonetim.KullaniciEkle("Ayşe");
// yonetim.RaporOlustur();
// yonetim.RaporuEpostaGonder("admin@example.com");
Bu sınıfta sorun nedir? Raporlama formatı değişirse veya e-posta gönderme mekanizması (örn: SMTP yerine API kullanma) değişirse, KullaniciYonetimi_SRP_Ihlali
sınıfının değiştirilmesi gerekir. Kullanıcı ekleme/silme mantığıyla doğrudan ilgisi olmayan bu değişiklikler, bu sınıfı gereksiz yere karmaşıklaştırır ve değiştirme riski taşır.
SRP Uygulanmış Çözüm
Çözüm, her bir sorumluluğu kendi sınıfına ayırmaktır:
// Sorumluluk 1: Kullanıcı Veri Yönetimi
public class KullaniciDeposu // (Repository Pattern örneği olabilir)
{
private List _kullanicilar = new List();
public void Ekle(string kullaniciAdi)
{
if (!_kullanicilar.Contains(kullaniciAdi))
{
_kullanicilar.Add(kullaniciAdi);
Console.WriteLine($"{kullaniciAdi} depoya eklendi.");
}
}
public void Sil(string kullaniciAdi)
{
if (_kullanicilar.Contains(kullaniciAdi))
{
_kullanicilar.Remove(kullaniciAdi);
Console.WriteLine($"{kullaniciAdi} depodan silindi.");
}
}
public IEnumerable TumKullanicilariGetir()
{
// Savunmacı kopya döndürmek daha iyi olabilir
return new List(_kullanicilar);
}
}
// Sorumluluk 2: Raporlama
public class KullaniciRaporlayici
{
public void RaporuYazdir(IEnumerable kullanicilar)
{
Console.WriteLine("\n--- Kullanıcı Raporu ---");
foreach (var k in kullanicilar)
{
Console.WriteLine($"- {k}");
}
Console.WriteLine("--- Rapor Sonu ---");
}
public string RaporIcerigiOlustur(IEnumerable kullanicilar)
{
// Rapor formatlama mantığı burada olurdu
return string.Join("\n", kullanicilar);
}
}
// Sorumluluk 3: E-posta Gönderme
public class EpostaServisi
{
public void EpostaGonder(string alici, string konu, string icerik)
{
Console.WriteLine($"'{alici}' adresine e-posta gönderiliyor...");
Console.WriteLine($"Konu: {konu}");
Console.WriteLine($"İçerik:\n{icerik}");
// ... Gerçek e-posta gönderme kodu ...
Console.WriteLine("E-posta gönderildi.");
}
}
// Kullanım (Orkestrasyon - belki başka bir sınıfta veya ana programda)
public class UygulamaMantigi_SRP
{
private readonly KullaniciDeposu _depo = new KullaniciDeposu();
private readonly KullaniciRaporlayici _raporlayici = new KullaniciRaporlayici();
private readonly EpostaServisi _epostaServisi = new EpostaServisi();
public void KullaniciEkleVeRaporla(string kullaniciAdi, string raporAlicisi)
{
_depo.Ekle(kullaniciAdi);
var kullanicilar = _depo.TumKullanicilariGetir();
_raporlayici.RaporuYazdir(kullanicilar);
string raporIcerik = _raporlayici.RaporIcerigiOlustur(kullanicilar);
_epostaServisi.EpostaGonder(raporAlicisi, "Kullanıcı Raporu", raporIcerik);
}
}
// UygulamaMantigi_SRP app = new UygulamaMantigi_SRP();
// app.KullaniciEkleVeRaporla("Zeynep", "sysadmin@example.com");
Bu yapıda, her sınıfın tek bir sorumluluğu vardır. E-posta gönderme değişirse sadece EpostaServisi
etkilenir. Rapor formatı değişirse KullaniciRaporlayici
değişir. Kullanıcı veritabanı değişirse (örneğin list yerine SQL) KullaniciDeposu
değişir. Sınıflar daha küçük, odaklı, test edilebilir ve yönetilebilirdir.
SRP'yi uygularken "sorumluluğun" ne kadar granüler olacağına karar vermek önemlidir. Aşırıya kaçmak çok fazla sayıda küçük sınıfa yol açabilir. Önemli olan, bir sınıfın değişmek için mantıksal olarak tek bir ana nedeni olmasıdır.
O: Açık/Kapalı Prensibi (Open/Closed Principle - OCP)
Bertrand Meyer tarafından ortaya atılan Açık/Kapalı Prensibi, SOLID'in ikinci harfini oluşturur ve şunu ifade eder: Yazılım varlıkları (sınıflar, modüller, fonksiyonlar vb.) genişletilmeye açık, ancak değiştirilmeye kapalı olmalıdır. Bu, bir sistemin davranışını değiştirmek veya yeni özellikler eklemek istediğimizde, mevcut, çalışan ve test edilmiş kodu doğrudan değiştirmek yerine, sisteme yeni kod ekleyerek (genişleterek) bunu yapabilmemiz gerektiği anlamına gelir.
OCP'nin Amacı Nedir?
OCP'nin temel hedefleri şunlardır:
- Değişiklik Riskini Azaltma: Mevcut kodu değiştirmek, her zaman yeni hatalar (regresyonlar) ekleme riski taşır. OCP, bu riski en aza indirir.
- Esneklik ve Genişletilebilirlik: Sistemin yeni gereksinimlere veya özelliklere kolayca adapte olabilmesini sağlar.
- Bakım Kolaylığı: Yeni özellikler eklemek için mevcut kodun karmaşık yapısını anlamak ve değiştirmek yerine, yeni, izole bileşenler eklemek genellikle daha kolaydır.
- Kararlılık: Sistemin temel bileşenleri daha kararlı kalır, çünkü sürekli değiştirilmezler.
OCP'yi ihlal etmek, genellikle yeni bir özellik eklendiğinde mevcut sınıflara sürekli yeni if/else
veya switch
blokları eklenmesi şeklinde kendini gösterir. Bu, sınıfın zamanla büyümesine, karmaşıklaşmasına ve değiştirilmesinin zorlaşmasına neden olur.
OCP Nasıl Uygulanır? Soyutlama Anahtardır!
OCP'yi sağlamanın anahtarı soyutlamadır. Değişkenlik gösterebilecek davranışları veya algoritmaları soyut sınıflar veya (daha yaygın olarak) arayüzler arkasına gizleyerek, sistemin bu soyutlamalara bağımlı olmasını sağlarız. Yeni bir davranış eklemek gerektiğinde, mevcut soyutlamayı implemente eden yeni bir somut sınıf oluştururuz.
Yaygın uygulama yöntemleri:
- Arayüzler (Interfaces) ve Kalıtım: Ortak bir davranışı temsil eden bir arayüz tanımlanır. Farklı davranışları implemente eden sınıflar bu arayüzü uygular. Sistemi kullanan kod, arayüze bağımlı olur. Yeni bir davranış gerektiğinde, arayüzü uygulayan yeni bir sınıf eklenir.
- Soyut Sınıflar (Abstract Classes) ve Kalıtım: Arayüzlere benzer şekilde, ortak bir temel ve bazı varsayılan davranışlar sağlamak için kullanılır.
- Tasarım Desenleri (Design Patterns): Strateji (Strategy), Şablon Metot (Template Method), Dekorator (Decorator) gibi birçok tasarım deseni OCP'yi uygulamaya yardımcı olur. Özellikle Strateji deseni, farklı algoritmaları veya davranışları değiştirilebilir hale getirmek için sıkça kullanılır.
OCP İhlali Örneği
Farklı şekillerin alanlarını hesaplayan bir sistemi düşünelim. OCP ihlali şöyle görünebilir:
// OCP İHLALİ ÖRNEĞİ (C#)
public enum SekilTipi { Daire, Dikdortgen /*, Ucgen - Yeni eklemek için burayı değiştirmeliyiz */ }
public class Sekil_OCP_Ihlali
{
public SekilTipi Tipi { get; set; }
public double YariCap { get; set; } // Sadece daire için
public double Genislik { get; set; } // Sadece dikdörtgen için
public double Yukseklik { get; set; } // Sadece dikdörtgen için
// public double Kenar1 { get; set; } // Üçgen için yeni özellik eklenmeli
}
public class AlanHesaplayici_OCP_Ihlali
{
public double ToplamAlanHesapla(List sekiller)
{
double toplamAlan = 0;
foreach (var sekil in sekiller)
{
if (sekil.Tipi == SekilTipi.Daire)
{
toplamAlan += Math.PI * sekil.YariCap * sekil.YariCap;
}
else if (sekil.Tipi == SekilTipi.Dikdortgen)
{
toplamAlan += sekil.Genislik * sekil.Yukseklik;
}
// YENİ ŞEKİL (örn: Üçgen) EKLENDİĞİNDE BURAYA YENİ BİR 'else if' EKLENMELİ!
// else if (sekil.Tipi == SekilTipi.Ucgen) { ... }
}
return toplamAlan;
}
}
Bu tasarımda, yeni bir şekil (örneğin Üçgen) eklemek istediğimizde hem SekilTipi
enum'ını hem de AlanHesaplayici_OCP_Ihlali
sınıfındaki ToplamAlanHesapla
metodunu değiştirmemiz gerekir. Bu, OCP'nin ihlalidir.
OCP Uygulanmış Çözüm
Çözüm, şekiller için soyut bir temel (arayüz veya soyut sınıf) oluşturmak ve alan hesaplama sorumluluğunu her şeklin kendisine vermektir.
// OCP UYGULANMIŞ ÇÖZÜM (C#)
// 1. Soyutlama (Arayüz veya Soyut Sınıf)
public interface ISekil
{
double AlanHesapla(); // Her şekil kendi alanını hesaplamalı
}
// 2. Somut Sınıflar (Genişletme)
public class Daire : ISekil
{
public double YariCap { get; set; }
public double AlanHesapla()
{
return Math.PI * YariCap * YariCap;
}
}
public class Dikdortgen : ISekil
{
public double Genislik { get; set; }
public double Yukseklik { get; set; }
public double AlanHesapla()
{
return Genislik * Yukseklik;
}
}
// YENİ ŞEKİL EKLEME (Sadece Yeni Kod)
public class Ucgen : ISekil
{
public double Taban { get; set; }
public double Yukseklik { get; set; }
public double AlanHesapla()
{
return (Taban * Yukseklik) / 2;
}
}
// 3. Kullanıcı Kod (Değiştirilmeye Kapalı)
public class AlanHesaplayici_OCP_Uyumlu
{
// AlanHesaplayici artık belirli şekil tiplerini bilmek zorunda değil!
// Sadece ISekil arayüzünü bilen nesnelerle çalışır.
public double ToplamAlanHesapla(List sekiller)
{
double toplamAlan = 0;
foreach (var sekil in sekiller)
{
// Her şekil kendi AlanHesapla() metodunu çağırır (Polimorfizm)
toplamAlan += sekil.AlanHesapla();
}
return toplamAlan;
}
}
// Kullanım:
// List sekilListesi = new List
// {
// new Daire { YariCap = 5 },
// new Dikdortgen { Genislik = 4, Yukseklik = 6 },
// new Ucgen { Taban = 3, Yukseklik = 4 } // Yeni şekil kolayca eklendi
// };
// AlanHesaplayici_OCP_Uyumlu hesaplayici = new AlanHesaplayici_OCP_Uyumlu();
// double toplam = hesaplayici.ToplamAlanHesapla(sekilListesi);
// Console.WriteLine($"Toplam Alan: {toplam}");
Bu yeni tasarımda, AlanHesaplayici_OCP_Uyumlu
sınıfı ISekil
arayüzüne bağımlıdır ve belirli şekil türlerini bilmez. Yeni bir şekil (Ucgen
) eklemek istediğimizde, sadece yeni Ucgen
sınıfını oluşturmamız yeterli oldu. AlanHesaplayici_OCP_Uyumlu
sınıfını değiştirmemize gerek kalmadı. Sistem yeni davranışlara (yeni şekiller) açık, ancak mevcut AlanHesaplayici
kodu değişikliğe kapalı hale geldi.
L: Liskov Yerine Geçme Prensibi (Liskov Substitution Principle - LSP)
Barbara Liskov tarafından formüle edilen Liskov Yerine Geçme Prensibi, kalıtımın doğru ve tutarlı bir şekilde kullanılmasını sağlayan kritik bir OOP prensibidir. Temel fikri şudur: Eğer S, T'nin bir alt tipi ise, o zaman T tipindeki nesnelerin yerine S tipindeki nesneler, programın istenen özelliklerini (doğruluğunu, beklenen davranışını) değiştirmeden kullanılabilmelidir. Yani, alt sınıf nesneleri, üst sınıf nesnelerinin kullanılabildiği her yerde, herhangi bir sürpriz veya hataya neden olmadan kullanılabilmelidir.
LSP'nin Amacı Nedir?
LSP'nin temel amacı:
- Davranışsal Alt Tiplemeyi Sağlamak: Alt sınıfların, üst sınıfın belirlediği sözleşmeyi (beklenen davranışları, metotların önkoşullarını/sonkoşullarını, değişmezleri) korumasını sağlamak.
- Kalıtımın Kötüye Kullanımını Önlemek: Sırf kod tekrarını azaltmak için mantıksal olarak "is-a" ilişkisi olmayan sınıflar arasında kalıtım kullanılmasını engellemek.
- Polimorfizmin Güvenilirliğini Artırmak: Üst sınıf referansı üzerinden alt sınıf nesneleriyle çalışırken beklenmedik hataların veya davranışların ortaya çıkmasını önlemek.
- Sistem Tutarlılığını Korumak: Alt sınıfların, üst sınıfın yerine geçtiğinde sistemin genel davranışını bozmamasını garanti etmek.
LSP ihlalleri genellikle alt sınıfın, miras aldığı bir metodun davranışını beklenmedik bir şekilde değiştirmesi, daha katı önkoşullar getirmesi veya üst sınıfın sağlamadığı bir istisna fırlatması gibi durumlarda ortaya çıkar.
LSP İhlali Örneği: Kare ve Dikdörtgen Problemi
OOP öğretilirken sıkça verilen klasik bir LSP ihlali örneği, Kare
sınıfını Dikdortgen
sınıfından türetmektir. İlk bakışta mantıklı görünebilir ("Kare bir Dikdörtgen'dir"), ancak davranışsal olarak sorun yaratır.
// LSP İHLALİ ÖRNEĞİ (C#)
public class Dikdortgen_LSP_Ihlali
{
public virtual double Genislik { get; set; }
public virtual double Yukseklik { get; set; }
public double AlanHesapla()
{
return Genislik * Yukseklik;
}
}
public class Kare_LSP_Ihlali : Dikdortgen_LSP_Ihlali
{
private double _kenar;
public override double Genislik
{
get { return _kenar; }
set { _kenar = value; /*Yukseklik = value; // LSP İHLALİ! */ }
// Eğer yukarıdaki satır eklenirse, Dikdörtgen'in Genislik set etme davranışı değişir.
// Eklenmezse, kare özelliği (genişlik=yükseklik) bozulur.
}
public override double Yukseklik
{
get { return _kenar; }
set { _kenar = value; /*Genislik = value; // LSP İHLALİ! */ }
}
// Basitleştirilmiş constructor
public Kare_LSP_Ihlali(double kenar) { this._kenar = kenar; }
}
public class MusteriKodu_LSP_Ihlali
{
public void Calistir(Dikdortgen_LSP_Ihlali dikdortgen)
{
// Bu kod Dikdörtgen için doğru çalışır:
dikdortgen.Genislik = 5;
dikdortgen.Yukseklik = 10;
// Beklenti: Alan = 5 * 10 = 50
double beklenenAlan = 5 * 10;
double gercekAlan = dikdortgen.AlanHesapla();
Console.WriteLine($"Beklenen Alan: {beklenenAlan}, Gerçek Alan: {gercekAlan}");
if (beklenenAlan != gercekAlan)
{
Console.WriteLine("HATA! LSP İhlal edildi!");
// Eğer Kare sınıfında set metotları birbirini etkileyecek şekilde override edilirse,
// Yukseklik=10 ataması Genislik'i de 10 yapar ve alan 100 çıkar. Beklenti bozulur.
// Eğer override edilmezse, Genislik=5, Yukseklik=10 olur ki bu da Kare değildir.
}
}
}
// Kullanım:
// MusteriKodu_LSP_Ihlali musteri = new MusteriKodu_LSP_Ihlali();
// Dikdortgen_LSP_Ihlali d1 = new Dikdortgen_LSP_Ihlali();
// Kare_LSP_Ihlali k1 = new Kare_LSP_Ihlali(4);
// musteri.Calistir(d1); // Beklenen Alan: 50, Gerçek Alan: 50
// musteri.Calistir(k1); // Beklenen Alan: 50, Gerçek Alan: 100 (veya başka bir tutarsızlık) -> HATA!
Buradaki sorun, Kare
nesnesinin, Dikdortgen
nesnesinin yerine geçtiğinde, Dikdortgen
için geçerli olan "genişlik ve yüksekliğin bağımsız olarak ayarlanabilmesi" sözleşmesini bozmasıdır. MusteriKodu_LSP_Ihlali
, kendisine verilen nesnenin bir Dikdortgen
gibi davranmasını bekler, ancak Kare
bu beklentiyi karşılamaz.
LSP Uygulanmış Çözüm
Bu tür ihlalleri çözmenin yolları:
- Kalıtım Hiyerarşisini Gözden Geçirme: Belki de Kare, Dikdörtgen'in alt tipi değildir. Daha genel bir
Sekil
soyut sınıfı veya arayüzü tanımlanabilir ve hem Dikdortgen
hem de Kare
bu soyutlamadan türetilebilir.
- Davranışı Değiştirmeme: Alt sınıf, üst sınıfın metotlarının davranışını (önkoşul, sonkoşul, değişmezler) korumalıdır.
- İstisnalar: Alt sınıf, üst sınıfın beklemediği yeni istisnalar fırlatmamalıdır.
Kare/Dikdörtgen örneği için daha iyi bir yaklaşım, ortak bir arayüz veya soyut sınıf kullanmaktır:
// LSP UYGULANMIŞ ÇÖZÜM (C#)
public interface ISekil_LSP // Veya abstract class Sekil
{
double AlanHesapla();
// Belki CevreHesapla() da eklenebilir
}
// Dikdörtgen artık Kare'nin üst sınıfı değil
public class Dikdortgen_LSP : ISekil_LSP
{
public double Genislik { get; set; }
public double Yukseklik { get; set; }
public double AlanHesapla()
{
return Genislik * Yukseklik;
}
}
// Kare de doğrudan ISekil'den türer
public class Kare_LSP : ISekil_LSP
{
public double Kenar { get; set; }
public double AlanHesapla()
{
return Kenar * Kenar;
}
}
// Müşteri kodu artık ISekil ile çalışır
public class MusteriKodu_LSP
{
// Bu metot artık sadece alanı hesaplar, genişlik/yükseklik ataması yapmaz
// veya atama yapacaksa nesnenin tipine göre farklı davranması gerekebilir
// ki bu da LSP'yi başka yerden ihlal etme riski taşır.
// Genellikle LSP, nesnelerin değiştirilebilir durumları yerine
// sorgulanabilir davranışları üzerinden daha kolay sağlanır.
public void AlanYazdir(ISekil_LSP sekil)
{
Console.WriteLine($"Şeklin Alanı: {sekil.AlanHesapla()}");
}
}
// Kullanım:
// MusteriKodu_LSP musteri = new MusteriKodu_LSP();
// ISekil_LSP d1 = new Dikdortgen_LSP { Genislik = 5, Yukseklik = 10 };
// ISekil_LSP k1 = new Kare_LSP { Kenar = 4 };
// musteri.AlanYazdir(d1); // Şeklin Alanı: 50
// musteri.AlanYazdir(k1); // Şeklin Alanı: 16
// Artık beklenmedik davranış yok çünkü her şekil kendi sözleşmesini (AlanHesapla) doğru uyguluyor.
LSP, kalıtımın getirdiği potansiyel karmaşıklığı yönetmek ve polimorfizmin güvenilir bir şekilde çalışmasını sağlamak için hayati öneme sahiptir.
I: Arayüz Ayırma Prensibi (Interface Segregation Principle - ISP)
Arayüz Ayırma Prensibi, SOLID'in dördüncü harfini oluşturur ve şunu savunur: İstemciler (bir arayüzü kullanan sınıflar), kullanmadıkları metotları içeren arayüzleri uygulamaya zorlanmamalıdır. Başka bir deyişle, büyük, her şeyi kapsayan "şişman" arayüzler yerine, daha küçük, özelleşmiş ve istemcinin ihtiyacına odaklanmış arayüzler oluşturulmalıdır.
ISP'nin Amacı Nedir?
ISP'nin hedefleri şunlardır:
- Gereksiz Bağımlılıkları Azaltma: Bir sınıf, kullanmadığı metotları içeren bir arayüzü uygularsa, o arayüzdeki (kullanmadığı) bir değişiklik bile o sınıfı yeniden derlemeye veya değiştirmeye zorlayabilir. ISP bu gereksiz bağımlılığı ortadan kaldırır.
- Yüksek Uyum (Cohesion): Arayüzler daha küçük ve belirli bir role odaklı hale gelir.
- Daha İyi Tasarım ve Anlaşılabilirlik: Küçük, role özgü arayüzleri anlamak ve kullanmak daha kolaydır.
- Gereksiz Implementasyonlardan Kaçınma: Sınıflar, ihtiyaç duymadıkları metotları boş veya hata fırlatan şekilde implemente etmek zorunda kalmazlar.
- Esneklik ve Yeniden Kullanılabilirlik: Küçük arayüzler, farklı kombinasyonlarda kullanılarak daha esnek sistemler oluşturmayı sağlar.
ISP ihlali, genellikle tek bir arayüzün çok fazla farklı sorumluluğu veya yeteneği bir araya getirmeye çalışmasıyla ortaya çıkar.
ISP İhlali Örneği
Farklı yeteneklere sahip makineleri (yazdırma, tarama, faks) temsil etmeye çalışan tek bir "şişman" arayüz düşünelim:
// ISP İHLALİ ÖRNEĞİ (C#)
// "Şişman" Arayüz
public interface IMultiFonksiyonMakine_ISP_Ihlali
{
void Yazdir(string belge);
void Tara(string belge);
void FaksGonder(string belge, string numara);
}
// Sadece yazdırma yapabilen basit bir yazıcı
public class BasitYazici_ISP_Ihlali : IMultiFonksiyonMakine_ISP_Ihlali
{
public void Yazdir(string belge)
{
Console.WriteLine($"'{belge}' yazdırılıyor...");
}
// Bu metotları uygulamak zorunda ama işlevi yok!
public void Tara(string belge)
{
// Boş bırakılabilir veya hata fırlatılabilir
throw new NotImplementedException("Tarama desteklenmiyor.");
}
public void FaksGonder(string belge, string numara)
{
// Boş bırakılabilir veya hata fırlatılabilir
throw new NotImplementedException("Faks desteklenmiyor.");
}
}
// Tüm fonksiyonları olan makine
public class GelismisMakine_ISP_Ihlali : IMultiFonksiyonMakine_ISP_Ihlali
{
public void Yazdir(string belge) { /* ... */ Console.WriteLine("Gelişmiş Yazdırma"); }
public void Tara(string belge) { /* ... */ Console.WriteLine("Gelişmiş Tarama"); }
public void FaksGonder(string belge, string numara) { /* ... */ Console.WriteLine("Gelişmiş Faks"); }
}
// Müşteri Kodu
public class OfisCalisani_ISP_Ihlali
{
// Bu çalışan sadece yazdırmaya ihtiyaç duyuyor olsa bile,
// makinenin Tara ve FaksGonder metotlarına da bağımlı hale geliyor.
public void BelgeYazdir(IMultiFonksiyonMakine_ISP_Ihlali makine, string belge)
{
makine.Yazdir(belge);
}
}
Buradaki sorun, BasitYazici_ISP_Ihlali
sınıfının, aslında sahip olmadığı Tara
ve FaksGonder
metotlarını anlamsız bir şekilde implemente etmek zorunda kalmasıdır. Ayrıca, OfisCalisani_ISP_Ihlali
sınıfı sadece yazdırma işlevine ihtiyaç duysa bile, tüm IMultiFonksiyonMakine_ISP_Ihlali
arayüzüne bağımlı olur. Arayüzdeki tarama veya faksla ilgili bir değişiklik (ki bu çalışanın ihtiyacı olmayan bir değişikliktir), bu çalışanın kodunu etkileyebilir.
ISP Uygulanmış Çözüm
Çözüm, "şişman" arayüzü daha küçük, role özgü arayüzlere ayırmaktır:
// ISP UYGULANMIŞ ÇÖZÜM (C#)
// Küçük, Role Özgü Arayüzler
public interface IYazdirici
{
void Yazdir(string belge);
}
public interface ITarayici
{
void Tara(string belge);
}
public interface IFaksGonderici
{
void FaksGonder(string belge, string numara);
}
// Sınıflar sadece ihtiyaç duydukları arayüzleri uygular
public class BasitYazici_ISP : IYazdirici // Sadece IYazdirici gerekli
{
public void Yazdir(string belge)
{
Console.WriteLine($"'{belge}' yazdırılıyor (Basit)...");
}
// Tara veya FaksGonder implementasyonu gerekmez!
}
public class FotokopiMakinesi_ISP : IYazdirici, ITarayici // Hem yazdırır hem tarar
{
public void Yazdir(string belge) { Console.WriteLine("Fotokopi: Yazdırıldı."); }
public void Tara(string belge) { Console.WriteLine("Fotokopi: Tarandı."); }
}
public class GelismisMakine_ISP : IYazdirici, ITarayici, IFaksGonderici // Tüm yetenekler
{
public void Yazdir(string belge) { Console.WriteLine("Gelişmiş: Yazdırıldı."); }
public void Tara(string belge) { Console.WriteLine("Gelişmiş: Tarandı."); }
public void FaksGonder(string belge, string numara) { Console.WriteLine("Gelişmiş: Faks Gönderildi."); }
}
// Müşteri Kodu sadece ihtiyaç duyduğu arayüze bağımlı olur
public class OfisCalisani_ISP
{
// Bu çalışan sadece IYazdirici arayüzünü bekler
public void BelgeYazdir(IYazdirici yazici, string belge)
{
Console.WriteLine("Ofis Çalışanı yazdırma işlemini başlatıyor...");
yazici.Yazdir(belge);
}
public void BelgeTara(ITarayici tarayici, string belge)
{
Console.WriteLine("Ofis Çalışanı tarama işlemini başlatıyor...");
tarayici.Tara(belge);
}
}
// Kullanım
// BasitYazici_ISP basitYazici = new BasitYazici_ISP();
// FotokopiMakinesi_ISP fotokopi = new FotokopiMakinesi_ISP();
// GelismisMakine_ISP gelismisMakine = new GelismisMakine_ISP();
//
// OfisCalisani_ISP calisan = new OfisCalisani_ISP();
// calisan.BelgeYazdir(basitYazici, "mektup.txt");
// calisan.BelgeYazdir(fotokopi, "rapor.docx");
// calisan.BelgeYazdir(gelismisMakine, "sunum.ppt");
//
// // calisan.BelgeTara(basitYazici, "resim.jpg"); // Hata! BasitYazici ITarayici değil.
// calisan.BelgeTara(fotokopi, "kimlik.pdf");
Bu yapıda, her sınıf sadece sahip olduğu yeteneklere karşılık gelen arayüzleri uygular. BasitYazici_ISP
gereksiz metotları implemente etmek zorunda kalmaz. OfisCalisani_ISP
ise sadece ihtiyaç duyduğu işlevselliği (IYazdirici
veya ITarayici
) talep eder, bu da bağımlılıkları azaltır ve sistemi daha esnek hale getirir.
D: Bağımlılıkların Tersine Çevrilmesi Prensibi (Dependency Inversion Principle - DIP)
Bağımlılıkların Tersine Çevrilmesi Prensibi, SOLID'in son harfini oluşturur ve gevşek bağlı (loosely coupled) sistemler tasarlamanın temelini atar. İki ana fikirden oluşur:
- Üst seviye modüller (politikaları belirleyenler), alt seviye modüllere (detayları implemente edenler) doğrudan bağımlı olmamalıdır. Her ikisi de soyutlamalara (arayüzler veya soyut sınıflar) bağımlı olmalıdır.
- Soyutlamalar detaylara bağımlı olmamalıdır. Detaylar (somut implementasyonlar) soyutlamalara bağımlı olmalıdır.
Basitçe ifade etmek gerekirse, kodumuzun belirli somut sınıflara ("detaylara") değil, soyut kavramlara ("arayüzlere") dayanması gerektiğini söyler. Bağımlılığın yönü, üst seviyeden alt seviyeye doğru değil, her ikisinden de soyutlamalara doğru "tersine çevrilir".
DIP'nin Amacı Nedir?
DIP'nin temel hedefleri:
- Gevşek Bağlılık (Loose Coupling): Üst seviye modüllerin, alt seviye modüllerin spesifik implementasyonlarından bağımsız hale gelmesini sağlar. Bu sayede alt seviye bir modül (detay) değiştirildiğinde veya yerine başka bir implementasyon konulduğunda, üst seviye modülün etkilenmemesi veya minimum düzeyde etkilenmesi sağlanır.
- Esneklik ve Değiştirilebilirlik: Sistemin farklı parçalarını (veritabanı, loglama mekanizması, e-posta servisi vb.) değiştirmek veya farklı implementasyonlarla çalışmak kolaylaşır.
- Test Edilebilirlik: Bağımlılıklar soyutlamalar üzerinden kurulduğu için, test sırasında gerçek bağımlılıklar yerine sahte (mock) nesnelerin kullanılması (Dependency Injection ile) kolaylaşır. Üst seviye modüller, alt seviye detaylardan bağımsız olarak test edilebilir.
- Yeniden Kullanılabilirlik: Hem üst seviye hem de alt seviye modüllerin farklı bağlamlarda yeniden kullanılma olasılığı artar.
DIP ihlali, genellikle üst seviye bir sınıfın içinde doğrudan alt seviye bir sınıfın örneğini new
anahtar kelimesi ile oluşturması şeklinde görülür. Bu, üst seviye sınıfı o spesifik alt seviye sınıfa sıkı sıkıya bağlar.
DIP İhlali Örneği
Bir bildirim sistemini düşünelim. Üst seviye modülümüz BildirimServisi
, alt seviye modülümüz ise EpostaGonderici
olsun.
// DIP İHLALİ ÖRNEĞİ (C#)
// Alt Seviye Modül (Detay)
public class EpostaGonderici_DIP_Ihlali
{
public void Gonder(string alici, string mesaj)
{
Console.WriteLine($"EPOSTA Gönderiliyor: Kime={alici}, Mesaj={mesaj}");
// ... Gerçek e-posta gönderme kodu ...
}
}
// Üst Seviye Modül (Politika)
public class BildirimServisi_DIP_Ihlali
{
// SIKI BAĞLILIK: BildirimServisi doğrudan EpostaGonderici sınıfını biliyor ve oluşturuyor!
private EpostaGonderici_DIP_Ihlali _epostaGonderici = new EpostaGonderici_DIP_Ihlali();
public void MusteriyeBildir(string musteriAdi, string mesaj)
{
string aliciEposta = $"{musteriAdi}@example.com"; // Basit örnek
_epostaGonderici.Gonder(aliciEposta, mesaj);
}
}
// Kullanım:
// BildirimServisi_DIP_Ihlali bildirim = new BildirimServisi_DIP_Ihlali();
// bildirim.MusteriyeBildir("Ahmet", "Siparişiniz kargoya verildi.");
Sorun: Eğer bildirim yöntemini e-postadan SMS'e veya push notification'a değiştirmek istersek, BildirimServisi_DIP_Ihlali
sınıfını doğrudan değiştirmemiz gerekir. Yeni bir SmsGonderici
sınıfı oluşturup _epostaGonderici
yerine onu new
ile oluşturmalıyız. Bu, DIP ihlalidir çünkü üst seviye modül (BildirimServisi), alt seviye modülün (EpostaGonderici) somut implementasyonuna sıkı sıkıya bağlıdır.
DIP Uygulanmış Çözüm (Dependency Injection ile)
Çözüm, bir soyutlama (arayüz) tanımlamak ve bağımlılığın dışarıdan (constructor aracılığıyla - Dependency Injection) verilmesidir.
// DIP UYGULANMIŞ ÇÖZÜM (C#)
// 1. Soyutlama (Arayüz)
public interface IBildirimGonderici
{
void Gonder(string hedef, string mesaj);
}
// 2. Alt Seviye Modüller (Detaylar) - Soyutlamaya Bağımlı
public class EpostaGonderici_DIP : IBildirimGonderici
{
public void Gonder(string hedefEposta, string mesaj)
{
Console.WriteLine($"EPOSTA Gönderiliyor: Kime={hedefEposta}, Mesaj={mesaj}");
// ... Gerçek e-posta gönderme kodu ...
}
}
public class SmsGonderici_DIP : IBildirimGonderici
{
public void Gonder(string hedefTelefon, string mesaj)
{
Console.WriteLine($"SMS Gönderiliyor: No={hedefTelefon}, Mesaj={mesaj}");
// ... Gerçek SMS gönderme kodu ...
}
}
// YENİ GÖNDERİCİ EKLEME (Sadece Yeni Kod)
public class PushNotificationGonderici : IBildirimGonderici
{
public void Gonder(string cihazToken, string mesaj)
{
Console.WriteLine($"PUSH Gönderiliyor: Token={cihazToken}, Mesaj={mesaj}");
// ...
}
}
// 3. Üst Seviye Modül (Politika) - Soyutlamaya Bağımlı
public class BildirimServisi_DIP
{
// GEVŞEK BAĞLILIK: BildirimServisi artık somut sınıfları bilmiyor, sadece arayüzü biliyor.
private readonly IBildirimGonderici _bildirimGonderici;
// Bağımlılık dışarıdan (Constructor Injection) enjekte ediliyor
public BildirimServisi_DIP(IBildirimGonderici gonderici)
{
_bildirimGonderici = gonderici; // Hangi göndericinin geleceğini bilmiyor, sadece arayüze uyduğunu biliyor
}
public void MusteriyeBildir(string musteriKimligi, string mesaj)
{
// Müşteri kimliğine göre hedef belirlenir (e-posta, telefon vb.)
string hedef = $"HedefBilgi({musteriKimligi})";
_bildirimGonderici.Gonder(hedef, mesaj); // Arayüz üzerinden metot çağrılır
}
}
// --- Kullanım (Dependency Injection - Manuel veya DI Konteyneri ile) ---
// Hangi göndericinin kullanılacağına karar veren yer (Composition Root)
IBildirimGonderici emailGonderici = new EpostaGonderici_DIP();
IBildirimGonderici smsGonderici = new SmsGonderici_DIP();
IBildirimGonderici pushGonderici = new PushNotificationGonderici();
// BildirimServisi'ni farklı göndericilerle oluşturma
BildirimServisi_DIP epostaBildirim = new BildirimServisi_DIP(emailGonderici);
BildirimServisi_DIP smsBildirim = new BildirimServisi_DIP(smsGonderici);
BildirimServisi_DIP pushBildirim = new BildirimServisi_DIP(pushGonderici);
// Kullanım
// epostaBildirim.MusteriyeBildir("ali@example.com", "Hoş geldiniz!");
// smsBildirim.MusteriyeBildir("5551234567", "Kampanya başladı!");
// pushBildirim.MusteriyeBildir("DEVICE_TOKEN_XYZ", "Yeni mesajınız var.");
Bu yapıda:
BildirimServisi_DIP
artık somut EpostaGonderici_DIP
veya SmsGonderici_DIP
sınıflarını bilmez, sadece IBildirimGonderici
arayüzünü bilir.
- Hangi somut göndericinin kullanılacağına
BildirimServisi_DIP
'nin kendisi değil, onu oluşturan dış kod (veya bir DI konteyneri) karar verir. Bağımlılık dışarıdan enjekte edilir.
- Yeni bir bildirim yöntemi (
PushNotificationGonderici
) eklemek istediğimizde, sadece yeni sınıfı oluşturup IBildirimGonderici
arayüzünü implemente etmemiz yeterlidir. BildirimServisi_DIP
sınıfında hiçbir değişiklik yapmaya gerek kalmaz.
BildirimServisi_DIP
sınıfı artık kolayca test edilebilir. Test sırasında gerçek göndericiler yerine sahte (mock) bir IBildirimGonderici
implementasyonu enjekte edilebilir.
DIP, Bağımlılık Enjeksiyonu (DI) pattern'i ve IoC (Inversion of Control) prensibi ile yakından ilişkilidir ve modern, esnek, test edilebilir yazılım mimarilerinin temelini oluşturur.
Sonuç: SOLID ile Daha İyi Yazılımlara Doğru
SOLID prensipleri (Tek Sorumluluk, Açık/Kapalı, Liskov Yerine Geçme, Arayüz Ayırma, Bağımlılıkların Tersine Çevrilmesi), Nesne Yönelimli Programlama'yı kullanarak daha sağlam, esnek, anlaşılır ve sürdürülebilir yazılımlar geliştirmek için paha biçilmez bir rehberlik sunar. Bu beş prensip, birbirini tamamlayarak kodun kalitesini artırmaya odaklanır.
Tek Sorumluluk Prensibi (SRP) ile sınıflarımızı odaklı tutarız. Açık/Kapalı Prensibi (OCP) sayesinde sistemimizi mevcut kodu kırmadan genişletilebilir hale getiririz. Liskov Yerine Geçme Prensibi (LSP), kalıtımı doğru kullanarak tutarlı davranışlar sergileyen hiyerarşiler kurmamızı sağlar. Arayüz Ayırma Prensibi (ISP), gereksiz bağımlılıkları ortadan kaldırarak daha küçük ve amaca yönelik arayüzler tasarlamamızı teşvik eder. Son olarak, Bağımlılıkların Tersine Çevrilmesi Prensibi (DIP), soyutlamalara dayanarak gevşek bağlı ve test edilebilir sistemler inşa etmemize olanak tanır.
Bu prensipleri anlamak ve uygulamak, başlangıçta ek bir çaba veya düşünce gerektirebilir. Her durumda her prensibi katı bir şekilde uygulamak her zaman en iyi çözüm olmayabilir; bazen pragmatik olmak ve projenin özel gereksinimlerine göre esneklik göstermek gerekebilir. Ancak SOLID'in arkasındaki temel fikirleri (uyum, bağlılık, soyutlama, değiştirilebilirlik) kavramak, daha iyi tasarım kararları vermenize ve uzun vadede yönetimi daha kolay, hatalara karşı daha dirençli ve değişime daha açık yazılımlar oluşturmanıza yardımcı olacaktır. SOLID, sadece bir dizi kural değil, aynı zamanda daha profesyonel ve etkili bir yazılım geliştirici olma yolunda önemli bir adımdır.