Nesne Tabanlı Programlama 2

9 - Yazılım Tasarım İlkeleri: SOLID Prensipleri

Emre Can Yılmaz

Ondokuz Mayıs Üniversitesi

2026

SOLID Prensipleri: Neden Önemli?

  • Bazı programlarda küçük bir değişiklik bile birçok yeri etkiler.
  • Bazı programlarda ise yeni özellik eklemek ve bakım yapmak daha kolaydır.
  • Bu farkın önemli bir kısmı, yazılımın nasıl tasarlandığıyla ilgilidir.

SOLID, nesne yönelimli tasarımda kodu daha:

  • anlaşılır,
  • esnek,
  • test edilebilir,
  • bakımı kolay

hale getirmeyi amaçlayan 5 temel prensibin kısaltmasıdır.

  • Single Responsibility Principle
  • Open/Closed Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle

S: Tek Sorumluluk Prensibi (SRP)

  • İlke: Bir sınıf yalnızca tek bir işi yapacak şekilde tasarlanmalıdır.
  • Bir sınıf hem doğrulama, hem kayıt, hem bildirim, hem hesaplama yapıyorsa fazla sorumluluk yüklenmiştir.
  • Bu durumda:
    • anlamak zorlaşır,
    • test etmek zorlaşır,
    • bir değişiklik başka işleri de etkileyebilir.

Amaç, sınıfları küçük ve odaklı tutmaktır.

SRP İhlali: Çok İş Yapan Sınıf

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.

  • kullanıcı bilgisini doğruluyor
  • veritabanına kayıt yapıyor
  • e-posta gönderiyor

Yani birden fazla sorumluluğu aynı yerde topluyor.

SRP Uygulaması: Sorumlulukları Ayırma

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.

O: Açık/Kapalı Prensip (OCP)

  • İlke: Yazılım bileşenleri genişlemeye açık, değişime kapalı olmalıdır.
  • Yeni bir davranış eklemek istediğimizde, mevcut çalışan kodu değiştirmek yerine sisteme yeni kod ekleyebilmeliyiz.
  • Böylece:
    • var olan kodu bozma riski azalır,
    • sistem daha kararlı kalır,
    • yeni gereksinimlere uyum daha kolay olur.

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.

OCP İhlali: Yeni Durum İçin Mevcut Kodu Değiştirme

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:

  • Yeni bir ödeme tipi geldiğinde bu sınıfın içini değiştirmek gerekir.
  • Her yeni if/elif bloğu sınıfı büyütür.
  • Mevcut çalışan davranışları bozma riski oluşur.

OCP Uygulaması: Strateji ile Genişletme

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.

L: Liskov Yerine Geçme Prensibi (LSP)

  • İlke: Bir alt sınıf, gerektiğinde üst sınıfın yerine kullanılabilmelidir.
  • Yani üst sınıfı bekleyen bir yere alt sınıf verdiğimizde programın çalışma mantığı bozulmamalıdır.
  • Alt sınıf, üst sınıfın sunduğu davranışla çelişmemelidir.
  • Üst sınıfı kullanan kod, alt sınıf geldi diye ek önlem almak zorunda kalmamalıdır.

Bu prensip özellikle kalıtım kullanırken önemlidir.

LSP İhlali: Üst Türün Beklentisini Bozan Alt Tür

class Kus:
    def __init__(self, ad: str):
        self.ad = ad

    def yemek_ye(self):
        print(f"{self.ad} yemek yiyor.")

    def uc(self):
        print(f"{self.ad} uçuyor.")

class Serce(Kus):
    def uc(self):
        print(f"{self.ad} hızla uçuyor.")

class Penguen(Kus):
    def uc(self):
        raise RuntimeError(f"{self.ad} uçamaz.")

LSP İhlali: Sorun Nerede?

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.
  • Bu durumda kalıtım ilişkisi tasarımsal olarak sorgulanmalıdır.

LSP İçin Daha Uygun Tasarım

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.

  • Her kuş Kus olabilir.
  • Ama sadece gerçekten uçabilen canlılar IUcabilen yapısını uygular.

LSP İçin Daha Uygun Tasarım

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.
  • Ama ondan uc() davranışı beklenmez.
  • Böylece üst tür ile alt tür arasında yanlış bir beklenti kurulmamış olur.

I: Arayüz Ayrımı Prensibi (ISP)

  • İlke: Bir sınıf, ihtiyaç duymadığı metotları içeren bir arayüzü kullanmaya zorlanmamalıdır.
  • Büyük ve her işi toplamaya çalışan arayüzler yerine, daha küçük ve odaklı arayüzler tercih edilmelidir.
  • Aksi durumda:
    • boş metotlar yazılır,
    • desteklenmeyen işler için hata üretilir,
    • gereksiz bağımlılıklar ortaya çıkar.

Amaç, her sınıfın sadece gerçekten kullandığı davranışlara bağlı olmasıdır.

ISP İhlali: Şişman Arayüz

from abc import ABC, abstractmethod

class IMakine(ABC):
    @abstractmethod
    def yazdir(self):
        pass

    @abstractmethod
    def tara(self):
        pass

    @abstractmethod
    def fax_gonder(self):
        pass
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")

ISP İhlalinin Sonucu: Kullanımda Sorun

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.
  • Ama EskiModelYazici, bu arayüzü implemente ettiği hâlde gerçekte tarama yapamaz.
  • Demek ki arayüz fazla geniş tutulmuştur.

ISP Uygulaması: Küçük ve Odaklı Arayüzler

from abc import ABC, abstractmethod

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
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...")

D: Bağımlılık Tersine Çevirme Prensibi (DIP)

  • İlke: Bir sınıf, işini doğrudan belirli bir somut sınıfa bağlayarak yapmamalıdır.
  • Bunun yerine, somut ayrıntıya değil ortak bir soyutlamaya bağlı olmalıdır.
  • Böylece aynı iş mantığı, farklı alt yapılarla da çalışabilir.
  • Bu da:
    • değişiklik yapmayı kolaylaştırır,
    • bağımlılığı azaltır,
    • test yazmayı kolaylaştırır.

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.

DIP İhlali: Somut Sınıfa Sıkı Bağımlılık

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.
  • Veri kaynağı değişirse üst seviye sınıfı da değiştirmek gerekir.

DIP Uygulaması: Soyutlamaya Bağlanma

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ı.

SOLID Özet

  • S — SRP: Bir sınıfın tek bir temel sorumluluğu olsun.
  • O — OCP: Yeni davranış eklemek için mevcut kodu mümkün olduğunca değiştirmeyelim.
  • L — LSP: Alt sınıf, üst sınıfın yerine güvenle geçebilsin.
  • I — ISP: Sınıfları kullanmadıkları metotlara zorlamayalım.
  • D — DIP: Somut ayrıntılara değil, soyutlamalara bağımlı olalım.

Bu ilkeler mutlak kurallar değildir.
Bunlar, daha sürdürülebilir tasarım kararları vermek için kullanılan yol gösterici prensiplerdir.

SOLID Her Durumda Aynı Şekilde Uygulanır mı?

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:

  • Gereksiz soyutlama kodu büyütebilir.
  • Çok erken genelleme yapmak bakım yükünü artırabilir.
  • Henüz ihtiyaç olmayan yapıları kurmak, sistemi olduğundan karmaşık hâle getirebilir.

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.

Alıştırmalar

Aşağıdaki örneklerde önce şu sorulara odaklanın:

  • Birincil ihlal hangi SOLID prensibiyle ilgilidir?
  • Neden?
  • Nasıl daha iyi tasarlanabilir?

Alıştırma 1

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...")

Alıştırma 2

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?

Alıştırma 3

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.

Alıştırma 3 - Kod

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)

Alıştırma 3 - Test ve Tartışma

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?
  • Burada bir alt sınıf, üst sınıfın yerine sorunsuz geçebiliyor mu?
  • Daha uygun bir tasarım için ne değiştirilebilir?

Alıştırma 4

Aşağıdaki USBKlavye sınıfında birincil olarak hangi SOLID prensibi ihlal edilmektedir?

from abc import ABC, abstractmethod

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):
    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")

Alıştırma 5

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?

Sorular & Tartışma

  • SOLID prensipleriyle ilgili takıldığınız bir nokta var mı?
  • Hangi prensip size daha doğal, hangisi daha zor göründü?
  • Gerçek projelerde bu ilkeleri uygulamak her zaman kolay mıdır?
  • Bir prensibi uygularken bazen başka bir karmaşıklık oluşabilir mi?

Gelecek hafta: Tasarım desenleri ve bu prensiplerin pratikte daha somut kullanımları