Nesne Tabanlı Programlama 2

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

Emre Can Yılmaz

Ondokuz Mayıs Üniversitesi

2025

Sağlam Yazılımların Temeli - SOLID Prensipleri

  • Neden bazı programları değiştirmek çok zorken bazıları kolaydır?
  • Kodumuz zamanla nasıl daha karmaşık ve kırılgan hale gelir?
  • İyi Tasarımın Önemi:
    • Bakım Kolaylığı
    • Esneklik & Genişletilebilirlik
    • Anlaşılabilirlik & Takım Çalışması
    • Test Edilebilirlik
  • Bugün, daha sağlam, esnek ve bakımı kolay nesne yönelimli programlar yazmak için temel SOLID prensiplerini öğreneceğiz.

SOLID Nedir?

  • 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)

S: Tek Sorumluluk Prensibi (SRP) 🎯

  • İlke: Bir sınıfın yalnızca tek bir sorumluluğu veya görevi olmalıdır.
  • Neden Önemli?
    • Bir sınıf çok fazla iş yaparsa (örn: hem veri doğrulama, hem hesaplama, hem kayıt), bu işlerden birindeki değişiklik diğerlerini etkileyebilir.
    • Çok sorumluluklu sınıfları anlamak, test etmek ve bakımını yapmak zordur.
    • Sorumluluklar ayrıldığında, her parça daha küçük, odaklanmış ve tekrar kullanılabilir olur.

SRP İhlali: “Her Şeyi Yapan” Sınıf 👎

# 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ı...
  1. Doğrulama kuralları değişirse?
  2. Veritabanı yapısı/teknolojisi değişirse?
  3. E-posta içeriği/gönderme şekli değişirse?

Bu sınıfın değişmesi için birden fazla neden var! SRP ihlali.

SRP Uygulaması: Sorumlulukları Ayırma 👍

# İ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.

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

  • İlke: Yazılım varlıkları (sınıflar, modüller, fonksiyonlar) genişlemeye açık, ancak değişime kapalı olmalıdır.
  • Başka Bir Deyişle: Yeni davranışlar eklemek istediğimizde, mevcut, çalışan ve test edilmiş kodları değiştirmek zorunda kalmamalıyız. Bunun yerine, yeni kod ekleyerek sistemi genişletebilmeliyiz.
  • Neden Önemli? Mevcut kodu değiştirmek her zaman risklidir. Hata yapma olasılığı artar ve mevcut işlevselliği bozabiliriz. OCP, sistemi yeni gereksinimlere daha stabil ve kolay bir şekilde adapte etmemizi sağlar.
  • Nasıl Uygulanır? Genellikle soyutlama (abstract sınıflar, arayüzler) ve polimorfizm (çok biçimlilik) kullanılarak sağlanır.

OCP İhlali: Yeni Tip İçin Değişiklik Gerektiren Kod 👎

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

OCP Uygulaması: Strateji ile Genişletme 👍

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!

L: Liskov Yerine Geçme Prensibi (LSP)

  • İlke: Eğer S sınıfı, T sınıfının bir alt türü ise, o zaman T türündeki nesnelerin yerine S türündeki nesneler, programın istenilen davranışını (beklentisini) değiştirmeden kullanılabilmelidir.
  • Basitçe: Çocuk (alt sınıf), Anne/Babanın (üst sınıf) yerine geçtiğinde ortalığı karıştırmamalı! Anne/Babanın yapabildiği temel şeyleri aynı beklentileri karşılayarak yapabilmelidir.
  • Neden Önemli? Polimorfizmin güvenilir çalışmasını sağlar. Alt sınıf, üst sınıfın “sözleşmesini” bozarsa, üst sınıfı bekleyen kodlar alt sınıf verildiğinde beklenmedik şekilde hata verebilir veya yanlış çalışabilir.
  • Analoji: “Uzaktan Kumanda” (üst sınıf) bekleyen TV’ye, TV’yi açmak yerine klimayı çalıştıran bir “Özel Kumanda” (alt sınıf) verirseniz, bu LSP ihlalidir. Kumandanın temel işlevi (TV’yi kontrol etmek) bozulmuştur.

LSP İhlali Örneği: Uçamayan Kuş 👎

# Ü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.

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

  • İlke: İstemciler (sınıflar), kullanmadıkları metotları içeren arayüzleri (ya da soyut sınıfları) implemente etmeye zorlanmamalıdır.
  • Başka Bir Deyişle: “Şişman” arayüzler yerine, daha küçük, amaca özel ve bağdaşık (cohesive) arayüzler tercih edilmelidir.
  • Neden Önemli? Büyük arayüzler, implemente eden sınıfları gereksiz bağımlılıklara ve boş metot implementasyonlarına iter (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.
  • Analoji: Sadece kahve yapmak isteyen birinin, aynı zamanda tost yapabilen, meyve sıkabilen ve mikrodalga özelliği olan devasa bir makineyi kullanmak zorunda kalması yerine, sadece basit bir kahve makinesi kullanması daha iyidir.

ISP İhlali: “Her İşi Yapan” Arayüz 👎

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

ISP Uygulaması: Rollere Göre Arayüzler 👍

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.

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

  • İlke (İki Parçalı):
    1. Yüksek seviyeli modüller (örn: iş akışını yöneten sınıflar), düşük seviyeli modüllere (örn: veritabanı erişimi, dosya okuma/yazma, ağ iletişimi gibi detayları yapan sınıflar) doğrudan bağımlı olmamalıdır. Her ikisi de soyutlamalara (arayüzler veya soyut sınıflar) bağımlı olmalıdır.
    2. Soyutlamalar, detaylara (somut implementasyonlara) bağlı olmamalıdır. Detaylar, soyutlamalara bağlı olmalıdır.
  • Neden Önemli? Yüksek seviyeli modüller, uygulamanın “ne” yapıldığını belirlerken, düşük seviyeli modüller “nasıl” yapıldığını belirler. Yüksek seviyeli kodun, düşük seviyedeki implementasyon detaylarına sıkı sıkıya bağlı olması (tight coupling), sistemi katı ve kırılgan hale getirir. Düşük seviyeli bir parçayı (örn: veritabanı türünü) değiştirmek, yüksek seviyeli kodu da değiştirmeyi gerektirir. DIP, bu bağımlılığı tersine çevirerek esneklik sağlar.
  • Nasıl Uygulanır? Genellikle Dependency Injection (Bağımlılık Enjeksiyonu) teknikleri ile birlikte kullanılır. Yüksek seviyeli sınıf, ihtiyaç duyduğu düşük seviyeli işlevselliği bir arayüz üzerinden tanımlar ve bu arayüzü implemente eden somut nesneyi dışarıdan (constructor, metot veya setter ile) alır.

DIP İhlali: Somut Detaylara Bağımlılık 👎

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

DIP Uygulaması: Soyutlamalara Bağlanma 👍

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! ✅

SOLID Özet: Neden Bu İlkeler?

  • Tek Sorumluluk (S): Sınıfları küçük ve odaklı tutar. Değişikliklerin etkisini sınırlar.
  • Açık/Kapalı (O): Yeni özellik eklemeyi kolaylaştırır, mevcut kodu korur. Sistemi daha stabil hale getirir.
  • Liskov Yerine Geçme (L): Kalıtımın doğru kullanılmasını sağlar, polimorfizmin güvenilirliğini artırır. Mantıklı hiyerarşiler kurmaya yardımcı olur.
  • Arayüz Ayrımı (I): Gereksiz bağımlılıkları ve kod kirliliğini önler. Sınıfları daha bağımsız yapar.
  • Bağımlılık Tersine Çevirme (D): Sistemi esnek hale getirir. Farklı implementasyonları (veritabanı, servis vb.) kolayca değiştirmeyi sağlar. Test edilebilirliği artırır.

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.

Alıştırmalar

  • Her bir örnekteki ihlalleri ve nedenlerini tartışalım.
  • Bu ihlalleri düzeltmek için nasıl bir yol izlerdiniz? (Kısa fikirler)
  • SOLID prensiplerini uygulamak kodumuzu nasıl daha iyi hale getirir?

Alıştırma 1:

Örnekteki sınıfta hangi SOLID prensibi (veya prensipleri) ihlal ediliyor olabilir? Neden?

Kod

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

Alıştırma 2:

Soru: Bu fonksiyonda hangi SOLID prensibi ihlal ediliyor? Neden?

Kod

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?

Alıştırma 3:

Soru: EmailBildirimi(““) durumu hangi SOLID prensibini ihlal ediyor? Neden?

Kod

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?

Alıştırma 4:

Soru: USBKlavye sınıfı hangi SOLID prensibini ihlal ediyor? Neden?

Bu durum ne gibi sorunlara yol açabilir?

Kod

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

Alıştırma 5:

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?

Sorular & Tartışma

  • SOLID prensipleri hakkında kalan sorularınız var mı?
  • Bu ilkeleri gerçek hayatta uygulamak ne kadar önemli?
  • Gelecek Hafta: Tasarım Desenleri (SOLID ilkelerinin pratik uygulamaları)

Sorular & Tartışma

  • SOLID prensipleri hakkında sorularınız var mı?
  • Alıştırmadaki ihlalleri ve çözüm önerilerini tartışalım.
  • Bu ilkeleri uygulamak her zaman kolay mıdır? Zorlukları neler olabilir?
  • Gelecek Hafta: Tasarım Desenleri (Bu ilkelerin pratikte nasıl kullanıldığına dair somut örnekler)