9 - Yazılım Tasarım İlkeleri: SOLID Prensipleri
2025
SOLID, 5 önemli nesne yönelimli tasarım prensibinin kısaltmasıdır.
Bu prensipler, kodumuzun daha anlaşılır, yönetilebilir ve değişime adapte olabilir olmasını hedefler.
Amacı, kötü tasarımdan kaynaklanan sorunları (katılık, kırılganlık, yerinden oynatılamama, gereksiz karmaşıklık) en aza indirmektir.
Single Responsibility Principle (Tek Sorumluluk Prensibi)
Open/Closed Principle (Açık/Kapalı Prensip)
Liskov Substitution Principle (Liskov Yerine Geçme Prensibi)
Interface Segregation Principle (Arayüz Ayrımı Prensibi)
Dependency Inversion Principle (Bağımlılık Tersine Çevirme Prensibi)
# KÖTÜ TASARIM: Bu sınıf çok fazla şey yapıyor!
class KullaniciIslemleri:
def __init__(self, kullanici_adi: str, email: str):
self.kullanici_adi = kullanici_adi
self.email = email
def bilgileri_dogrula(self) -> bool:
print(f"'{self.kullanici_adi}' bilgileri doğrulanıyor...")
# Karmaşık doğrulama kuralları...
if "@" not in self.email: return False
return True
def veritabanina_kaydet(self):
if self.bilgileri_dogrula():
print(f"'{self.kullanici_adi}' veritabanına kaydediliyor...")
# Veritabanı kodları...
else:
print("Doğrulama başarısız, kaydedilemedi.")
def hosgeldin_emaili_gonder(self):
print(f"'{self.email}' adresine hoşgeldin e-postası gönderiliyor...")
# E-posta gönderme kodları...
Bu sınıfın değişmesi için birden fazla neden var! SRP ihlali.
# İYİ TASARIM: Her sınıfın tek bir sorumluluğu var.
class Kullanici: # Sadece kullanıcı verisini tutar
def __init__(self, kullanici_adi: str, email: str):
self.kullanici_adi = kullanici_adi
self.email = email
class KullaniciDogrulayici: # Sadece doğrular
def dogrula(self, kullanici: Kullanici) -> bool:
print(f"'{kullanici.kullanici_adi}' bilgileri doğrulanıyor...")
if "@" not in kullanici.email: return False
return True
class KullaniciDB: # Sadece kaydeder
def kaydet(self, kullanici: Kullanici):
print(f"'{kullanici.kullanici_adi}' veritabanına kaydediliyor...")
# Veritabanı kodları...
class EmailGonderici: # Sadece email gönderir
def gonder_hosgeldin(self, email: str):
print(f"'{email}' adresine hoşgeldin e-postası gönderiliyor...")
# E-posta gönderme kodları...
# Kullanım (Daha organize):
k = Kullanici("ayse", "[email protected]")
dogrulayici = KullaniciDogrulayici()
db = KullaniciDB()
emailer = EmailGonderici()
if dogrulayici.dogrula(k):
db.kaydet(k)
emailer.gonder_hosgeldin(k.email)
else:
print("Kullanıcı bilgileri geçersiz.")
Artık her parçayı ayrı ayrı test etmek ve değiştirmek daha kolay.
# KÖTÜ TASARIM: Yeni ödeme tipi eklemek için sınıfı değiştirmek gerekiyor.
class OdemeIslemcisi:
def islem_yap(self, miktar: float, odeme_tipi: str):
if odeme_tipi == "kredi_karti":
print(f"{miktar} TL kredi kartı ile işleniyor...")
# Kredi kartı API çağrıları...
elif odeme_tipi == "paypal":
print(f"{miktar} TL PayPal ile işleniyor...")
# PayPal API çağrıları...
# YENİ BİR ÖDEME TİPİ (örn: havale) EKLENDİĞİNDE
# BU if/elif BLOKLARINI DEĞİŞTİRMEK ZORUNDAYIZ! OCP İhlali.
else:
print("Geçersiz ödeme tipi!")
# Kullanım
islemci = OdemeIslemcisi()
islemci.islem_yap(100, "kredi_karti")
islemci.islem_yap(50, "paypal")
from abc import ABC, abstractmethod # Soyutlama için
# 1. Soyut Strateji (Arayüz) Tanımla
class IOdemeStratejisi(ABC):
@abstractmethod
def ode(self, miktar: float): pass
# 2. Somut Stratejiler Oluştur
class KrediKartiOdeme(IOdemeStratejisi):
def ode(self, miktar: float):
print(f"{miktar} TL kredi kartı ile işleniyor...")
# Kredi kartı API çağrıları...
class PayPalOdeme(IOdemeStratejisi):
def ode(self, miktar: float):
print(f"{miktar} TL PayPal ile işleniyor...")
# PayPal API çağrıları...
# YENİ ÖDEME TİPİ EKLEMEK ÇOK KOLAY! Sadece yeni sınıf ekle:
class HavaleOdeme(IOdemeStratejisi):
def ode(self, miktar: float):
print(f"{miktar} TL Havale/EFT ile işleniyor...")
# Havale işlemleri...
# 3. İşlemci Sınıfı Artık Değişime Kapalı
class OdemeIslemcisi:
def islem_yap(self, miktar: float, strateji: IOdemeStratejisi): # Stratejiyi dışarıdan al
print("Ödeme işlemi başlatılıyor...")
strateji.ode(miktar) # İşi stratejiye devret
# Kullanım
kk_strateji = KrediKartiOdeme()
pp_strateji = PayPalOdeme()
hv_strateji = HavaleOdeme() # Yeni eklenen strateji
islemci = OdemeIslemcisi()
islemci.islem_yap(100, kk_strateji)
islemci.islem_yap(50, pp_strateji)
islemci.islem_yap(200, hv_strateji) # Yeni ödeme tipi sorunsuz çalışıyor!
# Yeni ödeme tipi için OdemeIslemcisi sınıfını HİÇ DEĞİŞTİRMEDİK!
# Üst Sınıf
class Kus:
def __init__(self, ad): self.ad = ad
def yemek_ye(self): print(f"{self.ad} yemek yiyor.")
def uc(self): print(f"{self.ad} kanat çırpıyor...") # Temel beklenti
# Alt Sınıf 1 (LSP Uyumlu ✅)
class Serce(Kus):
def uc(self): print(f"{self.ad} hızla uçuyor!") # Beklentiyi karşılıyor
# Alt Sınıf 2 (LSP İhlali ❌)
class Penguen(Kus):
def uc(self):
# Penguen uçamaz! Kus'un uc() metodu beklentisini ihlal ediyor.
raise TypeError(f"{self.ad} bir penguen, uçamaz!")
# Kus bekleyen fonksiyon
def havada_gezdir(kus: Kus):
print(f"--- {kus.ad} ile gezi ---")
try:
kus.uc() # Bu metodun güvenle çağrılabilmesi beklenir
print(f"{kus.ad} başarıyla gezdi.")
except TypeError as e:
print(f"!!! HATA: {e}") # Penguen verildiğinde burası çalışır!
print("--- Gezi bitti ---")
# Test
serce = Serce("Cik Cik")
penguen = Penguen("Gwin")
havada_gezdir(serce) # Sorunsuz çalışır.
havada_gezdir(penguen) # Hata verir! Penguen, Kus'un yerine güvenle geçemedi.
# Penguen'in uc() metodu, Kus'un temel beklentisini (hata vermeden uçmayı deneme)
# karşılamadığı için LSP ihlal edilmiştir.
Bu ihlal, Penguen’in aslında ‘uc()’ yeteneği olan bir ‘Kus’ alt türü olup olmadığını sorgulamamıza neden olur. Belki de kalıtım yerine başka bir yapı daha uygundur.
pass
veya NotImplementedError
fırlatma). Bu, kodun anlaşılırlığını azaltır ve gereksiz yere karmaşıklaştırır. Bir arayüzdeki değişiklik, onu kullanan ilgisiz sınıfları bile etkileyebilir.from abc import ABC, abstractmethod
# KÖTÜ TASARIM: Çok fazla metot içeren "şişman" arayüz
class IMakine(ABC):
@abstractmethod
def yazdir(self): pass
@abstractmethod
def tara(self): pass
@abstractmethod
def fax_gonder(self): pass
class CokFonskiyonluYazici(IMakine):
def yazdir(self): print("Yazdırılıyor...")
def tara(self): print("Taranıyor...")
def fax_gonder(self): print("Fax gönderiliyor...")
class EskiModelYazici(IMakine): # Sadece yazdırma yapabiliyor!
def yazdir(self): print("Eski model yazdırıyor...")
# Bu metotları implemente etmek ZORUNDA ama kullanamıyor!
def tara(self):
# print("Bu yazıcı tarama yapamaz!") # Yorum satırı veya...
raise NotImplementedError("Tarama desteklenmiyor") # Hata fırlat veya...
def fax_gonder(self):
pass # Boş bırak! 🤷♂️
# EskiModelYazici, kullanmadığı işlevler için bile metot tanımlamak zorunda kaldı.
from abc import ABC, abstractmethod
# İYİ TASARIM: Küçük ve odaklı arayüzler
class IYazdirici(ABC):
@abstractmethod
def yazdir(self): pass
class ITarayici(ABC):
@abstractmethod
def tara(self): pass
class IFax(ABC):
@abstractmethod
def fax_gonder(self): pass
# Sınıflar sadece ilgili arayüzleri implemente eder
class CokFonskiyonluYazici(IYazdirici, ITarayici, IFax): # Hepsini yapabilir
def yazdir(self): print("Yazdırılıyor...")
def tara(self): print("Taranıyor...")
def fax_gonder(self): print("Fax gönderiliyor...")
class EskiModelYazici(IYazdirici): # Sadece yazdırma yapabilir
def yazdir(self): print("Eski model yazdırıyor...")
# tarama() veya fax_gonder() implemente etmek zorunda DEĞİL! ✅
class SadeceTarayici(ITarayici): # Sadece tarama yapabilir
def tara(self): print("Belge taranıyor...")
# Kullanım:
cok_fonk = CokFonskiyonluYazici()
eski_yazici = EskiModelYazici()
tarayici = SadeceTarayici()
cok_fonk.yazdir()
cok_fonk.tara()
eski_yazici.yazdir()
# eski_yazici.tara() # Hata verir, doğru davranış!
tarayici.tara()
# tarayici.yazdir() # Hata verir, doğru davranış!
# Sınıflar sadece bildikleri işleri yapmakla sorumlu. Daha temiz ve esnek.
# KÖTÜ TASARIM: Raporlayici, belirli bir VeriOkuyucu'ya sıkı sıkıya bağlı.
class DosyadanVeriOkuyucu: # Düşük seviye - Somut implementasyon
def veri_oku(self, dosya_yolu: str) -> str:
print(f"'{dosya_yolu}' dosyasından veri okunuyor...")
# Dosya okuma işlemleri...
return "Dosyadan okunan veri"
class RaporUretici: # Yüksek seviye - İş mantığı
def __init__(self):
# Sıkı Bağımlılık! Direkt somut sınıfın nesnesini oluşturuyor.
self._veri_okuyucu = DosyadanVeriOkuyucu()
def rapor_olustur(self, kaynak_dosya: str):
print("Rapor oluşturuluyor...")
veri = self._veri_okuyucu.veri_oku(kaynak_dosya)
print(f"Rapor içeriği:\n{veri}")
# Kullanım
raporlayici = RaporUretici()
raporlayici.rapor_olustur("veriler.txt")
# Eğer veriyi dosyadan değil de veritabanından okumak istersek?
# RaporUretici sınıfını DEĞİŞTİRMEMİZ GEREKİR! ❌ DIP ihlali.
from abc import ABC, abstractmethod
# İYİ TASARIM: Soyutlama ve Bağımlılık Enjeksiyonu
# 1. Soyutlama (Arayüz) Tanımla
class IVeriOkuyucu(ABC):
@abstractmethod
def veri_oku(self, kaynak: str) -> str: pass
# 2. Düşük Seviye Modüller Soyutlamayı Implemente Etsin
class DosyadanVeriOkuyucu(IVeriOkuyucu):
def veri_oku(self, kaynak: str) -> str: # kaynak = dosya yolu
print(f"'{kaynak}' dosyasından veri okunuyor...")
return "Dosyadan okunan veri"
class VeritabanindanVeriOkuyucu(IVeriOkuyucu):
def veri_oku(self, kaynak: str) -> str: # kaynak = tablo adı
print(f"'{kaynak}' veritabanı tablosundan veri okunuyor...")
return "Veritabanından okunan veri"
# 3. Yüksek Seviye Modül Soyutlamaya Bağlansın (Dependency Injection)
class RaporUretici:
# Bağımlılığı (IVeriOkuyucu tipinde) dışarıdan alıyoruz
def __init__(self, veri_okuyucu: IVeriOkuyucu):
self._veri_okuyucu = veri_okuyucu # Artık arayüze bağımlı!
def rapor_olustur(self, veri_kaynagi: str):
print("Rapor oluşturuluyor...")
veri = self._veri_okuyucu.veri_oku(veri_kaynagi) # Hangi okuyucu gelirse onun metodu çalışır
print(f"Rapor içeriği:\n{veri}")
# Kullanım - Esneklik!
dosya_okuyucu = DosyadanVeriOkuyucu()
vt_okuyucu = VeritabanindanVeriOkuyucu()
# Hangi okuyucuyu istersek RaporUretici'ye verebiliriz!
raporlayici_dosya = RaporUretici(dosya_okuyucu)
raporlayici_vt = RaporUretici(vt_okuyucu)
print("Dosya Kaynaklı Rapor:")
raporlayici_dosya.rapor_olustur("veriler.txt")
print("\nVeritabanı Kaynaklı Rapor:")
raporlayici_vt.rapor_olustur("kullanicilar_tablosu")
# Yeni bir veri okuyucu (örn: ApiVeriOkuyucu) eklemek RaporUretici'yi etkilemez! ✅
Unutmayın: Bu ilkeler katı kurallar değil, yol gösterici prensiplerdir. Amaç, daha iyi, daha sürdürülebilir ve üzerinde çalışması daha keyifli yazılımlar oluşturmaktır.
Örnekteki sınıfta hangi SOLID prensibi (veya prensipleri) ihlal ediliyor olabilir? Neden?
Aşağıdaki DosyaYoneticisi
sınıfını inceleyin.
import json
import os
class DosyaYoneticisi:
def __init__(self, dosya_adi: str):
self.dosya_adi = dosya_adi
def veri_oku_json(self) -> dict:
print(f"'{self.dosya_adi}' JSON olarak okunuyor...")
# ... dosya okuma ve JSON parse etme ...
return {"veri": "okunan json data"}
def veri_yaz_json(self, veri: dict):
print(f"'{self.dosya_adi}' JSON olarak yazılıyor...")
# ... veriyi JSON formatına çevirme ve dosyaya yazma ...
pass
def dosyayi_ziple(self):
print(f"'{self.dosya_adi}' ZIP olarak sıkıştırılıyor...")
# ... dosyayı ZIP algoritması ile sıkıştırma ...
pass
def dosyayi_eposta_gonder(self, alici_email: str):
print(f"'{self.dosya_adi}' '{alici_email}' adresine gönderiliyor...")
# ... e-posta sunucusuna bağlanma ve gönderme ...
pass
Soru: Bu fonksiyonda hangi SOLID prensibi ihlal ediliyor? Neden?
Aşağıdaki FaturaHesaplayici
fonksiyonunu inceleyin.
def fatura_hesapla(tutar: float, musteri_tipi: str) -> float:
indirim_orani = 0.0
print(f"Müşteri Tipi: {musteri_tipi}, Tutar: {tutar}")
if musteri_tipi == "STANDART":
indirim_orani = 0.05
elif musteri_tipi == "PREMIUM":
indirim_orani = 0.10
elif musteri_tipi == "VIP":
indirim_orani = 0.20
# YENİ müşteri tipi (örn: "GOLD") eklenirse ne olur?
indirimli_tutar = tutar * (1 - indirim_orani)
print(f"İndirimli Tutar: {indirimli_tutar}")
return indirimli_tutar
# Kullanım
fatura_hesapla(100.0, "PREMIUM")
fatura_hesapla(100.0, "STANDART")
Yeni bir müşteri tipi eklemek istediğimizde ne yapmamız gerekir?
Soru: EmailBildirimi(““) durumu hangi SOLID prensibini ihlal ediyor? Neden?
Aşağıdaki Bildirim
sistemi örneğini inceleyin.
class Bildirim:
def gonder(self, mesaj: str):
print(f"Bildirim gönderiliyor: {mesaj}")
class EmailBildirimi(Bildirim):
def __init__(self, email_adresi: str):
self.email_adresi = email_adresi
def gonder(self, mesaj: str):
if not self.email_adresi: # E-posta adresi yoksa gönderemez!
raise ValueError("E-posta adresi belirtilmemiş!")
print(f"'{self.email_adresi}' adresine E-POSTA gönderildi: {mesaj}")
# Bildirim bekleyen bir fonksiyon
def toplu_bildirim_gonder(bildirim_listesi: list[Bildirim], ortak_mesaj: str):
print("\nToplu Bildirim Başlıyor...")
basarili_say = 0
for bildirim_nesnesi in bildirim_listesi:
try:
# Beklenti: Her bildirim nesnesinin gonder() metodu
# sorunsuz çağrılabilmeli.
bildirim_nesnesi.gonder(ortak_mesaj)
basarili_say += 1
except ValueError as e:
print(f"!! Hata: Bir bildirim gönderilemedi - {e}")
print(f"Toplu Bildirim Bitti. ({basarili_say} adet başarılı)")
# Test
bildirim1 = Bildirim()
email_bildirim_dolu = EmailBildirimi("[email protected]")
email_bildirim_bos = EmailBildirimi("") # Geçersiz durum!
bildirimler = [bildirim1, email_bildirim_dolu, email_bildirim_bos]
toplu_bildirim_gonder(bildirimler, "Kampanya başladı!")
toplu_bildirim_gonder fonksiyonu ne bekliyordu, ne oldu?
Soru: USBKlavye sınıfı hangi SOLID prensibini ihlal ediyor? Neden?
Bu durum ne gibi sorunlara yol açabilir?
Aşağıdaki IArayuzCihazi
arayüzünü ve onu implemente eden sınıfları inceleyin.
from abc import ABC, abstractmethod
# "Şişman" Arayüz
class IArayuzCihazi(ABC):
@abstractmethod
def dokunmatik_algila(self, x: int, y: int): pass
@abstractmethod
def klavye_girisi_al(self, karakter: str): pass
@abstractmethod
def sesli_komut_al(self, komut: str): pass
class AkilliTelefon(IArayuzCihazi):
def dokunmatik_algila(self, x: int, y: int): print("Dokunma algılandı.")
def klavye_girisi_al(self, karakter: str): print("Sanal klavye girişi alındı.")
def sesli_komut_al(self, komut: str): print("Sesli komut algılandı.")
class USBKlavye(IArayuzCihazi): # Sadece klavye girişi var!
def dokunmatik_algila(self, x: int, y: int):
# Bu klavyenin dokunmatik özelliği yok!
pass # Ne yapmalı?
def klavye_girisi_al(self, karakter: str):
print(f"Fiziksel klavye girişi: {karakter}")
def sesli_komut_al(self, komut: str):
# Bu klavyenin sesli komut özelliği yok!
raise NotImplementedError("Sesli komut desteklenmiyor")
Soru: VeriIsleyici sınıfı hangi SOLID prensibini ihlal ediyor? Neden?
Aşağıdaki VeriIsleyici
sınıfını inceleyin.
class SqlVeritabani: # Düşük seviye - Somut sınıf
def baglan(self): print("SQL Veritabanına bağlanıldı.")
def sorgu_calistir(self, sorgu: str) -> list:
print(f"SQL Sorgusu çalıştırılıyor: {sorgu}")
return [{"id": 1}, {"id": 2}] # Örnek veri
class VeriIsleyici: # Yüksek seviye - İş mantığı
def __init__(self):
# Sıkı Bağımlılık! Doğrudan somut sınıfı kullanıyor.
self.veritabani = SqlVeritabani()
self.veritabani.baglan()
def kullanicilari_getir(self) -> list:
print("Kullanıcılar getiriliyor...")
sorgu = "SELECT * FROM kullanicilar"
sonuclar = self.veritabani.sorgu_calistir(sorgu)
return sonuclar
# Kullanım
isleyici = VeriIsleyici()
kullanicilar = isleyici.kullanicilari_getir()
print(f"Getirilen kullanıcılar: {kullanicilar}")
Eğer SqlVeritabani yerine MongoDB kullanmak istersek ne olur?