Yazılım geliştirme dünyası, özellikle Nesne Yönelimli Programlama (Object-Oriented Programming — OOP) paradigması ile birlikte, karmaşıklığı yönetme, esnekliği artırma ve sürdürülebilir kod tabanları oluşturma ihtiyacını beraberinde getirmiştir. Zamanla büyüyen, değişen gereksinimlere adapte olması gereken ve birden fazla geliştirici tarafından üzerinde çalışılan projelerde, kodun iyi yapılandırılmamış olması ciddi sorunlara yol açabilir: Bakımı zorlaşır, hataları ayıklamak kabusa dönüşür, yeni özellikler eklemek riskli hale gelir ve kod tekrarı artar.
İşte bu noktada, yazılım tasarımını iyileştirmek ve daha sağlam, anlaşılır, esnek ve bakımı kolay OOP sistemleri oluşturmak için ortaya konmuş bir dizi prensip devreye girer: SOLID. SOLID, Amerikalı yazılım mühendisi ve yazar Robert C. Martin (Uncle Bob) tarafından popülerleştirilen ve beş temel tasarım prensibinin baş harflerinden oluşan bir akronimdir.
SOLID prensipleri, katı kurallar veya bir framework olmaktan ziyade, OOP tasarımında yol gösterici ilkelerdir. Bu prensiplere uymak, yazılımın “kötü kokmasını” (code smell) engellemeye yardımcı olur ve zamanla ortaya çıkabilecek birçok yaygın tasarım sorununu önlemeyi hedefler. Python gibi OOP’yi destekleyen dillerde bu prensipleri anlamak ve uygulamak, daha profesyonel ve uzun ömürlü yazılımlar geliştirmek için kritik öneme sahiptir.
Bu rehberde, SOLID prensiplerinin ne olduğuna genel bir bakış yapacağız, her bir prensibi (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) ayrı ayrı açıklayacak ve en önemlisi, bu prensiplere uymanın yazılım geliştirme sürecine ve sonuç ürüne sağladığı önemli faydaları detaylandıracağız.
Bölüm 1: SOLID Nedir? Beş Temel Prensip
SOLID, aşağıdaki beş OOP tasarım prensibinin baş harflerinden oluşan bir kısaltmadır:
S — Single Responsibility Principle (SRP — Tek Sorumluluk Prensibi)
O — Open/Closed Principle (OCP — Açık/Kapalı Prensibi)
L — Liskov Substitution Principle (LSP — Liskov Yerine Geçme Prensibi)
I — Interface Segregation Principle (ISP — Arayüz Ayırma Prensibi)
D — Dependency Inversion Principle (DIP — Bağımlılığın Tersine Çevrilmesi Prensibi)
Bu prensiplerin her biri, yazılım tasarımının farklı bir yönüne odaklanır ancak hepsi birlikte, daha anlaşılır, esnek, sürdürülebilir ve test edilebilir nesne yönelimli sistemler oluşturmaya katkıda bulunur. Bunlar sihirli formüller değildir, ancak yılların deneyimiyle ortaya çıkmış, kendini kanıtlamış iyi tasarım uygulamalarıdır.
Bölüm 2: SOLID Prensiplerinin Önemi: Neden Kullanmalıyız?
SOLID prensiplerini öğrenmek ve uygulamak neden bu kadar önemlidir? Çünkü bu prensipler, yazılım geliştirmenin doğasında var olan zorluklarla başa çıkmamıza yardımcı olur ve aşağıdaki somut faydaları sağlar:
Artan Okunabilirlik ve Anlaşılabilirlik (Increased Readability and Understandability):
SOLID prensiplerine uygun kod, genellikle daha iyi organize edilmiş, daha küçük ve daha odaklanmış sınıflardan oluşur. Her sınıfın veya modülün net bir sorumluluğu olduğu için, kodun ne yaptığını anlamak ve takip etmek kolaylaşır. Yeni bir geliştiricinin projeye adapte olması hızlanır.
Geliştirilmiş Bakım Kolaylığı (Improved Maintainability):
Bu, belki de SOLID’in en önemli faydasıdır. Kodda bir değişiklik yapılması veya bir hatanın düzeltilmesi gerektiğinde, prensiplere uyulmuşsa, değişikliğin etkisi genellikle daha sınırlı olur. Tek Sorumluluk Prensibi sayesinde, bir değişiklik genellikle sadece tek bir sınıfı etkiler. Açık/Kapalı Prensibi, mevcut kodu bozma riskini azaltır. Bu, bakım maliyetlerini düşürür ve süreci hızlandırır.
Daha Yüksek Esneklik ve Genişletilebilirlik (Higher Flexibility and Extensibility):
SOLID, değişime daha kolay adapte olabilen sistemler tasarlamamıza yardımcı olur. Özellikle Açık/Kapalı Prensibi ve Bağımlılığın Tersine Çevrilmesi Prensibi, yeni işlevsellikler eklemeyi veya mevcutları değiştirmeyi, sistemin geri kalanını minimum düzeyde etkileyerek mümkün kılar. Yeni gereksinimler ortaya çıktığında sistemi genişletmek daha kolay ve daha az riskli hale gelir.
Artan Kod Yeniden Kullanılabilirliği (Increased Code Reusability):
Tek bir sorumluluğa odaklanan, iyi tanımlanmış arayüzlere sahip ve diğer bileşenlerden gevşek bir şekilde bağlı olan sınıflar, farklı projelerde veya aynı projenin farklı bölümlerinde daha kolay yeniden kullanılabilir.
Geliştirilmiş Test Edilebilirlik (Improved Testability):
SOLID prensipleri genellikle daha küçük, daha odaklanmış ve daha az bağımlı sınıflarla sonuçlanır. Bu tür sınıfları izole bir şekilde test etmek (birim testi — unit testing) çok daha kolaydır. Bağımlılıkların soyutlamalara (arayüzler/ABCs) dayandırılması (DIP), testlerde sahte nesneler (mocks/stubs) kullanmayı kolaylaştırarak testlerin daha güvenilir ve hızlı olmasını sağlar.
Azaltılmış Bağlantı (Reduced Coupling):
SOLID prensipleri (özellikle ISP ve DIP), sistemin farklı bileşenleri arasındaki bağımlılıkları azaltmayı hedefler. Bileşenler birbirlerinin iç detaylarına değil, kararlı soyutlamalara (arayüzlere) bağımlı hale gelir. Bu “gevşek bağlantı” (loose coupling), bir bileşendeki değişikliğin diğerlerini etkileme olasılığını azaltır.
Artan İçsel Uyum (Increased Cohesion):
Tek Sorumluluk Prensibi ve Arayüz Ayırma Prensibi, bir sınıfın veya modülün içindeki öğelerin ne kadar ilişkili ve odaklanmış olduğunu ifade eden “içsel uyumu” (cohesion) artırır. Yüksek içsel uyuma sahip bileşenlerin anlaşılması, test edilmesi ve bakımı daha kolaydır.
Daha İyi Tasarım Kararları İçin Rehberlik:
SOLID prensipleri, tasarım aşamasında veya kod refactoring sırasında yol gösterici ilkeler sunar. Belirli bir tasarımın potansiyel sorunlarını erkenden fark etmeye ve daha iyi alternatifler düşünmeye teşvik eder.
Kısacası, SOLID prensiplerine uymak, sadece “doğru” olanı yapmak değil, aynı zamanda uzun vadede daha az baş ağrısı, daha düşük maliyet ve daha başarılı projeler anlamına gelir. Kısa vadede biraz daha fazla düşünme ve planlama gerektirse de, projenin ömrü boyunca bunun karşılığını fazlasıyla verir.
Bölüm 3: SOLID Prensiplerine Detaylı Bakış
Şimdi her bir prensibi daha yakından inceleyelim:
3.1. S: Single Responsibility Principle (SRP — Tek Sorumluluk Prensibi)
Bir sınıfın değişmesi için sadece tek bir nedeni olmalıdır.
Veya başka bir deyişle:
Bir sınıfın sadece tek bir sorumluluğu olmalıdır.
Temel Fikir: Bir sınıf, programın sadece tek bir yönüyle veya tek bir aktörüyle (kullanıcı, başka bir sistem vb.) ilgili olmalıdır. Eğer bir sınıfın değişmesi için birden fazla neden (farklı sorumluluklar) varsa, bu prensip ihlal ediliyor demektir.
Neden Önemli?
Değişiklik Etkisini Azaltır: Bir sorumluluk değiştiğinde, sadece o sorumluluğa sahip sınıfın değiştirilmesi gerekir. Diğer sorumluluklar etkilenmez.
Artan İçsel Uyum (Cohesion): Sınıfın içindeki tüm öğeler (metotlar, nitelikler) tek bir amaca hizmet ettiği için daha tutarlı ve anlaşılır olur.
Daha Kolay Test Edilebilirlik: Tek bir sorumluluğu olan sınıfı test etmek, birçok işi yapan karmaşık bir sınıfı test etmekten daha kolaydır.
Kod Tekrarını Önleme (Dolaylı): Farklı sınıfların aynı ilgisiz sorumluluğu tekrar tekrar implemente etmesini engelleyebilir.
Python Örneği (İhlal):
SRP İHLALİ class KullaniciAyarlari: def init(self, kullanici): self.kullanici = kullanici def ayarlari_getir(self): # 1. Sorumluluk: Veritabanından ayarları çekme print(f"{self.kullanici} için veritabanından ayarlar çekiliyor...") # ... db kodu ... ayarlar = {"tema": "koyu", "dil": "tr"} return ayarlar def ayarlari_dogrula(self, ayarlar): # 2. Sorumluluk: Ayarları doğrulama print("Ayarlar doğrulanıyor...") if "tema" not in ayarlar or "dil" not in ayarlar: print("Eksik ayar!") return False return True def ayarlari_kaydet(self, ayarlar): # 3. Sorumluluk: Ayarları veritabanına kaydetme if self.ayarlari_dogrula(ayarlar): print(f"{self.kullanici} için ayarlar veritabanına kaydediliyor...") # ... db kodu ... print("Ayarlar kaydedildi.") else: print("Geçersiz ayarlar kaydedilemedi.") # Bu sınıfın değişmesi için 3 farklı neden var: # 1. Veritabanı erişim mantığı değişirse. # 2. Ayar doğrulama kuralları değişirse. # 3. Ayar kaydetme/getirme şekli değişirse.
Python Örneği (Uyumlu):
SRP UYUMLU YAKLAŞIM class AyarDogrulayici: def dogrula(self, ayarlar): print("Ayarlar doğrulanıyor...") if "tema" not in ayarlar or "dil" not in ayarlar: print("Eksik ayar!") return False return True class AyarRepository: # Veri erişim katmanı def getir(self, kullanici): print(f"{kullanici} için veritabanından ayarlar çekiliyor...") # ... db kodu ... return {"tema": "koyu", "dil": "tr"} def kaydet(self, kullanici, ayarlar): print(f"{kullanici} için ayarlar veritabanına kaydediliyor...") # ... db kodu ... print("Ayarlar kaydedildi.") class KullaniciAyarlariServisi: # Koordinasyon sınıfı def init(self, kullanici): self.kullanici = kullanici # Bağımlılıkları dışarıdan almak daha iyi (DIP), ama şimdilik basit tutalım self.dogrulayici = AyarDogrulayici() self.repo = AyarRepository() def ayarlari_getir(self): return self.repo.getir(self.kullanici) def ayarlari_kaydet(self, ayarlar): if self.dogrulayici.dogrula(ayarlar): self.repo.kaydet(self.kullanici, ayarlar) else: print("Geçersiz ayarlar kaydedilemedi.") # Şimdi her sınıfın tek bir sorumluluğu var ve değişmek için tek bir nedeni var.
3.2. O: Open/Closed Principle (OCP — Açık/Kapalı Prensibi)
Yazılım varlıkları (sınıflar, modüller, fonksiyonlar vb.) genişletmeye açık (open for extension), ancak değiştirmeye kapalı (closed for modification) olmalıdır.
Temel Fikir: Yeni bir işlevsellik eklemek veya mevcut bir işlevselliği değiştirmek gerektiğinde, var olan, çalışan ve test edilmiş kodu doğrudan değiştirmek yerine, sisteme yeni kod (yeni sınıflar, modüller) ekleyerek bu değişikliği yapabilmeliyiz.
Nasıl Sağlanır? Genellikle soyutlama (abstraction) mekanizmalarıyla sağlanır:
Kalıtım: Yeni davranışlar eklemek veya mevcutları override etmek için alt sınıflar oluşturulur.
Arayüzler/ABCs: Ortak bir arayüz tanımlanır ve yeni implementasyonlar bu arayüze uyan yeni sınıflar olarak eklenir.
Kompozisyon ve Strateji Deseni: Farklı davranışları temsil eden nesneler (stratejiler) çalışma zamanında değiştirilebilir.
Neden Önemli?
Kararlılık (Stability): Mevcut, çalışan kodu değiştirmek her zaman risklidir (regression — mevcut işlevselliği bozma riski). OCP bu riski azaltır.
Bakım Kolaylığı: Yeni özellikler eklemek için tüm kodu gözden geçirmek ve değiştirmek yerine, sadece yeni kod eklemek daha kolay ve daha az hataya açıktır.
Genişletilebilirlik: Sistemin yeni gereksinimlere adapte olmasını kolaylaştırır.
Python Örneği (İhlal):
OCP İHLALİ class RaporUretici: def uret(self, rapor_tipi, veri): if rapor_tipi == "PDF": print(f"Veriden PDF raporu üretiliyor: {veri}") # ... pdf üretme kodu ... return "rapor.pdf" elif rapor_tipi == "Excel": print(f"Veriden Excel raporu üretiliyor: {veri}") # ... excel üretme kodu ... return "rapor.xlsx" # YENİ bir rapor tipi (örn: HTML) eklemek için bu sınıfı DEĞİŞTİRMEK GEREKİR! # elif rapor_tipi == "HTML": # ... else: print("Desteklenmeyen rapor tipi.") return None r = RaporUretici() r.uret("PDF", ["a", "b"]) r.uret("Excel", ["x", "y"])
Python Örneği (Uyumlu — ABC ile):
OCP UYUMLU YAKLAŞIM (ABC ve Kalıtım ile) import abc class RaporFormatlayici(abc.ABC): # Soyut temel sınıf (Arayüz) @abc.abstractmethod def formatla(self, veri) -> str: """Veriyi formatlar ve dosya adını döndürür.""" pass class PdfFormatlayici(RaporFormatlayici): def formatla(self, veri) -> str: print(f"Veri PDF olarak formatlanıyor: {veri}") # ... pdf formatlama kodu ... return "rapor.pdf" class ExcelFormatlayici(RaporFormatlayici): def formatla(self, veri) -> str: print(f"Veri Excel olarak formatlanıyor: {veri}") # ... excel formatlama kodu ... return "rapor.xlsx" # YENİ bir format eklemek için sadece YENİ bir sınıf eklemek yeterli: class HtmlFormatlayici(RaporFormatlayici): def formatla(self, veri) -> str: print(f"Veri HTML olarak formatlanıyor: {veri}") # ... html formatlama kodu ... return "rapor.html" class RaporServisi: # Bu sınıf artık formatlama detayını bilmiyor def init(self, formatlayici: RaporFormatlayici): # Soyutlamaya bağımlı self.formatlayici = formatlayici def rapor_olustur(self, veri): print("Rapor oluşturuluyor...") dosya_adi = self.formatlayici.formatla(veri) # Polimorfizm print(f"Rapor oluşturuldu: {dosya_adi}") # Kullanım pdf_uretici = RaporServisi(PdfFormatlayici()) excel_uretici = RaporServisi(ExcelFormatlayici()) html_uretici = RaporServisi(HtmlFormatlayici()) # Yeni format kolayca eklendi veriler = ["a", "b", "c"] pdf_uretici.rapor_olustur(veriler) excel_uretici.rapor_olustur(veriler) html_uretici.rapor_olustur(veriler) # RaporServisi sınıfı hiç DEĞİŞTİRİLMEDİ! Genişletmeye açık, değiştirmeye kapalı.
3.3. L: Liskov Substitution Principle (LSP — Liskov Yerine Geçme Prensibi)
Eğer S, T’nin bir alt tipi ise, T türündeki nesneler için yazılmış programların davranışı değiştirilmeden (veya bozulmadan) S türündeki nesneler T türündeki nesnelerin yerine kullanılabilmelidir.
Temel Fikir: Alt sınıflar, üst sınıflarının yerine tamamen geçebilmeli ve programın beklentilerini bozmamalıdır. Bir alt sınıf nesnesi, üst sınıf nesnesinin kullanılabildiği her yerde, programın doğruluğunu etkilemeden kullanılabilmelidir. Alt sınıflar, üst sınıfın kontratını (beklenen davranışını) daraltmamalı veya ihlal etmemelidir.
Neden Önemli?
Güvenilir Kalıtım Hiyerarşileri: Kalıtımın doğru kullanıldığından emin olmayı sağlar.
Öngörülebilir Polimorfizm: Üst sınıf referansı üzerinden alt sınıf metotları çağrıldığında beklenmedik davranışların veya hataların ortaya çıkmasını engeller.
Kodun Doğruluğu: Alt sınıfların üst sınıfın yerine geçtiği durumlarda programın mantıksal tutarlılığını korur.
İhlal Belirtileri:
Alt sınıf metodunun, üst sınıf metodundan daha kısıtlı parametreler kabul etmesi.
Alt sınıf metodunun, üst sınıf metodunun fırlatmadığı yeni türde istisnalar fırlatması.
Alt sınıf metodunun, üst sınıf metodunun döndürdüğü tiple uyumsuz bir tip döndürmesi.
Alt sınıf metodunun, üst sınıfın varsayımlarını (invariants) veya son koşullarını (postconditions) ihlal etmesi.
Kodda sık sık isinstance() kontrolü yapılarak alt sınıfa göre farklı davranma ihtiyacı duyulması (bu, yerine geçmenin tam sağlanamadığını gösterebilir).
Python Örneği (Klasik İhlal — Kare/Dikdörtgen):
LSP İHLALİ (Potansiyel) class Dikdortgen: def init(self, genislik, yukseklik): self.genislik = genislik self._yukseklik = yukseklik @property def genislik(self): return self._genislik @genislik.setter def genislik(self, value): self._genislik = value @property def yukseklik(self): return self._yukseklik @yukseklik.setter def yukseklik(self, value): self._yukseklik = value def alan(self): return self.genislik * self.yukseklik class Kare(Dikdortgen): def __init(self, kenar): super().init_(kenar, kenar) # Setter'ları override ederek kare özelliğini koruyalım @Dikdortgen.genislik.setter # Üst sınıfın setter'ını dekore et def genislik(self, value): self._genislik = value self._yukseklik = value # Genişlik değişince yükseklik de değişmeli @Dikdortgen.yukseklik.setter def yukseklik(self, value): self._genislik = value # Yükseklik değişince genişlik de değişmeli self._yukseklik = value # Dikdörtgen bekleyen bir fonksiyon def kullanici_fonksiyon(d: Dikdortgen): # Bu fonksiyon, genişliği ayarlamanın yüksekliği DEĞİŞTİRMEYECEĞİNİ varsayar w = 10 h = 20 d.genislik = w d.yukseklik = h beklenen_alan = w * h gercek_alan = d.alan() print(f"Beklenen Alan: {beklenen_alan}, Gerçek Alan: {gercek_alan}") assert gercek_alan == beklenen_alan # Bu iddia doğru mu? dikdortgen = Dikdortgen(2, 3) kare = Kare(5) print("Dikdörtgen ile test:") kullanici_fonksiyon(dikdortgen) # Beklenen Alan: 200, Gerçek Alan: 200 -> Başarılı print("\nKare ile test:") kullanici_fonksiyon(kare) # Beklenen Alan: 200, Gerçek Alan: 400 -> AssertionError! # Çünkü kare.yukseklik = 20 ataması, genişliği de 20 yaptı. # Kare nesnesi, Dikdörtgen nesnesinin yerine tamamen GEÇEMEDİ, fonksiyonun beklentisini bozdu
Çözüm: Bu durumda Kare’nin Dikdörtgen’den miras alması LSP’yi ihlal eder. Ya kalıtım ilişkisi kaldırılmalı (kompozisyon düşünülebilir) ya da kullanici_fonksiyon gibi fonksiyonlar nesnelerin spesifik davranışlarına karşı daha dikkatli olmalıdır (ki bu da LSP'nin ruhuna aykırıdır). Genellikle en iyi çözüm, "Is-A" ilişkisinin gerçekten geçerli olup olmadığını sorgulamaktır.
3.4. I: Interface Segregation Principle (ISP — Arayüz Ayırma Prensibi)
İstemciler (sınıflar), kullanmadıkları metotları içeren arayüzlere bağımlı olmaya zorlanmamalıdır.
Veya başka bir deyişle:
Büyük, “şişman” arayüzler yerine daha küçük, daha spesifik arayüzler tercih edilmelidir.
Temel Fikir: Bir sınıfın ihtiyaç duymadığı işlevleri içeren geniş bir arayüze (veya soyut sınıfa) bağımlı olması yerine, sadece ihtiyaç duyduğu metotları içeren daha küçük, odaklanmış arayüzlere bağımlı olması daha iyidir.
Neden Önemli?
Azaltılmış Bağlantı (Decoupling): Sınıflar sadece gerçekten kullandıkları işlevlere bağımlı olur. Kullanmadıkları bir metodun arayüzde değişmesi, onları etkilemez.
Artan İçsel Uyum (Cohesion): Hem arayüzler hem de onları implemente eden sınıflar daha odaklanmış ve tutarlı hale gelir.
Daha Kolay Implementasyon: Sınıfların, kullanmayacakları gereksiz metotları boşuna implemente etmek zorunda kalmasını engeller.
Daha İyi Tasarım: Sistemi daha küçük, daha yönetilebilir parçalara ayırmayı teşvik eder.
Python Örneği (İhlal):
import abc # ISP İHLALİ - "Şişman" Arayüz class CalisanArayuzu(abc.ABC): @abc.abstractmethod def calis(self): pass @abc.abstractmethod def yemek_ye(self): pass @abc.abstractmethod def toplantı_yonet(self): pass @abc.abstractmethod def kod_yaz(self): pass @abc.abstractmethod def rapor_onayla(self): pass class Yazilimci(CalisanArayuzu): def calis(self): print("Yazılımcı çalışıyor...") def yemek_ye(self): print("Yazılımcı yemek yiyor...") def kod_yaz(self): print("Yazılımcı kod yazıyor...") # Kullanmadığı metotları boş implemente etmek zorunda! def toplantı_yonet(self): pass def rapor_onayla(self): pass class GenelMudur(CalisanArayuzu): def calis(self): print("Genel Müdür çalışıyor...") def yemek_ye(self): print("Genel Müdür yemek yiyor...") def toplantı_yonet(self): print("Genel Müdür toplantı yönetiyor...") def rapor_onayla(self): print("Genel Müdür rapor onaylıyor...") # Kullanmadığı metotları boş implemente etmek zorunda! def kod_yaz(self): pass # Yazilimci, toplantı_yonet ve rapor_onayla'ya ihtiyaç duymuyor. # GenelMudur, kod_yaz'a ihtiyaç duymuyor. # Bu, ISP ihlalidir.
Python Örneği (Uyumlu):
ISP UYUMLU YAKLAŞIM - Ayrılmış Arayüzler (ABCs) class ICalisabilir(abc.ABC): @abc.abstractmethod def calis(self): pass class IYemekYiyebilir(abc.ABC): @abc.abstractmethod def yemek_ye(self): pass class IYoneticiGorevleri(abc.ABC): @abc.abstractmethod def toplantı_yonet(self): pass @abc.abstractmethod def rapor_onayla(self): pass class IKodlayici(abc.ABC): @abc.abstractmethod def kod_yaz(self): pass # Sınıflar sadece ilgili arayüzleri (ABC'leri) implemente eder class Yazilimci(ICalisabilir, IYemekYiyebilir, IKodlayici): # Çoklu kalıtım burada mantıklı def calis(self): print("Yazılımcı çalışıyor...") def yemek_ye(self): print("Yazılımcı yemek yiyor...") def kod_yaz(self): print("Yazılımcı kod yazıyor...") # Sadece ihtiyaç duyduğu metotları implemente eder! class GenelMudur(ICalisabilir, IYemekYiyebilir, IYoneticiGorevleri): def calis(self): print("Genel Müdür çalışıyor...") def yemek_ye(self): print("Genel Müdür yemek yiyor...") def toplantı_yonet(self): print("Genel Müdür toplantı yönetiyor...") def rapor_onayla(self): print("Genel Müdür rapor onaylıyor...") # Şimdi sınıflar kullanmadıkları metotlara bağımlı değil. # İstemciler de (bu sınıfları kullanan kodlar) sadece ihtiyaç duydukları # arayüze (örn: ICalisabilir) bağımlı olabilirler. def calisma_rutini(calisan: ICalisabilir): # Sadece çalışabilen birini bekliyor calisan.calis() yaz = Yazilimci() gm = GenelMudur() calisma_rutini(yaz) calisma_rutini(gm)
3.5. D: Dependency Inversion Principle (DIP — Bağımlılığın Tersine Çevrilmesi Prensibi)
Yüksek seviyeli modüller, düşük seviyeli modüllere doğrudan bağımlı olmamalıdır. Her ikisi de soyutlamalara (abstractions) bağımlı olmalıdır.
Soyutlamalar, detaylara bağımlı olmamalıdır. Detaylar, soyutlamalara bağımlı olmalıdır.
Temel Fikir: Kodumuzdaki bağımlılıkların yönünü değiştirmektir. Genellikle, yüksek seviyeli iş mantığını içeren modüller (örn: raporlama servisi), düşük seviyeli implementasyon detaylarını içeren modüllere (örn: belirli bir veritabanı sürücüsü, dosya yazma sınıfı) doğrudan bağımlı olma eğilimindedir. DIP, bu doğrudan bağımlılığı kırar ve her iki seviyenin de arada tanımlanmış bir soyutlamaya (abstraction) — genellikle bir arayüz veya soyut sınıf — bağımlı olmasını söyler. Düşük seviyeli modül bu soyutlamayı implemente eder, yüksek seviyeli modül ise bu soyutlama üzerinden çalışır.
Bağımlılık Enjeksiyonu (Dependency Injection): DIP’i uygulamanın yaygın bir yolu Bağımlılık Enjeksiyonu’dur. Yüksek seviyeli sınıf, ihtiyaç duyduğu düşük seviyeli nesneyi (soyutlamanın somut implementasyonunu) kendisi oluşturmak yerine, dışarıdan (genellikle constructor veya bir metot aracılığıyla) alır.
Neden Önemli?
Gevşek Bağlantı (Loose Coupling): Yüksek ve düşük seviyeli modüller arasındaki doğrudan bağımlılığı ortadan kaldırır.
Esneklik ve Değiştirilebilirlik: Düşük seviyeli implementasyon detaylarını (örn: veritabanı türünü) değiştirmek, yüksek seviyeli modülü etkilemez (soyutlama aynı kaldığı sürece). Farklı implementasyonlar kolayca değiştirilebilir.
Test Edilebilirlik: Yüksek seviyeli modülleri test ederken, gerçek düşük seviyeli bağımlılıklar yerine sahte (mock) implementasyonları enjekte etmek çok kolaylaşır.
Daha Modüler ve Yeniden Kullanılabilir Tasarım: Bileşenler daha bağımsız hale gelir.
Python Örneği (İhlal):
DIP İHLALİ class VeriLoglayici: # Düşük seviyeli modül (somut implementasyon) def logla(self, mesaj): print(f"[VeriLog] - {mesaj}") class Uygulama: # Yüksek seviyeli modül def init(self): # Yüksek seviyeli modül, düşük seviyeli somut sınıfa DOĞRUDAN bağımlı! self.loglayici = VeriLoglayici() def islem_yap(self, veri): print(f"İşlem yapılıyor: {veri}") # Doğrudan somut sınıfa bağımlı self.loglayici.logla(f"İşlem tamamlandı: {veri}") app = Uygulama() app.islem_yap("Test Verisi") # Eğer VeriLoglayici yerine başka bir loglama (örn: DosyaLoglayici) kullanmak # istersek, Uygulama sınıfını DEĞİŞTİRMEK gerekir.
Python Örneği (Uyumlu — ABC ve Dependency Injection ile):
import abc # DIP UYUMLU YAKLAŞIM # 1. Soyutlamayı Tanımla (Arayüz/ABC) class ILoglayici(abc.ABC): @abc.abstractmethod def logla(self, mesaj: str): pass # 2. Detaylar (Düşük Seviye Modüller) Soyutlamaya Bağımlı Olsun class KonsolLoglayici(ILoglayici): # Soyutlamayı implemente et def logla(self, mesaj: str): print(f"[KONSOL] {mesaj}") class DosyaLoglayici(ILoglayici): # Soyutlamayı implemente et def init(self, dosya_adi): self.dosya_adi = dosya_adi def logla(self, mesaj: str): with open(self.dosya_adi, 'a', encoding='utf-8') as f: f.write(f"[DOSYA] {mesaj}\n") # print(f"[DOSYA] '{self.dosya_adi}' dosyasına loglandı.") # İsteğe bağlı konsol çıktısı # 3. Yüksek Seviyeli Modül Soyutlamaya Bağımlı Olsun class UygulamaDI: # Bağımlılığı dışarıdan al (Dependency Injection - Constructor Injection) def init(self, loglayici: ILoglayici): # Somut sınıfı değil, soyutlamayı bekliyor if not isinstance(loglayici, ILoglayici): raise TypeError("loglayici, ILoglayici arayüzüne uymalıdır.") self.loglayici = loglayici # Gelen loglayıcıyı sakla def islem_yap(self, veri): print(f"İşlem yapılıyor: {veri}") # Soyutlama üzerinden çalış self.loglayici.logla(f"İşlem tamamlandı: {veri}") # Kullanım - Bağımlılıkları dışarıda oluştur ve enjekte et konsol_logger = KonsolLoglayici() dosya_logger = DosyaLoglayici("uygulama.log") print("\nKonsol Loglayıcı ile Uygulama:") app_konsol = UygulamaDI(konsol_logger) app_konsol.islem_yap("Veri A") print("\nDosya Loglayıcı ile Uygulama:") app_dosya = UygulamaDI(dosya_logger) app_dosya.islem_yap("Veri B") # UygulamaDI sınıfı, hangi loglayıcının kullanıldığını bilmek zorunda değil. # Sadece ILoglayici kontratına uyan bir nesne bekliyor. # Loglama mekanizmasını değiştirmek için UygulamaDI sınıfını değiştirmek GEREKMEZ.
Bölüm 6: SOLID Prensipleri Birlikte Nasıl Çalışır?
SOLID prensipleri izole kavramlar değildir; birbirlerini tamamlarlar ve genellikle birlikte uygulandıklarında en büyük faydayı sağlarlar:
SRP, sınıfları daha küçük ve odaklanmış hale getirerek diğer prensiplerin uygulanmasını kolaylaştırır.
OCP genellikle DIP ve soyutlamalar (ABCs/Arayüzler) kullanılarak elde edilir. Yeni davranışlar eklemek için mevcut kodu değiştirmek yerine soyutlamalardan türeyen yeni sınıflar oluşturulur.
LSP, kalıtımın doğru kullanılmasını sağlayarak OCP’yi destekler. Eğer alt sınıflar üst sınıfların yerine güvenle geçemiyorsa, OCP ile yapılan genişletmeler beklenmedik sorunlara yol açabilir.
ISP, istemcilerin gereksiz bağımlılıklardan kurtulmasını sağlayarak DIP’in etkinliğini artırır. İstemciler sadece ihtiyaç duydukları küçük arayüzlere bağımlı olurlar.
DIP, sistemin genel yapısını gevşek bağlı hale getirerek OCP, LSP ve test edilebilirliği doğrudan destekler.
Bu prensipleri bir arada düşünmek, daha bütünsel ve sağlam tasarımlar yapmaya yardımcı olur.
Bölüm 7: Sonuç: Daha İyi Yazılım Tasarımına Giden Yol
SOLID, Nesne Yönelimli Programlama ile daha anlaşılır, esnek, sürdürülebilir ve test edilebilir yazılımlar oluşturmak için beş temel tasarım prensibini bir araya getiren güçlü bir kılavuzdur. Bu prensipler, Robert C. Martin tarafından popülerleştirilmiş ve modern yazılım geliştirmenin temel taşlarından biri haline gelmiştir.
Her bir prensip — Tek Sorumluluk, Açık/Kapalı, Liskov Yerine Geçme, Arayüz Ayırma ve Bağımlılığın Tersine Çevrilmesi — kodun farklı yönlerini iyileştirmeyi hedefler. Birlikte uygulandıklarında, değişime daha dirençli, bakımı daha kolay ve anlaşılması daha basit sistemler ortaya çıkarırlar.
Python gibi dinamik dillerde bile, SOLID prensiplerinin altında yatan fikirler geçerlidir. Python’un kendine özgü mekanizmaları (konvansiyonlar, ABC’ler, protokoller, duck typing) bu prensipleri uygulamak için araçlar sunar.
SOLID prensiplerini öğrenmek ve uygulamak, bir geliştiricinin daha iyi tasarım kararları almasına, “kod kokularını” tanımasına ve uzun vadede daha başarılı ve kaliteli yazılımlar üretmesine yardımcı olur. Bunlar katı kurallar olmasa da, daha profesyonel ve etkili bir yazılım mühendisi olma yolunda değerli yol göstericilerdir.
Abdulkadir Güngör - Kişisel WebSite
Abdulkadir Güngör - Kişisel WebSite
Abdulkadir Güngör - Özgeçmiş
Github
Github
Linkedin