Liskov Yerine Geçme Prensibi (LSP), SOLID prensiplerinin ‘L’ harfini temsil eder ve Nesne Yönelimli Programlama’da (OOP) kalıtımın doğru kullanımı için kritik bir ilkedir. Temel olarak, alt sınıfların, türetildikleri üst sınıfların yerine, programın davranışını veya doğruluğunu bozmadan geçebilmesi gerektiğini belirtir. Yani, bir üst sınıf nesnesi bekleyen herhangi bir kod parçası, o üst sınıftan türetilmiş herhangi bir alt sınıf nesnesiyle de sorunsuz bir şekilde çalışabilmelidir.
LSP’ye uymak, sağlam, güvenilir ve bakımı kolay yazılımlar oluşturmak için önemlidir. Ancak, kalıtım ilişkileri tasarlanırken bu prensibi ihlal etmek oldukça kolaydır. LSP ihlalleri, genellikle alt sınıfın, üst sınıfın beklenen davranışını (kontratını) değiştirmesi veya bozmasıyla ortaya çıkar. Bu durum, ilk bakışta fark edilmeyebilir ancak programın ilerleyen aşamalarında beklenmedik hatalara, mantıksal tutarsızlıklara ve bakım zorluklarına yol açabilir.
Bu rehberde, Liskov Yerine Geçme Prensibi’nin yaygın ihlal senaryolarını Python örnekleri üzerinden inceleyeceğiz. Her ihlal türünü açıklayacak, kod üzerinde nasıl göründüğünü gösterecek ve bu ihlallerin programın çalışması üzerindeki olumsuz sonuçlarını vurgulayacağız. Amacımız, LSP ihlallerini tanımayı öğrenmek ve neden bunlardan kaçınmamız gerektiğini anlamaktır.
Bölüm 1: LSP İhlallerinin Genel Sonuçları
Bir alt sınıf LSP’yi ihlal ettiğinde, yani üst sınıfının yerine güvenle geçemediğinde, bunun çeşitli olumsuz sonuçları olabilir:
Beklenmedik Hatalar (Runtime Errors): Üst sınıfı kullanan kod, alt sınıf nesnesiyle çalıştığında, alt sınıfın beklenmedik bir istisna fırlatması veya uyumsuz bir değer döndürmesi nedeniyle çökebilir (örn: TypeError, AttributeError, ValueError).
Yanlış veya Tutarsız Davranış: Program çökmesede bile, alt sınıfın farklı davranışı nedeniyle mantıksal hatalar oluşabilir. İstemci kodun beklentileri karşılanmadığı için yanlış sonuçlar üretilebilir.
Polimorfizmin Kırılması: LSP, polimorfizmin düzgün çalışmasının temelini oluşturur. Eğer alt sınıflar üst sınıfların yerine geçemiyorsa, üst sınıf türü üzerinden polimorfik olarak çalışması gereken kodlar güvenilirliğini yitirir.
Artan Karmaşıklık ve Tip Kontrolü İhtiyacı: İstemci kod, aldığı nesnenin hangi alt tipe ait olduğunu kontrol etmek (if isinstance(...)) ve ona göre farklı davranmak zorunda kalabilir. Bu, kodu karmaşıklaştırır, OCP'yi ihlal eder ve LSP'nin amacına ters düşer.
Bakım Zorluğu: Üst sınıfta veya istemci kodda yapılan değişiklikler, LSP’yi ihlal eden alt sınıflarda beklenmedik sorunlara yol açabilir. Hataların kaynağını bulmak zorlaşır.
Güven Kaybı: Bir sınıf hiyerarşisi LSP’ye uymuyorsa, geliştiriciler üst sınıf referanslarına güvenemezler ve sürekli olarak gelen nesnenin gerçek tipini kontrol etme ihtiyacı hissederler.
Şimdi, bu sonuçlara yol açan spesifik ihlal örneklerine bakalım.
Bölüm 2: İhlal Örneği 1 — Metot Önkoşullarını Güçlendirme
İhlal Açıklaması: Alt sınıf metodu, üst sınıf metodunun kabul ettiğinden daha kısıtlı girdiler (parametreler) kabul eder veya ek parametreler gerektirir. Üst sınıfın kabul ettiği bazı geçerli girdiler, alt sınıf için geçersiz hale gelir.
Kural: Alt sınıf metodunun önkoşulları (kabul ettiği girdiler/durumlar), üst sınıfın önkoşullarından daha güçlü olamaz (ancak daha zayıf veya aynı olabilir).
LSP İhlali — Güçlendirilmiş Önkoşul
class BelgeIsleyici:
def isle(self, belge_icerigi: str):
"""Herhangi bir string içeriği işler."""
print(f"Belge işleniyor (içerik uzunluğu: {len(belge_icerigi)})...")
# ... genel işleme mantığı ...
print("Belge işlendi.")
class HtmlBelgeIsleyici(BelgeIsleyici):
def isle(self, belge_icerigi: str):
"""Sadece HTML etiketleri içeren string'leri işler."""
# ÖNKOŞUL GÜÇLENDİRİLDİ: İçerik HTML olmalı.
if not belge_icerigi.strip().startswith('Test", " "]
karisik_metinler = ["Test", "Sadece metin", "..."]
toplu_isle(genel_isleyici, karisik_metinler)
print("-" * 20)
html_isleyici, BelgeIsleyici yerine geçebilmeli ama...
toplu_isle(html_isleyici, karisik_metinler) # "Sadece metin" için ValueError fırlatacak!
İhlalin Sonuçları:
toplu_isle fonksiyonu, BelgeIsleyici türünden bir nesne aldığında, ona gönderilen her string için isle metodunun çalışacağını varsayar.
Ancak HtmlBelgeIsleyici, sadece belirli formatta stringleri kabul ettiği için bu varsayımı bozar.
Sonuç olarak, toplu_isle fonksiyonuna HtmlBelgeIsleyici nesnesi verildiğinde, normal metin içeren stringler için beklenmedik ValueError hataları alınır.
Alt sınıf (HtmlBelgeIsleyici), üst sınıfın (BelgeIsleyici) yerine güvenle geçememiştir.
Olası Çözümler:
HtmlBelgeIsleyici'nin isle metodu, HTML olmayan içerik için hata fırlatmak yerine belki farklı bir işlem yapabilir veya içeriği olduğu gibi işleyebilir (eğer mantıklıysa).
Kalıtım ilişkisi gözden geçirilebilir. Belki de HtmlBelgeIsleyici doğrudan BelgeIsleyici'den türememelidir veya farklı bir arayüz kullanılmalıdır.
İstemci kod (toplu_isle), işleyiciye göndermeden önce içeriği kontrol edebilir, ancak bu LSP'nin ruhuna aykırıdır.
Bölüm 3: İhlal Örneği 2 — Metot Sonkoşullarını Zayıflatma
İhlal Açıklaması: Alt sınıf metodu, üst sınıf metodunun sağlamayı vaat ettiği bir sonucu (garantiyi) sağlamaz veya daha azını sağlar.
Kural: Alt sınıf metodunun sonkoşulları (sağladığı garantiler), üst sınıfın sonkoşullarından daha zayıf olamaz (ancak daha güçlü veya aynı olabilir).
LSP İhlali — Zayıflatılmış Sonkoşul
class Hesaplama:
def init(self):
self.sonuc_cache = {}
def hesapla(self, girdi: int) -> int:
"""
Bir hesaplama yapar. Önemli Garanti (Sonkoşul):
Sonuç her zaman pozitiftir veya sıfırdır.
Ayrıca sonucu cache'ler.
"""
if girdi in self.sonuc_cache:
return self.sonuc_cache[girdi]
print(f"Hesaplama yapılıyor: {girdi}")
# Karmaşık bir hesaplama varsayalım, sonuç >= 0 olmalı
sonuc = abs(girdi * 2) # abs() ile pozitifliği garanti edelim
self.sonuc_cache[girdi] = sonuc
return sonuc
class OzelHesaplama(Hesaplama):
def hesapla(self, girdi: int) -> int:
"""
Farklı bir hesaplama yapar. AMA SONKOŞULU BOZAR!
Sonuç negatif olabilir. Cache mekanizmasını da kullanmıyor.
"""
print(f"Özel Hesaplama yapılıyor: {girdi}")
# Üst sınıfın pozitif sonuç garantisini ihlal ediyor!
sonuc = girdi - 10
# Cache'leme de yapılmıyor! (Bu da bir kontrat ihlali sayılabilir)
return sonuc
Hesaplama nesnesi bekleyen istemci kod
def sonuc_kullan(hesaplayici: Hesaplama, deger: int):
print(f"\n{deger} için sonuç kullanılıyor...")
sonuc = hesaplayici.hesapla(deger)
# İstemci, Hesaplama kontratına göre sonucun >= 0 olacağını varsayıyor
try:
import math
karekok = math.sqrt(sonuc) # Negatif sayı gelirse ValueError verir!
print(f"Sonuç: {sonuc}, Karekök: {karekok:.2f}")
except ValueError as e:
print(f"Hata: Sonuç ({sonuc}) beklendiği gibi pozitif değil! {e}")
except Exception as e:
print(f"Genel hata: {e}")
Kullanım
h1 = Hesaplama()
h2 = OzelHesaplama() # Hesaplama yerine geçebilmeli mi?
sonuc_kullan(h1, 5) # Başarılı
sonuc_kullan(h1, -3) # Başarılı (abs() sayesinde sonuç 6 olur)
print("-" * 20)
sonuc_kullan(h2, 20) # Başarılı (sonuç 10)
sonuc_kullan(h2, 5) # BAŞARISIZ! (sonuç -5 olur, sqrt hata verir)
İhlalin Sonuçları:
sonuc_kullan fonksiyonu, aldığı Hesaplama nesnesinin hesapla metodunun her zaman pozitif veya sıfır bir sonuç döndüreceği kontratına güvenir.
OzelHesaplama sınıfı, hesapla metodunu override ederken bu kontratı (pozitif sonuç garantisini) bozar ve negatif sonuçlar döndürebilir.
Fonksiyona OzelHesaplama nesnesi verildiğinde ve sonuç negatif olduğunda, math.sqrt() fonksiyonu ValueError fırlatır ve istemci kodun beklentisi karşılanmaz.
Alt sınıf, üst sınıfın yerine güvenle geçememiştir.
Olası Çözümler:
OzelHesaplama.hesapla metodu, üst sınıfın sonkoşuluna uyacak şekilde (örneğin, sonucu max(0, sonuc) yaparak) değiştirilebilir.
Eğer OzelHesaplama'nın negatif sonuç döndürmesi gerekiyorsa, belki de Hesaplama'dan türememelidir. Farklı bir arayüz veya hiyerarşi düşünülebilir.
Bölüm 5: İhlal Örneği 3 — Yeni İstisnalar Fırlatma
İhlal Açıklaması: Alt sınıf metodu, üst sınıf metodunun fırlatmadığı ve istemci kodun beklemediği yeni türde bir istisna fırlatır.
Kural: Alt sınıf metodu, üst sınıf metodunun fırlattığı istisnaların alt türlerini fırlatabilir, ancak istemcinin beklemediği tamamen yeni türler fırlatmamalıdır.
LSP İhlali — Yeni İstisna Türü
Özel Hata Tipleri (Örnek için)
class KimlikDogrulamaError(Exception): pass
class VeritabaniBaglantiError(Exception): pass
class AgBaglantiError(Exception): pass # Yeni ve beklenmedik hata tipi
class ServisBaglayici:
def baglan(self, adres: str):
"""Servise bağlanır. Bağlantı sorununda KimlikDogrulamaError veya VeritabaniBaglantiError fırlatabilir."""
print(f"'{adres}' adresine bağlanılıyor...")
if "hata" in adres:
raise VeritabaniBaglantiError(f"'{adres}' veritabanına bağlanamadı!")
elif "auth" in adres:
raise KimlikDogrulamaError(f"'{adres}' için kimlik doğrulanamadı!")
print("Bağlantı başarılı.")
class GelismisServisBaglayici(ServisBaglayici):
def baglan(self, adres: str):
"""
Gelişmiş servise bağlanır. Normal hatalara ek olarak
AgBaglantiError da fırlatabilir.
"""
print(f"'{adres}' adresine GELİŞMİŞ yöntemle bağlanılıyor...")
if "network" in adres:
# Üst sınıfın fırlatmadığı yeni bir istisna türü!
raise AgBaglantiError("Ağ bağlantısı zaman aşımına uğradı!")
else:
# Diğer durumlar için üst sınıfın davranışını çağıralım
try:
super().baglan(adres)
except (KimlikDogrulamaError, VeritabaniBaglantiError):
# Üst sınıftan gelen hataları tekrar fırlatabiliriz veya handle edebiliriz
print("Gelişmiş bağlayıcıda üst sınıf hatası oluştu.")
raise # Tekrar fırlat
ServisBaglayici bekleyen istemci kod
def servise_guvenli_baglan(baglayici: ServisBaglayici, hedef_adres: str):
print(f"\n'{hedef_adres}' adresine bağlanma denemesi...")
try:
baglayici.baglan(hedef_adres)
except KimlikDogrulamaError as e:
print(f"İstemci Kodu: Kimlik doğrulama sorunu yakalandı - {e}")
except VeritabaniBaglantiError as e:
print(f"İstemci Kodu: Veritabanı sorunu yakalandı - {e}")
# BU BLOK AgBaglantiError'u BEKLEMİYOR!
except Exception as e:
print(f"İstemci Kodu: Beklenmedik bir genel hata yakalandı - {type(e).name}: {e}")
Kullanım
s_baglayici = ServisBaglayici()
g_baglayici = GelismisServisBaglayici() # ServisBaglayici yerine geçebilmeli mi?
servise_guvenli_baglan(s_baglayici, "db://normal")
servise_guvenli_baglan(s_baglayici, "db://hata")
servise_guvenli_baglan(s_baglayici, "auth_problem")
print("-" * 20)
servise_guvenli_baglan(g_baglayici, "db://normal")
servise_guvenli_baglan(g_baglayici, "db://hata")
servise_guvenli_baglan(g_baglayici, "network_error") # AgBaglantiError fırlatacak!
İhlalin Sonuçları:
servise_guvenli_baglan fonksiyonu, ServisBaglayici'nin sadece KimlikDogrulamaError veya VeritabaniBaglantiError fırlatabileceğini varsayarak yazılmıştır ve sadece bu hataları özel olarak ele alır.
GelismisServisBaglayici ise ek olarak AgBaglantiError fırlatabilir.
Fonksiyona GelismisServisBaglayici nesnesi verildiğinde ve AgBaglantiError oluştuğunda, istemci kod bu hatayı özel olarak yakalayamaz. Ya genel Exception bloğuna düşer (eğer varsa) ya da hiç yakalanamazsa program çöker.
Alt sınıf, üst sınıfın istisna kontratını (fırlatabileceği hata türlerini) bozmuştur ve yerine güvenle geçememiştir.
Olası Çözümler:
GelismisServisBaglayici'nin fırlattığı AgBaglantiError, belki de daha genel bir üst sınıf istisnasından (örneğin, BaglantiError) türetilebilir ve istemci kod bu genel hatayı yakalayabilir.
AgBaglantiError fırlatmak yerine, bu durum farklı bir şekilde (örneğin, özel bir dönüş değeri ile) ele alınabilir.
Kalıtım ilişkisi gözden geçirilebilir.
Bölüm 6: İhlal Örneği 4 — Metotları Anlamsız Kılma
İhlal Açıklaması: Alt sınıf, miras aldığı ve üst sınıfın kontratında anlamlı olan bir metodu ya hiç implemente etmez (pass ile geçer) ya da her zaman bir hata (NotImplementedError) fırlatacak şekilde override eder.
LSP İhlali — Anlamsız Kılınan Metot
class VeriKaydedici:
def ac(self):
print("Kaynak açılıyor...")
# ... kaynak açma işlemleri ...
self._acik = True
def kapat(self):
print("Kaynak kapatılıyor...")
self._acik = False
def veri_yaz(self, veri):
if not getattr(self, '_acik', False): # Açık olup olmadığını kontrol et
raise IOError("Kaynak yazmak için açık değil!")
print(f"Veri yazılıyor: {veri}")
# ... yazma işlemleri ...
Salt Okunur Kaydedici? Bu tasarım LSP'yi ihlal eder.
class SaltOkunurKaydedici(VeriKaydedici):
def ac(self):
print("Salt okunur kaynak 'açılıyor' (gibi).")
self._acik = True # Durumu ayarlayabilir ama yazamaz
def kapat(self):
print("Salt okunur kaynak 'kapatılıyor'.")
self._acik = False
# !!! LSP İHLALİ BURADA !!!
def veri_yaz(self, veri):
# Üst sınıf yazmayı vaat ediyor ama bu alt sınıf yazamıyor!
# Bu metodu çağıran kod hata alacak.
raise IOError("Bu kaydedici salt okunurdur, yazma işlemi desteklenmez!")
VeriKaydedici bekleyen kod
def toplu_yaz(kaydedici: VeriKaydedici, veriler: list):
print(f"\n--- {type(kaydedici).name} ile Toplu Yazma ---")
try:
kaydedici.ac()
for veri in veriler:
# Bu kod, kaydedici'nin veri_yaz metoduna sahip olduğunu
# ve yazma işlemini desteklediğini varsayar.
kaydedici.veri_yaz(veri)
print("Yazma tamamlandı.")
except IOError as e:
print(f"Yazma sırasında hata: {e}")
except Exception as e:
print(f"Genel hata: {e}")
finally:
try:
kaydedici.kapat()
except: pass # Kapatma hatasını yoksayalım şimdilik
Kullanım
normal_kaydedici = VeriKaydedici()
salt_okunur = SaltOkunurKaydedici() # VeriKaydedici yerine geçebilir mi?
veriler_list = ["veri1", "veri2"]
toplu_yaz(normal_kaydedici, veriler_list) # Başarılı
print("-" * 20)
toplu_yaz(salt_okunur, veriler_list) # BAŞARISIZ! (veri_yaz IOError fırlatır)
İhlalin Sonuçları:
toplu_yaz fonksiyonu, aldığı VeriKaydedici nesnesinin hem ac, hem kapat, hem de veri_yaz yeteneğine sahip olduğunu varsayar.
SaltOkunurKaydedici, veri_yaz metodunu miras almasına rağmen, onu her zaman hata fırlatacak şekilde override eder. Bu, üst sınıfın "veri yazabilme" kontratını ihlal eder.
Fonksiyona SaltOkunurKaydedici nesnesi verildiğinde, veri_yaz çağrısı her zaman IOError fırlatır ve fonksiyon beklendiği gibi çalışmaz.
Alt sınıf, üst sınıfın yerine güvenle geçememiştir. “SaltOkunurKaydedici bir VeriKaydedici’dir” ifadesi davranışsal olarak yanlıştır.
Olası Çözümler:
Kalıtım hiyerarşisini yeniden tasarlamak. Belki hem okuma hem yazma için ayrı arayüzler (ABCs) olmalı (IOkuyucu, IYazici) ve sınıflar sadece destekledikleri arayüzleri implemente etmeli. VeriKaydedici belki hem IOkuyucu hem IYazici olabilirken, SaltOkunurKaydedici sadece IOkuyucu olabilir.
Kompozisyon kullanılabilir.
Bölüm 7: İhlalleri Tespit Etme Yolları (Code Smells)
Kodunuzda LSP ihlali olup olmadığını anlamanıza yardımcı olabilecek bazı “kod kokuları”:
isinstance veya type() Kontrolleri: Kodunuzun bir yerinde, bir üst sınıf referansı üzerinden gelen nesnenin spesifik alt tipini kontrol edip ona göre farklı bir kod yolu izliyorsanız (if isinstance(obj, AltTip1): ... elif isinstance(obj, AltTip2): ...), bu genellikle LSP'nin ihlal edildiğinin ve polimorfizmin düzgün çalışmadığının güçlü bir işaretidir.
Boş veya Hata Fırlatan Override Metotlar: Alt sınıflarda sık sık pass ile geçilen veya NotImplementedError fırlatan override edilmiş metotlar görmek, kalıtım hiyerarşisinin yanlış kurulmuş olabileceğini düşündürür.
Beklenmedik Davranışlar ve Hatalar: Polimorfik olarak kullanılması beklenen kodda, belirli alt sınıf türleri verildiğinde beklenmedik hatalar veya yanlış sonuçlar alıyorsanız, LSP ihlali olabilir.
Alt Sınıfın Üst Sınıf Kontratını Daraltması: Dokümantasyonda veya kodun mantığında, alt sınıfın üst sınıftan daha kısıtlı çalıştığı durumlar LSP ihlaline işaret edebilir.
Bölüm 8: Sonuç: Davranışsal Uyumluluğun Sağlanması
Liskov Yerine Geçme Prensibi (LSP), Nesne Yönelimli Tasarımda kalıtımın doğru ve güvenilir kullanımını sağlamak için temel bir ilkedir. Alt sınıfların, üst sınıfların davranışsal kontratlarına uymasını ve onların yerine programın doğruluğunu bozmadan geçebilmesini gerektirir.
Bu rehberde incelenen ihlal örnekleri (önkoşulları güçlendirme, sonkoşulları zayıflatma, yeni istisnalar fırlatma, metotları anlamsız kılma) LSP’nin nasıl kırılabileceğini ve bunun sonucunda ortaya çıkan sorunları (beklenmedik hatalar, yanlış davranışlar, kırık polimorfizm, artan karmaşıklık) göstermiştir.
LSP ihlallerinden kaçınmak için:
Kalıtım ilişkilerini kurarken “Is-A” testini dikkatlice uygulayın ve sadece davranışsal olarak da uyumluysa kullanın.
Alt sınıfların, üst sınıfların kontratlarına (beklentilerine) saygı gösterdiğinden emin olun.
Gerektiğinde kalıtım yerine kompozisyon veya daha uygun tasarım desenlerini tercih edin.
Arayüzleri netleştirmek ve kontratları zorunlu kılmak için Soyut Temel Sınıfları (ABCs) veya Protokolleri kullanmayı düşünün.
Kodunuzda aşırı tip kontrolü yapma ihtiyacı hissediyorsanız, tasarımınızı gözden geçirin.
LSP’ye uygun kod yazmak, sadece teorik bir egzersiz değil, aynı zamanda daha sağlam, güvenilir, bakımı kolay ve gerçekten polimorfik OOP sistemleri oluşturmanın pratik bir gerekliliğidir.
Abdulkadir Güngör - Kişisel WebSite
Abdulkadir Güngör - Kişisel WebSite
Abdulkadir Güngör - Özgeçmiş
Github
Github
Linkedin