LSP: суштина, принцип супституције Барбаре Лисков у развоју

Аутор: IT Sectr Објављено: 2026-05-12 Време читања: 9 мин

LSP (Liskov Substitution Principle) — трећи принцип SOLID-а који одређује услове исправног наслеђивања у објектно-оријентисаном програмирању. Принцип је формулисала Барбара Лисков 1987. године и формализован је као: ако је S подтип од T, онда објекти T могу бити замењени објектима S без промене својстава програма. Како се наводи у књизи Роберта Мартина Clean Architecture (2017), принцип супституције захтева да подкласа не слаби уговор базне класе.

Главно

  • LSP — принцип супституције Лисков, трећи принцип SOLID-а о исправном наслеђивању
  • Подкласа мора да задржи уговор базне класе — предуслове и послеуслове
  • Кршење LSP се манифестује у проблему квадрата и правоугаоника и баченим изузецима
  • Композиција је често пожељнија од наслеђивања за поштовање LSP
  • Пројектовање по уговору (Design by Contract) — формални начин провере LSP

Шта је LSP (Liskov Substitution Principle)?

LSP (Liskov Substitution Principle) — принцип супституције који је формулисала Барбара Лисков на конференцији OOPSLA 1987. године. Формална дефиниција: нека је q(x) доказиво својство објеката x типа T. Тада q(y) мора бити доказиво за објекте y типа S, где је S подтип од T. Једноставније речено: објекти подкласе морају да се понашају тако да код који ради са базном класом настави исправно да ради и са подкласом.

У пракси LSP значи да подкласа не сме да крши уговор базне класе. Уговор укључује предуслове (шта је потребно за позив метода), послеуслове (шта се гарантује након позива) и инваријанте (услови који се одржавају током живота објекта). Подкласа може да ојача предуслове или ослаби послеуслове — то је кршење LSP.

Класичан пример кршења LSP — квадрат који наслеђује од правоугаоника. Метод setWidth код правоугаоника поставља ширину, код квадрата — и ширину и висину. Клијент који очекује понашање правоугаоника (промена једне стране не утиче на другу) добија неочекивани резултат. Квадрат није исправан подтип правоугаоника.

Формални услови LSP

LSP поставља три услова за исправно наслеђивање: предуслови подкласе не могу бити јачи од предуслова базне класе (подкласа не захтева више), послеуслови подкласе не могу бити слабији од послеуслова базне класе (подкласа гарантује не мање), инваријанте базне класе морају бити очуване у подкласи. Ови услови су познати као правило пројектовања по уговору Бертрана Мејера.

Ако је бар један услов прекршен — код који користи полиморфизам може да ради неисправно. Компајлер не проверава семантичке уговоре, само синтаксне. Зато је LSP питање архитектонске дисциплине, а не статичког типизовања.

Како ради принцип супституције Лисков

Механизам LSP се заснива на бихејвиоралној компатибилности типова. Ако класа S наслеђује од класе T, клијентски код мора моћи да користи S свуда где се очекује T, без промене свог понашања. Ово укључује не само сигнатуре метода, већ и њихову семантику.

LSP не забрањује подкласи да додаје ново понашање. Забрањено је кршење очекивања кода написаног за базну класу. Ако базна класа гарантује да метод save не баца изузетке, подкласа не сме да их баца. Ако базна класа враћа ненегативну вредност, подкласа не сме да враћа негативну.

У реалним пројектима LSP се најчешће крши додавањем условне логике у методе подкласе: «ако услов — баци изузетак», «ако услов — врати null». Свако такво «изненађење» нарушава полиморфизам и приморава клијентски код да проверава тип објекта пре позива — што је у супротности са самом идејом објектно-оријентисаног пројектовања.

У мобилним пројектима типично кршење LSP се јавља при креирању базних ViewModel. Ако BaseViewModel гарантује да метод onCleared ослобађа све ресурсе, а подкласа преписује овај метод као празан — било који код који се ослања на ослобађање ресурса путем полиморфног позива onCleared радиће неисправно. LSP захтева да подкласа или позове super.onCleared(), или сама обави исти посао. Композиција путем LifecycleObserver — алтернатива која искључује кршење LSP при управљању животним циклусом.

Знаци кршења LSP у коду

Главни индикатори кршења LSP укључују: проверу типа објекта путем instanceof или is пре позива метода, празне имплементације метода (стубове), бацање изузетка NotImplementedError или UnsupportedOperationException, враћање null уместо вредности. Сваки од ових образаца сигнализира да подкласа није исправан подтип.

Још један чест знак — наслеђивање ради поновног коришћења кода, а не ради моделирања односа «јесте» (is-a). Класа Bird има метод fly(). Класа Penguin наслеђује од Bird и преписује fly() као празан или који баца изузетак. Ово је кршење LSP: пингвин није исправан подтип птице.

У мобилном развоју LSP се крши при креирању базних ViewHolder, Fragment или ViewController са stub методима. Ако подкласа не користи половину метода базне класе — наслеђивање је погрешно изабрано. Композиција или раздвајање интерфејса решавају проблем исправније.

Тест за LSP

Једноставан тест за проверу LSP: напишите unit-тест за базну класу који проверава њен уговор (повратне вредности, изузеци, споредни ефекти). Покрените овај тест за сваку подкласу. Ако тест падне — LSP је прекршен. Овај приступ се назива «тестирање кроз уговор базне класе».

У Android пројектима такав тест је користан за ViewModel и Repository. Ако BaseViewModel гарантује стање Loading пре грешке, а подкласа баца грешку без Loading — тест ће забележити кршење LSP у фази CI.

Примери LSP у мобилном развоју

Размотримо Android пример са обрадом ClickListener. Кршење LSP настаје када базна имплементација нешто гарантује, а подкласа то крши.

kotlin
// Базна класа са гаранцијом: onClick ће бити позван
open class BaseClickListener {
    open fun onClick(view: View) {
        // базна обрада
    }
}

// Кршење LSP: подкласа додаје услов који баца изузетак
class RestrictedClickListener : BaseClickListener() {
    override fun onClick(view: View) {
        if (!isLoggedIn) {
            throw IllegalStateException("Not logged in")
        }
        super.onClick(view)
    }
}

// Исправно решење: уговор није прекршен
class ConditionalClickListener : BaseClickListener() {
    override fun onClick(view: View) {
        if (isLoggedIn) {
            super.onClick(view)
        }
    }
}

iOS пример са протоколом DataSource демонстрира кршење LSP кроз враћање nil уместо података:

swift
// Протокол са уговором: враћа податке или грешку
protocol DataProvider {
    func fetchData() async throws -> [String]
}

// Кршење LSP: враћа nil без грешке
class SilentFailProvider: DataProvider {
    func fetchData() async throws -> [String] {
        return [] // празан низ уместо грешке
    }
}

// Исправно поштовање LSP
class NetworkProvider: DataProvider {
    func fetchData() async throws -> [String] {
        throw NetworkError.timeout
    }
}

Практично правило: ако подкласа не може да испуни уговор базне класе — не треба да буде подкласа. Алтернатива — издвојити интерфејс са минималним уговором и имплементирати га у сваком типу на свој начин.

LSP и наслеђивање: када изабрати композицију

Композиција је пожељнија од наслеђивања у ситуацијама где је однос «јесте» (is-a) двосмислен или услован. Класичан пример: Manager је Employee? Да. Али да ли је Square исправан Rectangle? LSP каже «не». Ако сумњате у исправност наслеђивања — изаберите композицију.

У мобилном развоју композиција се често користи кроз убризгавање зависности: уместо да наслеђује понашање од базне класе, класа га добија кроз конструктор. ViewModel не наслеђује од Repository, већ га прима као зависност. Ово искључује кршење LSP по дефиницији — нема наслеђивања, нема кршења уговора.

Знаци да наслеђивање треба заменити композицијом: подкласа не користи део метода базне класе, подкласа преписује методе празним stub-овима, клијентски код проверава тип објекта кроз instanceof. У овим случајевима наслеђивање је погрешно изабрано и LSP је прекршен.

Решење кроз интерфејсе

Интерфејси решавају проблем LSP без наслеђивања: сваки тип имплементира тачно оне методе које су му потребне. Уместо заједничке базне класе Bird са методом fly (где Penguin не лети) — интерфејс Flyable, који имплементирају само птице које лете. Penguin имплементира Bird без метода fly — LSP није прекршен.

У Android архитектури овај приступ се примењује кроз сегрегиране интерфејсе UseCase: уместо једног великог UseCase са методима getAll, getById, save, delete — одвојени интерфејси GetItemsUseCase, SaveItemUseCase. Клијент зависи само од потребног интерфејса и свака класа која имплементира тај интерфејс је исправна са становишта LSP.

Често постављана питања

По чему се LSP разликује од обичног наслеђивања?

Наслеђивање — механизам језика, LSP — правило исправног коришћења тог механизма. Наслеђивање гарантује компатибилност сигнатура (синтакса), LSP захтева компатибилност понашања (семантика). Наслеђивање без LSP даје полиморфизам који се ломи у runtime-у.

Да ли null у подкласи увек крши LSP?

Ако базна класа гарантује non-null повратак — да. Ако уговор дозвољава null (опциона вредност) — не. LSP не забрањује null, забрањује слабљење уговора. Проучите документацију базне класе и проверите да ли је уговор подкласе компатибилан.

Како се LSP примењује на протоколе у Swift-у?

На протоколе LSP се примењује исто као на класе. Имплементација протокола мора да поштује семантички уговор: ако протокол дефинише метод као non-throwing, имплементација не сме да баца грешке. Swift ово не проверава на нивоу компилације — одговорност је на програмеру.

Може ли LSP бити прекршен при коришћењу sealed class?

Sealed class у Kotlin-у — посебан случај, јер је хијерархија затворена и позната компајлеру. LSP се у мањој мери примењује на sealed class, јер су сви подтипови експлицитно наведени у when изразу. Грешка sealed-подкласе биће локална, а не скривена полиморфна грешка.

Како тестирати поштовање LSP у пројекту?

Напишите параметризовани тест за базну класу који се покреће за све њене подкласе. Тест проверава кључне бихејвиоралне уговоре: повратне вредности, изузетке, стања. Ако тест падне на једној од подкласа — LSP је прекршен. У CI-ју такав тест спречава регресију полиморфног кода.

Завршни преглед

  • LSP (Liskov Substitution Principle) — принцип супституције, трећи у SOLID-у, о семантичкој компатибилности наслеђивања
  • Подкласа мора да задржи уговор базне класе: предуслове, послеуслове и инваријанте
  • Провера instanceof и празна преписивања метода — главни знаци кршења LSP
  • Композиција и интерфејси решавају проблем LSP тамо где је наслеђивање неправилно
  • Проблем квадрата и правоугаоника — класичан пример некомпатибилности подтипова
  • Тест уговора за базну класу, покренут за све подкласе, открива кршење LSP у CI-ју
  • Sealed class у Kotlin-у смањује ризике LSP захваљујући затвореној хијерархији познатој компајлеру

Развићемо мобилну апликацију под кључ

IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.

Разговарајте о пројекту

Прочитајте такође