LSP (Liskov Substitution Principle) — SOLID-in üçüncü prinsipi olub, obyekt yönümlü proqramlaşdırmada düzgün mirasın şərtlərini müəyyən edir. Prinsip Barbara Liskov tərəfindən 1987-ci ildə formalaşdırılmışdır: əgər S T-nin alt tipidirsə, onda T obyektləri proqramın xüsusiyyətlərini dəyişmədən S obyektləri ilə əvəz edilə bilər. Robert Martinin Clean Architecture (2017) kitabında qeyd edildiyi kimi, əvəzetmə prinsipi alt sinfin baza sinfinin kontraktını zəiflətməməsini tələb edir.
Əsas məqamlar
LSP (Liskov Substitution Principle) — Barbara Liskov tərəfindən 1987-ci ildə OOPSLA konfransında formalaşdırılmış əvəzetmə prinsipidir. Formal tərif: q(x) T tipli x obyektlərinin sübut edilə bilən xüsusiyyəti olsun. Onda q(y) S tipli y obyektləri üçün də sübut edilə bilən olmalıdır, burada S T-nin alt tipidir. Sadə dillə desək: alt sinfin obyektləri elə davranmalıdır ki, baza sinfi ilə işləyən kod alt siniflə də düzgün işləməyə davam etsin.
Praktikada LSP o deməkdir ki, alt sinif baza sinfinin kontraktını pozmamalıdır. Kontrakt ilkin şərtləri (metodun çağırılması üçün nə tələb olunur), son şərtləri (çağırışdan sonra nə zəmanət verilir) və invariantları (obyektin ömrü boyu qorunan şərtlər) əhatə edir. Alt sinif ilkin şərtləri gücləndirə və ya son şərtləri zəiflədə bilər — bu LSP-nin pozulmasıdır.
LSP-nin pozulmasının klassik nümunəsi — düzbucaqlıdan miras alan kvadrat. Düzbucaqlıda setWidth metodu eni təyin edir, kvadratda isə — həm eni, həm də hündürlüyü. Düzbucaqlının davranışını gözləyən müştəri (bir tərəfin dəyişməsi digərinə təsir etmir) gözlənilməz nəticə alır. Kvadrat düzbucaqlının düzgün alt tipi deyil.
LSP düzgün miras üçün üç şərt müəyyən edir: alt sinfin ilkin şərtləri baza sinfinin ilkin şərtlərindən güclü ola bilməz (alt sinif daha çox tələb etmir), alt sinfin son şərtləri baza sinfinin son şərtlərindən zəif ola bilməz (alt sinif az zəmanət vermir), baza sinfinin invariantları alt sinifdə qorunmalıdır. Bu şərtlər Bertrand Meyerin kontrakt üzrə dizayn qaydası kimi tanınır.
Ən azı bir şərt pozularsa — polimorfizmdən istifadə edən kod nasaz işləyə bilər. Kompilyator semantik kontraktları yox, yalnız sintaktik olanları yoxlayır. Buna görə LSP statik tipləmə deyil, memarlıq intizamı məsələsidir.
LSP mexanizmi tiplərin davranış uyğunluğuna əsaslanır. S sinfi T sinfindən miras alırsa, müştəri kodu T-nin gözlənildiyi hər yerdə S-dən istifadə edə bilməli və davranışı dəyişməməlidir. Buraya təkcə metod imzaları deyil, həm də onların semantikası daxildir.
LSP alt sinfə yeni davranış əlavə etməyi qadağan etmir. Baza sinfi üçün yazılmış kodun gözləntilərini pozmaq qadağandır. Baza sinfi save metodunun istisna atmadığına zəmanət verirsə, alt sinif onları atmamalıdır. Baza sinfi qeyri-mənfi dəyər qaytarırsa, alt sinif mənfi dəyər qaytarmamalıdır.
Real layihələrdə LSP ən çox alt sinfin metodlarına şərti məntiq əlavə etməklə pozulur: «şərt varsa — istisna at», «şərt varsa — null qaytar». Hər bir belə «sürpriz» polimorfizmi sarsıdır və müştəri kodunu çağırışdan əvvəl obyektin tipini yoxlamağa məcbur edir — bu, obyekt yönümlü dizayn ideyasına ziddir.
Mobil layihələrdə tipik LSP pozulması əsas ViewModel yaratdıqda baş verir. BaseViewModel onCleared metodunun bütün resursları buraxdığına zəmanət verirsə, alt sinif bu metodu boş olaraq dəyişdirirsə — onCleared-ın polimorf çağırışı ilə resursların boşaldılmasına güvənən hər hansı kod düzgün işləməyəcək. LSP tələb edir ki, alt sinif ya super.onCleared() çağırsın, ya da eyni işi özü görsün. LifecycleObserver vasitəsilə kompozisiya — həyat dövrünün idarə edilməsində LSP pozulmasını istisna edən alternativ.
Əsas göstəricilər LSP-nin pozulmasına daxildir: metod çağırmazdan əvvəl obyektin tipini instanceof və ya is ilə yoxlamaq, metodların boş implementasiyaları (stublar), NotImplementedError və ya UnsupportedOperationException istisnası atmaq, dəyər əvəzinə null qaytarmaq. Bu nümunələrin hər biri alt sinfin düzgün alt tip olmadığını göstərir.
Digər geniş yayılmış əlamət — «dir» (is-a) münasibətini modelləşdirmək deyil, kodu təkrar istifadə etmək məqsədilə miras almaqdır. Bird sinfinin fly() metodu var. Penguin sinfi Bird-dən miras alır və fly()-i boş və ya istisna atan kimi dəyişdirir. Bu LSP-nin pozulmasıdır: pinqvin quşun düzgün alt tipi deyil.
Mobil proqramlaşdırmada LSP stublar-metodları olan əsas ViewHolder, Fragment və ya ViewController yaratdıqda pozulur. Alt sinif baza sinfinin metodlarının yarısını istifadə etmirsə — miras səhv seçilmişdir. Kompozisiya və ya interfeysin bölünməsi problemi daha düzgün həll edir.
Sadə test LSP-ni yoxlamaq üçün: baza sinfi üçün onun kontraktını yoxlayan vahid test yazın (qaytarılan dəyərlər, istisnalar, yan təsirlər). Bu testi hər bir alt sinif üçün işə salın. Test uğursuz olarsa — LSP pozulub. Bu yanaşma «baza sinfinin kontraktı vasitəsilə test etmək» adlanır.
Android layihələrində belə test ViewModel və Repository üçün faydalıdır. BaseViewModel səhvdən əvvəl Loading vəziyyətinə zəmanət verirsə, alt sinif Loading olmadan səhv atırsa — test CI mərhələsində LSP pozulmasını aşkarlayacaq.
ClickListener-in işlənməsi ilə Android nümunəsini nəzərdən keçirək. LSP pozulması o zaman baş verir ki, əsas implementasiya nəyəsə zəmanət verir, alt sinif isə bunu pozur.
// Zəmanətli baza sinfi: onClick çağırılacaq
open class BaseClickListener {
open fun onClick(view: View) {
// əsas emal
}
}
// LSP pozulması: alt sinif istisna atan şərt əlavə edir
class RestrictedClickListener : BaseClickListener() {
override fun onClick(view: View) {
if (!isLoggedIn) {
throw IllegalStateException("Not logged in")
}
super.onClick(view)
}
}
// Düzgün həll: kontrakt pozulmayıb
class ConditionalClickListener : BaseClickListener() {
override fun onClick(view: View) {
if (isLoggedIn) {
super.onClick(view)
}
}
}
DataSource protokolu ilə iOS nümunəsi məlumat əvəzinə nil qaytarmaqla LSP pozulmasını göstərir:
// Kontraktlı protokol: məlumat və ya səhv qaytarır
protocol DataProvider {
func fetchData() async throws -> [String]
}
// LSP pozulması: səhv olmadan nil qaytarır
class SilentFailProvider: DataProvider {
func fetchData() async throws -> [String] {
return [] // səhv əvəzinə boş massiv
}
}
// LSP-yə düzgün riayət
class NetworkProvider: DataProvider {
func fetchData() async throws -> [String] {
throw NetworkError.timeout
}
}
Praktik qayda: alt sinif baza sinfinin kontraktını yerinə yetirə bilmirsə — o, alt sinif olmamalıdır. Alternativ — minimal kontraktlı interfeys ayırmaq və onu hər bir tipdə özünəməxsus şəkildə implementasiya etməkdir.
Kompozisiya «dir» (is-a) münasibətinin qeyri-müəyyən və ya şərti olduğu hallarda mirasdan daha üstündür. Klassik nümunə: Manager Employee-dir? Bəli. Bəs Square düzgün Rectangle-dirmi? LSP «xeyr» deyir. Mirasın düzgünlüyünə şübhə edirsinizsə — kompozisiyanı seçin.
Mobil proqramlaşdırmada kompozisiya tez-tez dependensiyaların enjeksiyonu vasitəsilə istifadə olunur: baza sinfindən davranışı miras almaq əvəzinə, sinif onu konstruktor vasitəsilə alır. ViewModel Repository-dən miras almır, onu dependensiya kimi qəbul edir. Bu, tərifinə görə LSP pozulmasını istisna edir — miras yoxdur, kontrakt pozulması da yoxdur.
Mirasın kompozisiya ilə əvəz edilməli olduğunu göstərən əlamətlər: alt sinif baza sinfinin metodlarının bir hissəsini istifadə etmir, alt sinif metodları boş stublarla dəyişdirir, müştəri kodu instanceof vasitəsilə obyektin tipini yoxlayır. Bu hallarda miras səhv seçilmişdir və LSP pozulub.
İnterfeyslər LSP problemini miras olmadan həll edir: hər bir tip yalnız ehtiyac duyduğu metodları implementasiya edir. fly() metodu olan ümumi Bird baza sinfi (Penguin uçmur) əvəzinə — yalnız uçan quşların implementasiya etdiyi Flyable interfeysi. Penguin fly() metodu olmadan Bird-i implementasiya edir — LSP pozulmayıb.
Android arxitekturasında bu yanaşma seqreqə edilmiş interfeyslər UseCase vasitəsilə tətbiq olunur: getAll, getById, save, delete metodları olan bir böyük UseCase əvəzinə — ayrıca GetItemsUseCase, SaveItemUseCase interfeysləri. Müştəri yalnız lazım olan interfeysdən asılıdır və bu interfeysi implementasiya edən hər bir sinif LSP baxımından düzgündür.
Tez-tez verilən suallar
Miras — dil mexanizmi, LSP — bu mexanizmdən düzgün istifadə qaydası. Miras imzaların uyğunluğuna (sintaksis) zəmanət verir, LSP davranış uyğunluğu (semantika) tələb edir. LSP olmadan miras iş vaxtı pozulan polimorfizm verir.
Əgər baza sinfi non-null qaytarışına zəmanət verirsə — bəli. Kontrakt null-a (opsional dəyər) icazə verirsə — yox. LSP null-u qadağan etmir, kontraktın zəiflədilməsini qadağan edir. Baza sinfinin sənədlərini öyrənin və alt sinfin kontraktının uyğun olub-olmadığını yoxlayın.
Protokollara LSP siniflərə olduğu kimi tətbiq olunur. Protokolun implementasiyası semantik kontrakta riayət etməlidir: protokol metodu non-throwing kimi təyin edirsə, implementasiya səhv atmamalıdır. Swift bunu kompilyasiya səviyyəsində yoxlamır — məsuliyyət proqramçının üzərindədir.
Kotlin-də Sealed class — xüsusi haldır, çünki iyerarxiya qapalıdır və kompilyatora məlumdur. LSP sealed class-a daha az tətbiq olunur, çünki bütün alt tiplər when ifadəsində açıq şəkildə sadalanır. Sealed-alt sinfin səhvi lokal olacaq, gizli polimorf səhv olmayacaq.
Baza sinfi üçün onun bütün alt siniflərində işə düşən parametrləşdirilmiş test yazın. Test əsas davranış kontraktlarını yoxlayır: qaytarılan dəyərlər, istisnalar, vəziyyətlər. Test alt siniflərdən birində uğursuz olarsa — LSP pozulub. CI-da belə test polimorf kodun reqressiyasının qarşısını alır.
Nəticə
Açar təslim mobil tətbiq hazırlayacağıq
IT Sectr 2017-ci ildən startaplar və bizneslər üçün iOS və Android tətbiqləri yaradır. Sizə məsləhət verəcəyik və ən yaxşı həlli təklif edəcəyik.
Həm də oxuyun