LSP: diwa, prinsipyo ng pagpapalit ni Barbara Liskov sa pag-develop

May-akda: IT Sectr Nai-publish: 2026-05-12 Oras ng pagbabasa: 9 min

LSP (Liskov Substitution Principle) — ang ikatlong prinsipyo ng SOLID na tumutukoy sa mga kondisyon ng tamang inheritance sa object-oriented programming. Ang prinsipyo ay binuo ni Barbara Liskov noong 1987 at pinormalisa bilang: kung ang S ay subtype ng T, ang mga bagay ng T ay maaaring palitan ng mga bagay ng S nang hindi binabago ang mga katangian ng programa. Gaya ng nabanggit sa aklat ni Robert Martin Clean Architecture (2017), ang prinsipyo ng pagpapalit ay nangangailangan na ang subclass ay hindi magpahina sa kontrata ng base class.

Mga pangunahing punto

  • LSP — prinsipyo ng pagpapalit ng Liskov, ikatlong prinsipyo ng SOLID tungkol sa tamang inheritance
  • Subclass ay dapat panatilihin ang kontrata ng base class — mga precondition at postcondition
  • Paglabag sa LSP ay makikita sa problema ng parisukat at parihaba at mga itinapon na exception
  • Komposisyon ay madalas mas mainam kaysa inheritance para sa pagsunod sa LSP
  • Design by Contract — pormal na paraan ng pagsusuri ng LSP

Ano ang LSP (Liskov Substitution Principle)?

LSP (Liskov Substitution Principle) — prinsipyo ng pagpapalit na binuo ni Barbara Liskov sa OOPSLA conference noong 1987. Pormal na depinisyon: hayaan ang q(x) na isang mapapatunayang katangian ng mga bagay x ng uri T. Kung gayon, ang q(y) ay dapat mapatunayan para sa mga bagay y ng uri S, kung saan ang S ay subtype ng T. Sa madaling salita: ang mga bagay ng subclass ay dapat kumilos sa paraang ang code na gumagana sa base class ay patuloy na gumagana nang tama sa subclass.

Sa praktika, ang LSP ay nangangahulugan na ang subclass ay hindi dapat lumabag sa kontrata ng base class. Kasama sa kontrata ang mga precondition (kung ano ang kinakailangan upang tumawag ng method), postcondition (kung ano ang garantisado pagkatapos ng tawag), at invariants (mga kondisyon na nananatili sa buong buhay ng bagay). Ang subclass ay maaaring palakasin ang mga precondition o pahinain ang mga postcondition — ito ang paglabag sa LSP.

Klasikong halimbawa ng paglabag sa LSP — parisukat na nagmana mula sa parihaba. Ang method setWidth sa parihaba ay nagtatakda ng lapad, sa parisukat — parehong lapad at taas. Ang client na umaasa sa pag-uugali ng parihaba (ang pagbabago ng isang gilid ay hindi nakakaapekto sa isa) ay nakakakuha ng hindi inaasahang resulta. Ang parisukat ay hindi tamang subtype ng parihaba.

Mga pormal na kondisyon ng LSP

Nagtatakda ang LSP ng tatlong kondisyon para sa tamang inheritance: ang mga precondition ng subclass ay hindi maaaring mas malakas kaysa sa mga precondition ng base class (ang subclass ay hindi humihingi ng higit), ang mga postcondition ng subclass ay hindi maaaring mas mahina kaysa sa mga postcondition ng base class (ang subclass ay hindi garantisado ng mas kaunti), ang mga invariant ng base class ay dapat panatilihin sa subclass. Ang mga kondisyong ito ay kilala bilang tuntunin ng design by contract ni Bertrand Meyer.

Kung hindi bababa sa isang kondisyon ang nilabag — ang code na gumagamit ng polymorphism ay maaaring magkaroon ng problema. Hindi sinusuri ng compiler ang mga semantikong kontrata, tanging ang mga sintaktiko. Kaya ang LSP ay isang usapin ng disiplina sa arkitektura, hindi ng static na pag-type.

Paano gumagana ang prinsipyo ng pagpapalit ng Liskov

Ang mekanismo ng LSP ay batay sa pagiging tugma ng pag-uugali ng mga uri. Kung ang klase S ay nagmana mula sa klase T, ang client code ay dapat magamit ang S kahit saan inaasahan ang T, nang hindi binabago ang pag-uugali nito. Kasama rito hindi lamang ang mga signature ng method, kundi pati na rin ang kanilang semantika.

Hindi ipinagbabawal ng LSP ang subclass na magdagdag ng bagong pag-uugali. Ang paglabag sa mga inaasahan ng code na isinulat para sa base class ay ipinagbabawal. Kung ginagarantiyahan ng base class na ang method save ay hindi nagtatapon ng exception, hindi dapat itapon ng subclass ang mga ito. Kung ang base class ay nagbabalik ng hindi-negatibong halaga, ang subclass ay hindi dapat magbalik ng negatibo.

Sa totoong mga proyekto, ang LSP ay madalas na nilalabag sa pamamagitan ng pagdaragdag ng conditional logic sa mga method ng subclass: “kung kondisyon — magtapon ng exception”, “kung kondisyon — magbalik ng null”. Ang bawat ganitong “sorpresa” ay sumisira sa polymorphism at pinipilit ang client code na suriin ang uri ng bagay bago tumawag — na sumasalungat sa ideya ng object-oriented na disenyo.

Sa mga mobile project, ang tipikal na paglabag sa LSP ay nangyayari kapag gumagawa ng base na ViewModel. Kung ginagarantiyahan ng BaseViewModel na ang method onCleared ay naglalabas ng lahat ng resources, at ang subclass ay nag-o-override sa method na ito bilang walang laman — anumang code na umaasa sa paglabas ng resources sa pamamagitan ng polymorphic na tawag sa onCleared ay gagana nang hindi tama. Hinihiling ng LSP na ang subclass ay tumawag ng super.onCleared() o gawin ang parehong gawain mismo. Komposisyon sa pamamagitan ng LifecycleObserver — alternatibo na hindi kasama ang paglabag sa LSP sa pamamahala ng lifecycle.

Mga palatandaan ng paglabag sa LSP sa code

Mga pangunahing indikador ng paglabag sa LSP ay kinabibilangan ng: pagsusuri ng uri ng bagay sa pamamagitan ng instanceof o is bago tumawag ng method, walang laman na implementasyon ng method (stub), pagtatapon ng exception na NotImplementedError o UnsupportedOperationException, pagbabalik ng null sa halip na halaga. Ang bawat isa sa mga pattern na ito ay nagpapahiwatig na ang subclass ay hindi tamang subtype.

Isa pang karaniwang palatandaan — inheritance na may layuning muling gamitin ang code, hindi upang imodelo ang relasyong “ay” (is-a). Ang klase Bird ay may method fly(). Ang klase Penguin ay nagmana mula sa Bird at nag-override sa fly() bilang walang laman o nagtatapon ng exception. Ito ay paglabag sa LSP: ang penguin ay hindi tamang subtype ng ibon.

Sa mobile development, ang LSP ay nilalabag kapag gumagawa ng base na ViewHolder, Fragment o ViewController na may mga stub method. Kung ang subclass ay hindi gumagamit ng kalahati ng mga method ng base class — ang inheritance ay napili nang hindi tama. Komposisyon o paghihiwalay ng interface ay mas tamang malutas ang problema.

Test para sa LSP

Simpleng test para suriin ang LSP: sumulat ng unit test para sa base class na sumusuri sa kontrata nito (mga return value, exception, side effects). Patakbuhin ang test na ito para sa bawat subclass. Kung nabigo ang test — nilabag ang LSP. Ang approach na ito ay tinatawag na “pagsubok sa pamamagitan ng kontrata ng base class”.

Sa mga Android project, ang ganitong test ay kapaki-pakinabang para sa ViewModel at Repository. Kung ginagarantiyahan ng BaseViewModel ang status na Loading bago ang error, at ang subclass ay nagtatapon ng error nang walang Loading — itatala ng test ang paglabag sa LSP sa CI stage.

Mga halimbawa ng LSP sa mobile development

Tingnan natin ang halimbawa ng Android sa paghawak ng ClickListener. Ang paglabag sa LSP ay nangyayari kapag ang base implementasyon ay may ginagarantiyahan at nilalabag ito ng subclass.

kotlin
// Base class na may garantiya: tatawagin ang onClick
open class BaseClickListener {
    open fun onClick(view: View) {
        // base processing
    }
}

// Paglabag sa LSP: nagdagdag ng condition ang subclass na nagtatapon ng exception
class RestrictedClickListener : BaseClickListener() {
    override fun onClick(view: View) {
        if (!isLoggedIn) {
            throw IllegalStateException("Not logged in")
        }
        super.onClick(view)
    }
}

// Tamang solusyon: hindi nilabag ang kontrata
class ConditionalClickListener : BaseClickListener() {
    override fun onClick(view: View) {
        if (isLoggedIn) {
            super.onClick(view)
        }
    }
}

Ang halimbawa ng iOS na may DataSource protocol ay nagpapakita ng paglabag sa LSP sa pamamagitan ng pagbabalik ng nil sa halip na data:

swift
// Protocol na may kontrata: nagbabalik ng data o error
protocol DataProvider {
    func fetchData() async throws -> [String]
}

// Paglabag sa LSP: nagbabalik ng nil nang walang error
class SilentFailProvider: DataProvider {
    func fetchData() async throws -> [String] {
        return [] // walang laman na array sa halip na error
    }
}

// Tamang pagsunod sa LSP
class NetworkProvider: DataProvider {
    func fetchData() async throws -> [String] {
        throw NetworkError.timeout
    }
}

Praktikal na tuntunin: kung hindi matupad ng subclass ang kontrata ng base class — hindi ito dapat maging subclass. Alternatibo — kumuha ng interface na may minimal na kontrata at ipatupad ito sa bawat uri sa sarili nitong paraan.

LSP at inheritance: kailan pumili ng komposisyon

Komposisyon ay mas mainam kaysa inheritance sa mga sitwasyon kung saan ang relasyong “ay” (is-a) ay hindi malinaw o may kondisyon. Klasikong halimbawa: Ang Manager ay Employee? Oo. Ngunit ang Square ba ay tamang Rectangle? Sinasabi ng LSP na “hindi”. Kung nagdududa ka sa tamang inheritance — pumili ng komposisyon.

Sa mobile development, ang komposisyon ay madalas gamitin sa pamamagitan ng dependency injection: sa halip na manahin ang pag-uugali mula sa base class, natatanggap ito ng klase sa pamamagitan ng constructor. Ang ViewModel ay hindi nagmamana mula sa Repository, tinatanggap ito bilang dependency. Ibinubukod nito ang paglabag sa LSP sa pamamagitan ng depinisyon — walang inheritance, walang paglabag sa kontrata.

Mga palatandaan na ang inheritance ay dapat palitan ng komposisyon: ang subclass ay hindi gumagamit ng bahagi ng mga method ng base class, ang subclass ay nag-o-override ng mga method na may walang laman na stub, ang client code ay sumusuri sa uri ng bagay sa pamamagitan ng instanceof. Sa mga kasong ito, ang inheritance ay napili nang hindi tama at ang LSP ay nilabag.

Solusyon sa pamamagitan ng mga interface

Mga interface ay lumulutas sa problema ng LSP nang walang inheritance: bawat uri ay nagpapatupad ng eksaktong mga method na kailangan nito. Sa halip na isang karaniwang base class na Bird na may method fly (kung saan ang Penguin ay hindi lumilipad) — ang interface Flyable, na ipinapatupad lamang ng mga ibong lumilipad. Ang Penguin ay nagpapatupad ng Bird nang walang method fly — ang LSP ay hindi nilalabag.

Sa arkitektura ng Android, ang approach na ito ay inilalapat sa pamamagitan ng mga segregated interface UseCase: sa halip na isang malaking UseCase na may mga method na getAll, getById, save, delete — magkahiwalay na interface na GetItemsUseCase, SaveItemUseCase. Ang client ay nakadepende lamang sa kinakailangang interface at ang bawat klase na nagpapatupad ng interface na iyon ay tama mula sa pananaw ng LSP.

Mga madalas itanong

Ano ang pagkakaiba ng LSP sa ordinaryong inheritance?

Inheritance — mekanismo ng wika, LSP — tuntunin ng tamang paggamit ng mekanismong ito. Ginagarantiyahan ng inheritance ang pagiging tugma ng mga signature (syntax), hinihingi ng LSP ang pagiging tugma ng pag-uugali (semantika). Ang inheritance na walang LSP ay nagbibigay ng polymorphism na nasisira sa runtime.

Ang null ba sa subclass ay palaging lumalabag sa LSP?

Kung ang base class ay ginagarantiyahan ang non-null na pagbabalik — oo. Kung pinapayagan ng kontrata ang null (opsyonal na halaga) — hindi. Hindi ipinagbabawal ng LSP ang null, ipinagbabawal nito ang pagpapahina ng kontrata. Pag-aralan ang dokumentasyon ng base class at suriin kung ang kontrata ng subclass ay tugma.

Paano inilalapat ang LSP sa mga protocol sa Swift?

Sa mga protocol ang LSP ay inilalapat tulad ng sa mga klase. Ang implementasyon ng protocol ay dapat sumunod sa semantikong kontrata: kung tinukoy ng protocol ang isang method bilang non-throwing, ang implementasyon ay hindi dapat magtapon ng error. Hindi ito sinusuri ng Swift sa antas ng compilation — ang responsibilidad ay nasa developer.

Maaari bang labagin ang LSP kapag gumagamit ng sealed class?

Ang Sealed class sa Kotlin — espesyal na kaso, dahil ang hierarchy ay sarado at alam ng compiler. Ang LSP ay nalalapat sa mas mababang antas sa sealed class, dahil ang lahat ng subtype ay malinaw na nakalista sa when expression. Ang error ng sealed-subclass ay magiging lokal, hindi isang nakatagong polymorphic error.

Paano subukan ang pagsunod sa LSP sa isang proyekto?

Sumulat ng parameterized test para sa base class na tumatakbo para sa lahat ng mga subclass nito. Sinusuri ng test ang mga pangunahing kontrata ng pag-uugali: mga return value, exception, estado. Kung nabigo ang test sa isa sa mga subclass — nilabag ang LSP. Sa CI, ang ganitong test ay pumipigil sa regression ng polymorphic code.

Buod

  • LSP (Liskov Substitution Principle) — prinsipyo ng pagpapalit, ikatlo sa SOLID, tungkol sa semantikong pagiging tugma ng inheritance
  • Subclass ay dapat panatilihin ang kontrata ng base class: mga precondition, postcondition, at invariants
  • Pagsusuri ng instanceof at walang laman na pag-override ng method — pangunahing palatandaan ng paglabag sa LSP
  • Komposisyon at mga interface ay lumulutas sa problema ng LSP kung saan hindi tama ang inheritance
  • Problema ng parisukat at parihaba — klasikong halimbawa ng hindi pagkakatugma ng subtype
  • Test ng kontrata para sa base class, tumatakbo para sa lahat ng subclass, nakakatuklas ng paglabag sa LSP sa CI
  • Sealed class sa Kotlin ay nagbabawas ng mga panganib sa LSP dahil sa saradong hierarchy na alam ng compiler

Gagawa kami ng mobile application na turnkey

Gumagawa ang IT Sectr ng mga iOS at Android application para sa mga startup at negosyo mula noong 2017. Magpapayo kami sa iyo at magmumungkahi ng pinakamahusay na solusyon.

Pag-usapan ang proyekto

Basahin din