Načelo zamjene Liskova u Javi

1. Pregled

Načela SOLID dizajna uveo je Robert C. Martin u svom radu iz 2000. godine, Principi dizajna i uzorci dizajna. Čvrsti principi dizajna pomažu nam stvoriti održiviji, razumljiviji i fleksibilniji softver.

U ovom ćemo članku razgovarati o načelu zamjene Liskova, što je "L" u kratici.

2. Otvoreni / zatvoreni princip

Da bismo razumjeli Princip zamjene Liskova, prvo moramo razumjeti Otvoreni / Zatvoreni princip ("O" iz ČVRSTOG).

Cilj načela otvoreno / zatvoreno potiče nas da dizajniramo svoj softver tako da i mi dodajte nove značajke samo dodavanjem novog koda. Kada je to moguće, slabo smo spojili, a samim tim i lako održive aplikacije.

3. Primjer upotrebe

Pogledajmo primjer bankarske aplikacije kako bismo još bolje razumjeli otvoreni / zatvoreni princip.

3.1. Bez otvorenog / zatvorenog principa

Naša bankarska aplikacija podržava dvije vrste računa - "tekući" i "štedni". Oni su predstavljeni razredima Trenutni račun i Štedni račun odnosno.

The BankingAppWithdrawalService pruža uslugu povlačenja za svoje korisnike:

Nažalost, postoji problem s proširivanjem ovog dizajna. The BankingAppWithdrawalService svjestan je dviju konkretnih provedbi računa. Stoga je BankingAppWithdrawalService treba mijenjati svaki put kad se uvede nova vrsta računa.

3.2. Korištenje načela otvoreno / zatvoreno da bi se kôd proširio

Preoblikujmo rješenje u skladu s načelom otvoreno / zatvoreno. Zatvorit ćemo BankingAppWithdrawalService od izmjene kada su potrebne nove vrste računa, pomoću Račun osnovna klasa umjesto:

Ovdje smo uveli novi sažetak Račun razred koji Trenutni račun i Štedni račun produžiti.

The BankingAppWithdrawalService više ne ovisi o konkretnim klasama računa. Budući da to sada ovisi samo o apstraktnoj klasi, ne treba ga mijenjati kada se uvede nova vrsta računa.

Slijedom toga, BankingAppWithdrawalService je otvoren za produžetak s novim vrstama računa, ali zatvoreno za preinaku, jer novi tipovi ne zahtijevaju promjenu kako bi se integrirali.

3.3. Java kod

Pogledajmo ovaj primjer u Javi. Za početak, definirajmo Račun razred:

javni sažetak klase Račun {zaštićeni sažetak nevažeći depozit (BigDecimalni iznos); / ** * Smanjuje stanje na računu za navedeni iznos * ako je zadani iznos> 0 i račun udovoljava minimalnim dostupnim * kriterijima stanja. * * @param iznos * / zaštićeni sažetak poništavanje povlačenja (BigDecimalni iznos); } 

I, definirajmo BankingAppWithdrawalService:

javna klasa BankingAppWithdrawalService {račun privatnog računa; javni BankingAppWithdrawalService (račun računa) {this.account = account; } javno prazno povlačenje (BigDecimalni iznos) {account.withdraw (iznos); }}

Sada, pogledajmo kako bi u ovom dizajnu novi tip računa mogao kršiti načelo zamjene Liskova.

3.4. Nova vrsta računa

Banka sada želi svojim klijentima ponuditi oročeni depozitni račun s visokom kamatom.

Da bismo to podržali, predstavimo novo FixedTermDepositAccount razred. Račun s oročenim depozitom u stvarnom svijetu "je" vrsta računa. To podrazumijeva nasljeđivanje u našem objektno orijentiranom dizajnu.

Pa, napravimo FixedTermDepositAccount podrazred od Račun:

javna klasa FixedTermDepositAccount proširuje račun {// Nadjačane metode ...}

Zasada je dobro. Međutim, banka ne želi dopustiti podizanje sredstava na račune oročenih depozita.

To znači da je novo FixedTermDepositAccount razred ne može smisleno pružiti povući metoda koja Račun definira. Jedno uobičajeno rješenje za to je napraviti FixedTermDepositAccount baciti an UnsupportedOperationException u metodi koju ne može ispuniti:

javna klasa FixedTermDepositAccount proširuje račun {@Override zaštićen nevažeći depozit (BigDecimalni iznos) {// Polog na ovaj račun} @Override zaštićen void povlačenje (BigDecimalni iznos) {throw new UnsupportedOperationException ("FixedTermDepositAccount ne podržava isplate;) }}

3.5. Testiranje pomoću nove vrste računa

Iako nova klasa dobro funkcionira, pokušajmo je koristiti s BankingAppWithdrawalService:

Račun myFixedTermDepositAccount = novi FixedTermDepositAccount (); myFixedTermDepositAccount.deposit (novi BigDecimal (1000,00)); BankingAppWithdrawalService povlačenjeService = novo BankingAppWithdrawalService (myFixedTermDepositAccount); povlačenjeService.withdraw (novi BigDecimal (100,00));

Ne iznenađuje što se bankarska aplikacija ruši s pogreškom:

Isplata sredstava ne podržava FixedTermDepositAccount !!

Očito je da nešto nije u redu s ovim dizajnom ako valjana kombinacija objekata dovede do pogreške.

3.6. Što je pošlo po zlu?

The BankingAppWithdrawalService je klijent tvrtke Račun razred. Očekuje da oboje Račun i njegovi podtipovi jamče ponašanje koje Račun razred je odredio za svoj povući metoda:

/ ** * Smanjuje stanje na računu za navedeni iznos * ako je zadani iznos> 0 i račun ispunjava minimalne dostupne * kriterije stanja. * * @param iznos * / zaštićeni sažetak poništavanje povlačenja (BigDecimalni iznos);

Međutim, ne podržavajući povući metoda, FixedTermDepositAccount krši ovu specifikaciju metode. Stoga ne možemo pouzdano zamijeniti FixedTermDepositAccount za Račun.

Drugim riječima, FixedTermDepositAccount je prekršio načelo zamjene Liskova.

3.7. Ne možemo li riješiti pogrešku BankingAppWithdrawalService?

Mogli bismo izmijeniti dizajn tako da klijent tvrtke Račun‘S povući metoda mora biti svjesna moguće pogreške u pozivanju. Međutim, to bi značilo da klijenti moraju imati posebna znanja o neočekivanom ponašanju podtipa. Ovo počinje kršiti načelo otvoreno / zatvoreno.

Drugim riječima, da bi otvoreni / zatvoreni princip dobro funkcionirao podtipovi moraju biti zamjenjivi svojim nadtipom, a da nikada ne moraju mijenjati klijentski kod. Pridržavanje principa zamjene Liskova osigurava tu zamjenjivost.

Pogledajmo sada detaljno Princip zamjene Liskova.

4. Načelo zamjene Liskova

4.1. Definicija

Robert C. Martin rezimira:

Podtipovi moraju biti zamjenjivi za svoje osnovne tipove.

Barbara Liskov, definirajući ga 1988., pružila je matematičku definiciju:

Ako za svaki objekt o1 tipa S postoji objekt o2 tipa T takav da je za sve programe P definirane u smislu T, ponašanje P nepromijenjeno kada je o1 zamijenjen za o2, tada je S podtip T.

Shvatimo ove definicije malo više.

4.2. Kada je podtip zamjenjiv za svoj supertip?

Podtip ne postaje automatski zamjenjiv za svoj supertip. Da bi mogla biti zamjenjiva, podtip se mora ponašati poput svog supertipa.

Ponašanje objekta je ugovor na koji se mogu osloniti njegovi klijenti. Ponašanje je specificirano javnim metodama, svim ograničenjima koja se postavljaju na njihove ulaze, bilo kojim promjenama stanja kroz koje objekt prolazi i nuspojavama od izvršavanja metoda.

Podtipiziranje u Javi zahtijeva svojstva i metode osnovne klase dostupne u potklasi.

Međutim, podtipiranje ponašanja znači da podtip ne samo da pruža sve metode u nadtipu, već mora se pridržavati specifikacije ponašanja za supertip. To osigurava da podtipa ispunjava sve pretpostavke klijenata o ponašanju supertipa.

To je dodatno ograničenje koje načelo zamjene Liskova donosi objektno orijentiranom dizajnu.

Idemo sada refaktorizirati našu bankovnu aplikaciju kako bismo riješili probleme s kojima smo se ranije susreli.

5. Refaktoriranje

Da bismo riješili probleme koje smo pronašli u primjeru bankarstva, krenimo od razumijevanja osnovnog uzroka.

5.1. Korijenski uzrok

U primjeru, naš FixedTermDepositAccount nije bio podvrsta ponašanja za Račun.

Dizajn Račun pogrešno pretpostavio da svi Račun vrste omogućuju podizanje novca. Slijedom toga, sve podvrste Račun, uključujući FixedTermDepositAccount koja ne podržava povlačenje novca, naslijedila je povući metoda.

Iako bismo to mogli zaobići produženjem ugovora od Račun, postoje alternativna rješenja.

5.2. Revidirani dijagram razreda

Dizajnirajmo hijerarhiju računa drugačije:

Budući da svi računi ne podržavaju povlačenja, premjestili smo povući metoda iz Račun razreda na novu apstraktnu potklasu Povučeni račun. Oba Trenutni račun i Štedni račun dopustiti povlačenje. Tako su sada napravljene potklase novog Povučeni račun.

To znači BankingAppWithdrawalService mogu se pouzdati u pravu vrstu računa koja će pružiti povući funkcija.

5.3. Refaktorirano BankingAppWithdrawalService

BankingAppWithdrawalService sada treba koristiti Povučeni račun:

javna klasa BankingAppWithdrawalService {private WithdrawableAccount povlačenje; javni BankingAppWithdrawalService (WithdrawableAccount povlačljivi račun) {this.withdrawableAccount = povlačenje računa; } javno prazno povlačenje (BigDecimalni iznos) {povučiviAccount.withdraw (iznos); }}

Što se tiče FixedTermDepositAccount, zadržavamo Račun kao roditeljska klasa. Slijedom toga, nasljeđuje samo polog ponašanje koje može pouzdano ispuniti i više ne nasljeđuje povući metoda koju ne želi. Ovaj novi dizajn izbjegava probleme koje smo ranije vidjeli.

6. Pravila

Pogledajmo sada neka pravila / tehnike koja se odnose na potpise metoda, invarijante, preduvjete i postuslove koje možemo slijediti i koristiti kako bismo osigurali stvaranje dobrih podtipova.

U njihovoj knjizi Razvoj programa na Javi: apstrakcija, specifikacija i objektno orijentirani dizajn, Barbara Liskov i John Guttag grupirali su ta pravila u tri kategorije - pravilo potpisa, pravilo svojstava i pravilo metoda.

Neke od ovih praksi već se primjenjuju nadređena pravila Jave.

Ovdje bismo trebali primijetiti neku terminologiju. Široki tip je općenitiji - Objekt na primjer može značiti BILO KOJI Java objekt i širi je od, recimo, CharSequence, gdje Niz je vrlo specifičan i zato uži.

6.1. Pravilo potpisa - Vrste argumenata metode

Ovo pravilo kaže da tipovi argumenata metode podtipa mogu biti identični ili širi od tipova argumenata metode supertipa.

Pravila nadjačavanja Java metode podržavaju ovo pravilo prisiljavajući da se tipovi argumenata nadjačane metode točno podudaraju s metodom supertipa.

6.2. Pravilo potpisa - Vrste povrata

Tip povrata metode nadjačane podvrste može biti uži od tipa povratka metode supertipa. To se naziva kovarijancija vrsta povratka. Kovarijancija označava kada se podtip prihvaća umjesto supertipa. Java podržava kovarijanciju vrsta povratka. Pogledajmo primjer:

javna apstraktna klasa Foo {javna sažetak Broj generatedNumber (); // Ostale metode} 

The generirajBroj metoda u Foo ima tip povratka kao Broj. Zamijenimo sada ovu metodu vraćanjem užeg tipa Cijeli broj:

traka javne klase proširuje Foo {@Override public Integer generirajNumber () {return new Integer (10); } // Ostale metode}

Jer Cijeli broj JE Broj, klijentski kod koji očekuje Broj može zamijeniti Foo s Bar bez ikakvih problema.

S druge strane, ako je nadjačana metoda u Bar trebali vratiti širi tip od Broj, npr. Objekt, koji može uključivati ​​bilo koju podvrstu Objekt npr. a Kamion. Bilo koji klijentski kod koji se oslanjao na vrstu povrata Broj nije mogao podnijeti a Kamion!

Srećom, pravila nadjačavanja Java metode sprječavaju metodu nadjačavanja koja vraća širi tip.

6.3. Pravilo potpisa - Iznimke

Metoda podtipa može izbaciti manje ili uže (ali ne i neke dodatne ili šire) iznimke od metode supertipa.

To je razumljivo jer kada klijentski kod zamjenjuje podtip, može se nositi s metodom izbacujući manje iznimaka od metode supertipa. Međutim, ako metoda podtipa baci nove ili šire provjerene iznimke, razbila bi klijentski kôd.

Pravila koja nadjačavaju Java metodu već primjenjuju ovo pravilo za provjerene iznimke. Međutim, nadjačavanje metoda u Javi MOŽE BACATI bilo koje RuntimeException bez obzira izjašnjava li nadjačana metoda iznimku.

6.4. Invarijante klase svojstava

Invarijant klase je tvrdnja koja se odnosi na svojstva objekta koja mora biti istinita za sva valjana stanja objekta.

Pogledajmo primjer:

javni sažetak klase Car {zaštićen int limit; // nepromjenjivo: brzina <ograničenje; zaštićena brzina int; // postuslov: brzina <ograničiti zaštićenu apstraktnu prazninu accelerate (); // Ostale metode ...}

The Automobil class navodi klasu invarijantnu koja ubrzati mora uvijek biti ispod ograničiti. Pravilo o invarijantima navodi da sve metode podtipa (naslijeđene i nove) moraju održavati ili jačati invarijante klase nadtipa.

Definirajmo podrazred od Automobil koji čuva invarijantnu klasu:

javna klasa HybridCar proširuje Car {// invariant: charge> = 0; privatna int naplata; @Override // postcondition: speed <limit protected void accelerate () {// Ubrzati HybridCar osiguravajući brzinu <limit} // Ostale metode ...}

U ovom primjeru, invarijantna u Automobil je sačuvano nadjačanim ubrzati metoda u Hibridni automobil. The Hibridni automobil dodatno definira vlastiti klasni invarijant naboj> = 0, i ovo je sasvim u redu.

Suprotno tome, ako podvrsta ne sačuva invarijant klase, on razbija bilo koji klijentski kôd koji se oslanja na nadtip.

6.5. Pravilo svojstava - povijesno ograničenje

Povijesno ograničenje kaže da podrazredmetode (naslijeđene ili nove) ne bi trebale dopustiti promjene stanja koje osnovna klasa nije dopuštala.

Pogledajmo primjer:

javni sažetak klase Car {// Dopušteno postavljanje jednom u vrijeme izrade. // Vrijednost se može samo povećavati nakon toga. // Vrijednost se ne može resetirati. zaštićena int kilometraža; javni automobil (int kilometraža) {this.mileage = kilometraža; } // Ostala svojstva i metode ...}

The Automobil klasa navodi ograničenje na kilometraža imovine. The kilometraža svojstvo se može postaviti samo jednom u vrijeme stvaranja i ne može se resetirati nakon toga.

Ajmo sada definirati a Autić to se proteže Automobil:

javna klasa ToyCar produžuje automobil {reset javne praznine () {kilometraža = 0; } // Ostala svojstva i metode}

The Autić ima dodatnu metodu resetirati koji resetira kilometraža imovine. Pritom je Autić ignorirao ograničenje koje je njegov roditelj nametnuo na kilometraža imovine. To razbija bilo koji klijentski kod koji se oslanja na ograničenje. Tako, Autić nije zamjenjiv za Automobil.

Slično tome, ako osnovna klasa ima nepromjenjivo svojstvo, podrazred ne bi smio dopustiti izmjenu ovog svojstva. Zbog toga bi trebali biti nepromjenjivi razredi konačni.

6.6. Pravilo metoda - preduvjeti

Prije izvođenja metode treba biti zadovoljen preduvjet. Pogledajmo primjer preduvjeta koji se odnosi na vrijednosti parametara:

javna klasa Foo {// preduvjet: 0 <num <= 5 public void doStuff (int num) {if (num 5) {throw new IllegalArgumentException ("Ulaz izvan raspona 1-5"); } // ovdje ima malo logike ...}}

Ovdje je preduvjet za doStuff metoda navodi da num vrijednost parametra mora biti između 1 i 5. Proveli smo ovaj preduvjet provjerom raspona unutar metode. Podtip može oslabiti (ali ne i ojačati) preduvjet metode koju nadjača. Kada podtip oslabi preduvjet, on ublažava ograničenja koja nameće metoda supertipa.

Zamijenimo sada doStuff metoda s oslabljenim preduvjetom:

traka javne klase proširuje Foo {@Override // preduvjet: 0 <num <= 10 public void doStuff (int num) {if (num 10) {throw new IllegalArgumentException ("Ulaz izvan raspona 1-10"); } // ovdje ima malo logike ...}}

Ovdje je preduvjet oslabljen u nadjačanom doStuff metoda za 0 <broj <= 10, dopuštajući širi raspon vrijednosti za num. Sve vrijednosti num koji vrijede za Foo.doStuff vrijede za Bar.doStuff također. Slijedom toga, klijent tvrtke Foo.doStuff ne primjećuje razliku kad zamjenjuje Foo s Bar.

Suprotno tome, kada podtip ojačava preduvjet (npr. 0 <num <= 3 u našem primjeru) primjenjuje stroža ograničenja od supertipa. Na primjer, vrijednosti 4 i 5 za num vrijede za Foo.doStuff, ali više ne vrijede za Bar.doStuff.

To bi razbilo klijentski kod koji ne očekuje ovo novo strože ograničenje.

6.7. Pravilo metoda - postuslovi

Postuslov je uvjet koji treba biti zadovoljen nakon izvršavanja metode.

Pogledajmo primjer:

javni sažetak klase Car {zaštićen int brzina; // postuslov: brzina mora smanjiti zaštićenu apstraktnu praznu kočnicu (); // Ostale metode ...} 

Evo, kočnica metoda Automobil specificira postuslov da Automobil‘S ubrzati mora smanjiti na kraju izvršenja metode. Podtip može ojačati (ali ne i oslabiti) postuslov za metodu koju nadjačava. Kada podtip ojača postkondiciju, pruža više od metode supertipa.

Sada, definirajmo izvedenu klasu od Automobil koji jača ovaj preduvjet:

javna klasa HybridCar proširuje Car {// Neka svojstva i druge metode [email protected] // postcondition: brzina se mora smanjiti // postcondition: punjenje mora povećati zaštićenu praznu kočnicu () {// Primijeniti HybridCar kočnicu}}

Nadjačano kočnica metoda u Hibridni automobil jača postkondiciju dodatno osiguravajući da naplatiti je također povećan. Slijedom toga, bilo koji klijentski kod koji se oslanja na postuslov kočnica metoda u Automobil klasa ne zamjećuje razliku kada zamjenjuje Hibridni automobil za Automobil.

Suprotno tome, ako Hibridni automobil trebali oslabiti postkondiciju nadjačanih kočnica metodom, to više ne bi jamčilo da ubrzati bi se smanjio. To bi moglo prekinuti klijentski kôd s obzirom na Hibridni automobil kao zamjena za Automobil.

7. Šifra miriše

Kako možemo uočiti podtip koji nije zamjenjiv svojim supertipom u stvarnom svijetu?

Pogledajmo neke uobičajene mirise koda koji su znakovi kršenja načela zamjene Liskova.

7.1. Podtip donosi izuzetak za ponašanje koje ne može ispuniti

Primjer toga vidjeli smo ranije u našem primjeru bankarske prijave.

Prije refaktoriranja, Račun razred imao dodatnu metodu povući da je njegov podrazred FixedTermDepositAccount nije htio. The FixedTermDepositAccount razred je to zaobišao bacajući UnsupportedOperationException za povući metoda. Međutim, ovo je bio samo hak kako bi se prikrila slabost u modeliranju hijerarhije nasljedstva.

7.2. Podtip ne pruža provedbu ponašanja koje ne može ispuniti

Ovo je varijacija gornjeg mirisa koda. Podtip ne može ispuniti ponašanje i tako ne čini ništa u nadjačanoj metodi.

Evo primjera. Definirajmo a Sustav datoteka sučelje:

javno sučelje FileSystem {File [] listFiles (put niza); void deleteFile (put niza) baca IOException; } 

Definirajmo a ReadOnlyFileSystem koji provodi Sustav datoteka:

javna klasa ReadOnlyFileSystem implementira FileSystem {javna datoteka [] listFiles (put niza) {// kôd za popis datoteka vraća novu datoteku [0]; } public void deleteFile (put niza) baca IOException {// Ne poduzimajte ništa. // operacija deleteFile nije podržana u datotečnom sustavu samo za čitanje}}

Evo, ReadOnlyFileSystem ne podržava izbrisati dateoteku operacija i tako ne pruža provedbu.

7.3. Klijent zna za podvrste

Ako klijentski kod treba koristiti instanceof ili spuštanje, tada su šanse da su prekršeni i otvoreni / zatvoreni princip i princip zamjene Liskova.

Pokažimo to pomoću a FilePurgingJob:

javna klasa FilePurgingJob {private FileSystem fileSystem; javni FilePurgingJob (FileSystem fileSystem) {this.fileSystem = fileSystem; } javna praznina purgeOldestFile (put niza) {if (! (instanca datotečnog sustava ReadOnlyFileSystem)) {// kôd za otkrivanje najstarije datoteke fileSystem.deleteFile (put); }}}

Jer Sustav datoteka model u osnovi je nespojiv s datotečnim sustavima samo za čitanje, ReadOnlyFileSystem nasljeđuje a izbrisati dateoteku metoda koju ne može podržati. Ovaj primjer koda koristi instanceof check za obavljanje posebnog posla na temelju implementacije podtipa.

7.4. Metoda podtipa uvijek vraća istu vrijednost

Ovo je daleko suptilnije kršenje od ostalih i teže ga je uočiti. U ovom primjeru, Autić uvijek vraća fiksnu vrijednost za preostaloGorivo svojstvo:

javna klasa ToyCar proširuje Car {@Override protected int getRemainingFuel () {return 0; }} 

Ovisi o sučelju i o tome što vrijednost znači, ali općenito tvrdo kodiranje koja bi trebala biti promjenjiva vrijednost stanja objekta znak je da podrazred ne ispunjava cjelokupni svoj supertip i da ga nije istinski zamjenjiv.

8. Zaključak

U ovom smo članku pogledali princip oblikovanja ČVRSTE zamjene Liskov.

Zamjenski princip Liskova pomaže nam u oblikovanju dobrih hijerarhija nasljeđivanja. Pomaže nam u sprečavanju hijerarhija modela koje nisu u skladu s načelom otvoreno / zatvoreno.

Bilo koji model nasljeđivanja koji se pridržava principa zamjene Liskova implicitno će slijediti princip Otvoreno / Zatvoreno.

Za početak smo pogledali slučaj upotrebe koji pokušava slijediti načelo otvoreno / zatvoreno, ali krši načelo zamjene Liskova. Zatim smo pogledali definiciju Liskovskog principa supstitucije, pojam ponašanja podtipova i pravila koja podtipovi moraju slijediti.

Na kraju smo pregledali neke uobičajene mirise koda koji nam mogu pomoći u otkrivanju kršenja postojećeg koda.

Kao i uvijek, primjer koda iz ovog članka dostupan je na GitHubu.