9 - Yazılım Tasarım İlkeleri: SOLID Prensipleri
2026
SOLID, nesne yönelimli tasarımda kodu daha:
hale getirmeyi amaçlayan 5 temel prensibin kısaltmasıdır.
Amaç, sınıfları küçük ve odaklı tutmaktır.
class KullaniciIslemleri:
def __init__(self, kullanici_adi: str, email: str):
self.kullanici_adi = kullanici_adi
self.email = email
def bilgileri_dogrula(self) -> bool:
if "@" not in self.email:
return False
return True
def veritabanina_kaydet(self):
if self.bilgileri_dogrula():
print(f"{self.kullanici_adi} veritabanına kaydedildi.")
else:
print("Doğrulama başarısız.")
def hosgeldin_emaili_gonder(self):
print(f"{self.email} adresine hoş geldin e-postası gönderildi.")Bu sınıf tek bir işle ilgilenmiyor.
Yani birden fazla sorumluluğu aynı yerde topluyor.
class Kullanici:
def __init__(self, kullanici_adi: str, email: str):
self.kullanici_adi = kullanici_adi
self.email = email
class KullaniciDogrulayici:
def dogrula(self, kullanici: Kullanici) -> bool:
return "@" in kullanici.email
class KullaniciDeposu:
def kaydet(self, kullanici: Kullanici):
print(f"{kullanici.kullanici_adi} veritabanına kaydedildi.")
class EmailGonderici:
def hosgeldin_gonder(self, email: str):
print(f"{email} adresine hoş geldin e-postası gönderildi.")kullanici = Kullanici("ayse", "[email protected]")
dogrulayici = KullaniciDogrulayici()
depo = KullaniciDeposu()
emailer = EmailGonderici()
if dogrulayici.dogrula(kullanici):
depo.kaydet(kullanici)
emailer.hosgeldin_gonder(kullanici.email)
else:
print("Kullanıcı bilgileri geçersiz.")Her parça daha odaklı, daha kolay test edilebilir ve daha kolay değiştirilebilir.
Bu prensip çoğu zaman soyutlama ve polimorfizm ile uygulanır.
Not: Buradaki yaklaşım, tasarım desenleri içinde sık görülen Strategy Pattern ile yakından ilişkilidir. Ayrıntısını ileride ayrıca göreceğiz.
class OdemeIslemcisi:
def islem_yap(self, miktar: float, odeme_tipi: str):
if odeme_tipi == "kredi_karti":
print(f"{miktar} TL kredi kartı ile ödendi.")
elif odeme_tipi == "paypal":
print(f"{miktar} TL PayPal ile ödendi.")
else:
print("Geçersiz ödeme tipi.")Sorun:
if/elif bloğu sınıfı büyütür.from abc import ABC, abstractmethod
class IOdemeStratejisi(ABC):
@abstractmethod
def ode(self, miktar: float):
pass
class KrediKartiOdeme(IOdemeStratejisi):
def ode(self, miktar: float):
print(f"{miktar} TL kredi kartı ile ödendi.")
class PayPalOdeme(IOdemeStratejisi):
def ode(self, miktar: float):
print(f"{miktar} TL PayPal ile ödendi.")
class HavaleOdeme(IOdemeStratejisi):
def ode(self, miktar: float):
print(f"{miktar} TL havale ile ödendi.")
class OdemeIslemcisi:
def islem_yap(self, miktar: float, strateji: IOdemeStratejisi):
strateji.ode(miktar)islemci = OdemeIslemcisi()
islemci.islem_yap(100, KrediKartiOdeme())
islemci.islem_yap(50, PayPalOdeme())
islemci.islem_yap(200, HavaleOdeme())Yeni ödeme türü eklemek için OdemeIslemcisi sınıfını değiştirmedik.
Sadece yeni bir strateji sınıfı ekledik.
Bu prensip özellikle kalıtım kullanırken önemlidir.
def ucus_testi(kus: Kus):
kus.uc()
serce = Serce("Cik Cik")
penguen = Penguen("Gwin")
ucus_testi(serce)
ucus_testi(penguen)Sorun şudur:
ucus_testi, verilen her Kus nesnesinin uc() metodunu sorunsuz çağırabileceğini varsayıyor.Penguen, Kus türünden geliyor gibi görünse de bu beklentiyi bozar.from abc import ABC, abstractmethod
class Kus:
def __init__(self, ad: str):
self.ad = ad
def yemek_ye(self):
print(f"{self.ad} yemek yiyor.")
class IUcabilen(ABC):
@abstractmethod
def uc(self):
pass
class Serce(Kus, IUcabilen):
def uc(self):
print(f"{self.ad} hızla uçuyor.")Burada Kus ile ucabilme aynı şey olarak düşünülmüyor.
Kus olabilir.IUcabilen yapısını uygular.class Penguen(Kus):
def yuz(self):
print(f"{self.ad} yüzüyor.")
def ucus_goster(canli: IUcabilen):
canli.uc()
serce = Serce("Cik Cik")
ucus_goster(serce)
penguen = Penguen("Gwin")
# ucus_goster(penguen)Bu tasarımda:
Penguen, hâlâ bir Kustur.uc() davranışı beklenmez.Amaç, her sınıfın sadece gerçekten kullandığı davranışlara bağlı olmasıdır.
class CokFonksiyonluYazici(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):
def yazdir(self):
print("Eski model yazdırıyor...")
def tara(self):
raise NotImplementedError("Tarama desteklenmiyor")
def fax_gonder(self):
raise NotImplementedError("Fax desteklenmiyor")def tarama_baslat(cihaz: IMakine):
cihaz.tara()
cok_fonk = CokFonksiyonluYazici()
eski = EskiModelYazici()
tarama_baslat(cok_fonk)
tarama_baslat(eski)Buradaki problem şudur:
tarama_baslat, verilen her IMakine nesnesinin tara() davranışını desteklediğini varsayar.EskiModelYazici, bu arayüzü implemente ettiği hâlde gerçekte tarama yapamaz.class CokFonksiyonluYazici(IYazdirici, ITarayici, IFax):
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):
def yazdir(self):
print("Eski model yazdırıyor...")
class SadeceTarayici(ITarayici):
def tara(self):
print("Belge taranıyor...")Bu prensip genellikle bağımlılığı dışarıdan verme yaklaşımıyla uygulanır.
Yaygın adıyla buna dependency injection denir.
Bu dersteki örnekte bağımlılığı constructor üzerinden verdiğimiz için buna constructor injection da denir.
class DosyadanVeriOkuyucu:
def veri_oku(self, dosya_yolu: str) -> str:
print(f"{dosya_yolu} dosyasından veri okunuyor...")
return "Dosyadan okunan veri"
class RaporUretici:
def __init__(self):
self.veri_okuyucu = DosyadanVeriOkuyucu()
def rapor_olustur(self, kaynak: str):
veri = self.veri_okuyucu.veri_oku(kaynak)
print("Rapor içeriği:")
print(veri)Sorun:
RaporUretici, belirli bir veri okuyucuya kilitlenmiştir.from abc import ABC, abstractmethod
class IVeriOkuyucu(ABC):
@abstractmethod
def veri_oku(self, kaynak: str) -> str:
pass
class DosyadanVeriOkuyucu(IVeriOkuyucu):
def veri_oku(self, kaynak: str) -> str:
print(f"{kaynak} dosyasından veri okunuyor...")
return "Dosyadan okunan veri"
class VeritabanindanVeriOkuyucu(IVeriOkuyucu):
def veri_oku(self, kaynak: str) -> str:
print(f"{kaynak} tablodan veri okunuyor...")
return "Veritabanından okunan veri"
class RaporUretici:
def __init__(self, veri_okuyucu: IVeriOkuyucu):
self.veri_okuyucu = veri_okuyucu
def rapor_olustur(self, kaynak: str):
veri = self.veri_okuyucu.veri_oku(kaynak)
print("Rapor içeriği:")
print(veri)rapor1 = RaporUretici(DosyadanVeriOkuyucu())
rapor2 = RaporUretici(VeritabanindanVeriOkuyucu())
rapor1.rapor_olustur("veriler.txt")
rapor2.rapor_olustur("kullanicilar")Aynı üst seviye sınıf, farklı veri kaynaklarıyla çalışabildi.
Üst seviye kod somut ayrıntıya değil, soyutlamaya bağlandı.
Bu ilkeler mutlak kurallar değildir.
Bunlar, daha sürdürülebilir tasarım kararları vermek için kullanılan yol gösterici prensiplerdir.
Hayır. Bu ilkeler yararlıdır; ama her küçük problem için yeni arayüzler, yeni soyut sınıflar ve çok katmanlı yapılar kurmak da doğru değildir.
Dikkat edilmesi gerekenler:
Bu nedenle SOLID prensipleri, mekanik kurallar gibi değil, tasarım kararlarını değerlendirmede kullanılan rehberler gibi düşünülmelidir.
Kısacası:
Daha iyi tasarım hedeflenir, daha fazla soyutlama değil.
Aşağıdaki örneklerde önce şu sorulara odaklanın:
Aşağıdaki DosyaYoneticisi sınıfında birincil olarak hangi SOLID prensibi ihlal edilmektedir?
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...")
return {"veri": "okunan json data"}
def veri_yaz_json(self, veri: dict):
print(f"{self.dosya_adi} JSON olarak yazılıyor...")
def dosyayi_ziple(self):
print(f"{self.dosya_adi} ZIP olarak sıkıştırılıyor...")
def dosyayi_eposta_gonder(self, alici_email: str):
print(f"{self.dosya_adi}, {alici_email} adresine gönderiliyor...")Aşağıdaki sınıfta birincil olarak hangi SOLID prensibi ihlal edilmektedir?
class IndirimHesaplayici:
def hesapla(self, tutar: float, musteri_tipi: str) -> float:
indirim_orani = 0.0
if musteri_tipi == "STANDART":
indirim_orani = 0.05
elif musteri_tipi == "PREMIUM":
indirim_orani = 0.10
elif musteri_tipi == "VIP":
indirim_orani = 0.20
return tutar * (1 - indirim_orani)Yeni bir müşteri tipi eklemek istediğimizde ne yapmak zorunda kalırız?
Aşağıdaki örnekte EmailBildirimi("") durumu birincil olarak hangi SOLID prensibiyle ilişkilidir?
Kodun ne beklediğine ve gerçekte ne olduğuna dikkat edin.
class Bildirim:
def gonder(self, mesaj: str):
print(f"Bildirim gönderildi: {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:
raise ValueError("E-posta adresi belirtilmemiş!")
print(f"{self.email_adresi} adresine e-posta gönderildi: {mesaj}")
def toplu_bildirim_gonder(bildirim_listesi: list[Bildirim], ortak_mesaj: str):
for bildirim in bildirim_listesi:
bildirim.gonder(ortak_mesaj)bildirimler = [
Bildirim(),
EmailBildirimi("[email protected]"),
EmailBildirimi("")
]
try:
toplu_bildirim_gonder(bildirimler, "Kampanya başladı!")
except ValueError as hata:
print(f"Hata oluştu: {hata}")Bu örnekte şunları tartışın:
toplu_bildirim_gonder fonksiyonu neyi varsayıyor?EmailBildirimi("") nesnesi bu beklentiyi neden bozuyor?Aşağıdaki USBKlavye sınıfında birincil olarak hangi SOLID prensibi ihlal edilmektedir?
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):
def dokunmatik_algila(self, x: int, y: int):
raise NotImplementedError("Dokunmatik giriş desteklenmiyor")
def klavye_girisi_al(self, karakter: str):
print(f"Fiziksel klavye girişi: {karakter}")
def sesli_komut_al(self, komut: str):
raise NotImplementedError("Sesli komut desteklenmiyor")Aşağıdaki VeriIsleyici sınıfında birincil olarak hangi SOLID prensibi ihlal edilmektedir?
class SqlVeritabani:
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}]
class VeriIsleyici:
def __init__(self):
self.veritabani = SqlVeritabani()
self.veritabani.baglan()
def kullanicilari_getir(self) -> list:
sorgu = "SELECT * FROM kullanicilar"
return self.veritabani.sorgu_calistir(sorgu)SqlVeritabani yerine başka bir veri kaynağı kullanmak istersek ne değişir?
Gelecek hafta: Tasarım desenleri ve bu prensiplerin pratikte daha somut kullanımları