LSP (Liskov Substitution Principle), nesne yönelimli programlamada doğru kalıtımın koşullarını tanımlayan SOLID’in üçüncü ilkesidir. İlke, Barbara Liskov tarafından 1987’de formüle edilmiş ve şu şekilde resmileştirilmiştir: S, T’nin bir alt türüyse, T türündeki nesneler, programın özelliklerini değiştirmeden S türündeki nesnelerle değiştirilebilir. Robert Martin’in kitabında Clean Architecture (2017) belirtildiği gibi, yerine koyma ilkesi, bir alt sınıfın temel sınıfın sözleşmesini zayıflatmamasını gerektirir.
Anahtar Noktalar
LSP (Liskov Substitution Principle), Barbara Liskov tarafından 1987 OOPSLA konferansında formüle edilen yerine koyma ilkesidir. Resmi tanım: q(x), T türündeki x nesnelerinin kanıtlanabilir bir özelliği olsun. O halde q(y), S türündeki y nesneleri için kanıtlanabilir olmalıdır; burada S, T’nin bir alt türüdür. Basitçe söylemek gerekirse: bir alt sınıfın nesneleri, temel sınıfla çalışan kodun alt sınıfla da doğru şekilde çalışmaya devam edeceği şekilde davranmalıdır.
Pratikte LSP, bir alt sınıfın temel sınıfın sözleşmesini ihlal etmemesi gerektiği anlamına gelir. Sözleşme, ön koşulları (bir metodu çağırmak için neyin gerekli olduğu), son koşulları (çağrıdan sonra neyin garanti edildiği) ve değişmezleri (nesnenin ömrü boyunca devam eden koşullar) içerir. Bir alt sınıf ön koşulları güçlendirebilir veya son koşulları zayıflatabilir — bu tam olarak bir LSP ihlalidir.
LSP ihlalinin klasik bir örneği, bir dikdörtgenden miras alan karedir. Dikdörtgenin setWidth metodu genişliği ayarlarken, karede hem genişliği hem de yüksekliği ayarlar. Bir dikdörtgenin davranışını (bir kenarı değiştirmenin diğerini etkilememesi) bekleyen bir istemci, beklenmeyen bir sonuç alır. Bir kare, bir dikdörtgenin geçerli bir alt türü değildir.
LSP, doğru kalıtım için üç koşul belirler: alt sınıfın ön koşulları temel sınıfın ön koşullarından daha güçlü olamaz (alt sınıf daha fazlasını talep etmez), alt sınıfın son koşulları temel sınıfın son koşullarından daha zayıf olamaz (alt sınıf daha azını garanti etmez) ve temel sınıfın değişmezleri alt sınıfta korunmalıdır. Bu koşullar, Bertrand Meyer’e göre Sözleşmeye Göre Tasarım kuralı olarak bilinir.
En az bir koşul ihlal edilirse, polimorfizm kullanan kod başarısız olabilir. Derleyici anlamsal sözleşmeleri değil, yalnızca sözdizimsel sözleşmeleri kontrol eder. Bu nedenle LSP, statik türlemenin değil, mimari disiplinin bir meselesidir.
LSP mekanizması, türlerin davranışsal uyumluluğuna dayanır. S sınıfı, T sınıfından miras alıyorsa, istemci kodu T’nin beklendiği her yerde S’yi davranışını değiştirmeden kullanabilmelidir. Bu, yalnızca metod imzalarını değil, aynı zamanda anlamlarını da içerir.
LSP, bir alt sınıfın yeni davranış eklemesini yasaklamaz. Temel sınıf için yazılmış kodun beklentilerini ihlal etmek yasaktır. Temel sınıf, save metodunun istisna fırlatmadığını garanti ediyorsa, alt sınıf onları fırlatmamalıdır. Temel sınıf negatif olmayan bir değer döndürüyorsa, alt sınıf negatif bir değer döndürmemelidir.
Gerçek projelerde LSP en çok alt sınıfın metodlarına koşullu mantık eklenirken ihlal edilir: «koşul varsa — istisna fırlat», «koşul varsa — null döndür». Bu «sürprizlerin» her biri polimorfizmi zayıflatır ve istemci kodunu çağırmadan önce nesne türünü kontrol etmeye zorlar — bu, nesne yönelimli tasarımın temel fikriyle çelişir.
Mobil projelerde tipik bir LSP ihlali, temel ViewModel’ler oluşturulurken meydana gelir. BaseViewModel, onCleared metodunun tüm kaynakları serbest bıraktığını garanti ediyorsa ve bir alt sınıf bu metodu boş olarak geçersiz kılarsa — polimorfik bir onCleared çağrısı yoluyla kaynak temizliğine güvenen herhangi bir kod hatalı çalışır. LSP, alt sınıfın super.onCleared() çağırmasını veya aynı işi kendisinin yapmasını gerektirir. LifecycleObserver aracılığıyla kompozisyon, yaşam döngüsü yönetiminde LSP ihlalini ortadan kaldıran bir alternatiftir.
LSP ihlalinin başlıca göstergeleri şunları içerir: bir metodu çağırmadan önce instanceof veya is ile nesne türünü kontrol etme, boş metod uygulamaları (stubs), NotImplementedError veya UnsupportedOperationException fırlatma, değer yerine null döndürme. Bu modellerin her biri, alt sınıfın geçerli bir alt tür olmadığını gösterir.
Bir diğer yaygın belirti, «bir-şeydir» (is-a) ilişkisini modellemek yerine kod yeniden kullanımı amacıyla yapılan kalıtımdır. Bird sınıfının fly() metodu vardır. Penguin sınıfı, Bird’den miras alır ve fly() metodunu boş veya istisna fırlatan olarak geçersiz kılar. Bu bir LSP ihlalidir: bir penguen, kuşun geçerli bir alt türü değildir.
Mobil geliştirmede, stub metotları olan temel ViewHolder, Fragment veya ViewController sınıfları oluşturulurken LSP ihlal edilir. Bir alt sınıf, temel sınıfın metotlarının yarısını kullanmıyorsa — kalıtım yanlış seçilmiştir. Kompozisyon veya arayüz ayrıştırması sorunu daha doğru çözer.
LSP’yi kontrol etmek için basit bir test: temel sınıf için sözleşmesini (dönüş değerleri, istisnalar, yan etkiler) doğrulayan bir birim testi yazın. Bu testi her alt sınıf için çalıştırın. Test başarısız olursa — LSP ihlal edilmiştir. Bu yaklaşıma «temel sınıf sözleşmesi aracılığıyla test etme» denir.
Android projelerinde böyle bir test, ViewModel ve Repository için kullanışlıdır. BaseViewModel bir hatadan önce Loading durumunu garanti ediyorsa ve bir alt sınıf Loading olmadan hata fırlatıyorsa — test, CI aşamasında LSP ihlalini tespit edecektir.
ClickListener işleme ile bir Android örneğine bakalım. Temel uygulama bir şeyi garanti ettiğinde ve alt sınıf bunu ihlal ettiğinde bir LSP ihlali oluşur.
// Garantili temel sınıf: onClick çağrılacak
open class BaseClickListener {
open fun onClick(view: View) {
// temel işleme
}
}
// LSP ihlali: alt sınıf istisna fırlatan bir koşul ekler
class RestrictedClickListener : BaseClickListener() {
override fun onClick(view: View) {
if (!isLoggedIn) {
throw IllegalStateException("Not logged in")
}
super.onClick(view)
}
}
// Doğru çözüm: sözleşme ihlal edilmedi
class ConditionalClickListener : BaseClickListener() {
override fun onClick(view: View) {
if (isLoggedIn) {
super.onClick(view)
}
}
}
DataSource protokolüyle bir iOS örneği, veri yerine nil döndürerek LSP ihlalini gösterir:
// Sözleşmeli protokol: veri veya hata döndürür
protocol DataProvider {
func fetchData() async throws -> [String]
}
// LSP ihlali: hata olmadan nil döndürür
class SilentFailProvider: DataProvider {
func fetchData() async throws -> [String] {
return [] // hata yerine boş dizi
}
}
// Doğru LSP uyumu
class NetworkProvider: DataProvider {
func fetchData() async throws -> [String] {
throw NetworkError.timeout
}
}
Pratik kural: bir alt sınıf temel sınıfın sözleşmesini yerine getiremiyorsa, alt sınıf olmamalıdır. Bir alternatif, minimum sözleşmeli bir arayüz çıkarmak ve her türde kendi yolunda uygulamaktır.
Kompozisyon, «bir-şeydir» (is-a) ilişkisinin belirsiz veya koşullu olduğu durumlarda kalıtıma tercih edilir. Klasik bir örnek: Manager bir Employee midir? Evet. Peki Square geçerli bir Rectangle mıdır? LSP «hayır» der. Kalıtımın doğruluğundan şüphe ediyorsanız — kompozisyonu seçin.
Mobil geliştirmede kompozisyon genellikle bağımlılık enjeksiyonu yoluyla kullanılır: bir sınıf, davranışı bir temel sınıftan miras almak yerine bir kurucu aracılığıyla alır. Bir ViewModel, Repository’den miras almaz, onu bir bağımlılık olarak kabul eder. Bu, tanım gereği LSP ihlalini ortadan kaldırır — kalıtım yoksa sözleşme ihlali de yoktur.
Kalıtımın kompozisyonla değiştirilmesi gerektiğinin işaretleri: alt sınıf temel sınıfın bazı metotlarını kullanmıyor, alt sınıf metotları boş stub’larla geçersiz kılıyor, istemci kodu instanceof ile nesne türünü kontrol ediyor. Bu durumlarda kalıtım yanlış seçilmiştir ve LSP ihlal edilmiştir.
Arayüzler, LSP sorununu kalıtım olmadan çözer: her tür, ihtiyaç duyduğu metotları tam olarak uygular. fly() metodu olan ortak bir temel Bird sınıfı (Penguin’in uçamadığı) yerine — yalnızca uçan kuşların uyguladığı bir Flyable arayüzü. Penguin, fly() metodu olmadan Bird’ü uygular — LSP ihlal edilmemiştir.
Android mimarisinde bu yaklaşım, ayrıştırılmış UseCase arayüzleri aracılığıyla uygulanır: getAll, getById, save, delete metotları olan tek bir büyük UseCase yerine — ayrı GetItemsUseCase, SaveItemUseCase arayüzleri. Bir istemci yalnızca ihtiyaç duyulan arayüze bağımlıdır ve bu arayüzü uygulayan herhangi bir sınıf, LSP açısından doğrudur.
Sıkça Sorulan Sorular
Kalıtım bir dil mekanizmasıdır; LSP, bu mekanizmanın doğru kullanımı için bir kuraldır. Kalıtım imza uyumluluğunu (sözdizimi) garanti eder; LSP, davranışsal uyumluluk (anlam) gerektirir. LSP’siz kalıtım, çalışma zamanında bozulan polimorfizm sağlar.
Temel sınıf null olmayan bir dönüşü garanti ediyorsa — evet. Sözleşme null’a (İsteğe bağlı değer) izin veriyorsa — hayır. LSP null’u yasaklamaz; sözleşmeyi zayıflatmayı yasaklar. Temel sınıfın belgelerini inceleyin ve alt sınıfın sözleşmesinin uyumlu olup olmadığını kontrol edin.
LSP, protokollere sınıflara uygulandığı gibi uygulanır. Bir protokol uygulaması anlamsal sözleşmeye uymalıdır: bir protokol bir metodu non-throwing olarak tanımlıyorsa, uygulama hata fırlatmamalıdır. Swift bunu derleyici düzeyinde kontrol etmez — sorumluluk geliştiricidedir.
Kotlin’de Sealed class özel bir durumdur çünkü hiyerarşi kapalıdır ve derleyici tarafından bilinir. LSP, sealed class’a daha az uygulanır çünkü tüm alt türler when ifadesinde açıkça listelenmiştir. Sealed bir alt sınıfın hatası yerel olur, gizli bir polimorfik hata olmaz.
Temel sınıf için tüm alt sınıflarında çalışan parametreli bir test yazın. Test, temel davranışsal sözleşmeleri doğrular: dönüş değerleri, istisnalar, durumlar. Test alt sınıflardan birinde başarısız olursa — LSP ihlal edilmiştir. CI’da böyle bir test, polimorfik kodun gerilemesini önler.
Özet
Anahtar teslim bir mobil uygulama geliştireceğiz
IT Sectr, 2017'den beri girişimler ve işletmeler için iOS ve Android uygulamaları oluşturmaktadır. Size danışmanlık yapacak ve en iyi çözümü önereceğiz.
Ayrıca okuyun