Princíp substitúcie Liskov v Jave

1. Prehľad

Princípy návrhu SOLID predstavil Robert C. Martin vo svojej práci z roku 2000, Princípy návrhu a vzory návrhu. Princípy solídneho dizajnu nám pomáhajú vytvárať udržateľnejší, zrozumiteľnejší a flexibilnejší softvér.

V tomto článku sa budeme zaoberať princípom substitúcie Liskov, ktorý je v skratke „L“.

2. Princíp otvorenia / zatvorenia

Aby sme pochopili princíp substitúcie Liskov, musíme najskôr porozumieť princípu otvoreného / uzavretého („O“ od SOLID).

Cieľ princípu otvoreného / uzavretého nás povzbudzuje, aby sme navrhovali náš softvér tak, aby sme aj my pridávať nové funkcie iba pridaním nového kódu. Pokiaľ je to možné, máme voľne spojené a teda ľahko udržiavateľné aplikácie.

3. Príklad použitia

Pozrime sa na príklad bankovej aplikácie, aby sme lepšie pochopili princíp otvoreného / uzavretého.

3.1. Bez princípu otvorenia / zatvorenia

Naša banková aplikácia podporuje dva typy účtov - „bežný“ a „sporiaci“. Predstavujú ich triedy CurrentAccount a Sporiaci účet resp.

The BankingAppWithdrawalService slúži svojim používateľom na výber:

Bohužiaľ, s rozšírením tohto dizajnu je problém. The BankingAppWithdrawalService je si vedomý dvoch konkrétnych implementácií účtu. Preto BankingAppWithdrawalService bude potrebné zmeniť pri každom zavedení nového typu účtu.

3.2. Využitie princípu otvorenia / zatvorenia na rozšírenie kódu

Poďme redizajnovať riešenie tak, aby bolo v súlade s princípom Open / Closed. Zatvoríme BankingAppWithdrawalService od úpravy, keď sú potrebné nové typy účtov, pomocou Účet základná trieda namiesto toho:

Tu sme predstavili nový abstrakt Účet trieda že CurrentAccount a Sporiaci účet predĺžiť.

The BankingAppWithdrawalService už nezávisí od konkrétnych tried účtov. Pretože teraz záleží iba na abstraktnej triede, nemusí sa pri zavedení nového typu účtu meniť.

V dôsledku toho BankingAppWithdrawalService je otvorené pre rozšírenie s novými typmi účtov, ale pre modifikáciu uzavreté, pretože nové typy nevyžadujú zmenu, aby sa mohli integrovať.

3.3. Java kód

Pozrime sa na tento príklad v prostredí Java. Na začiatok si zadefinujeme Účet trieda:

verejná abstraktná trieda Účet {chránený abstrakt neplatný vklad (BigDecimal suma); / ** * Znižuje zostatok na účte o zadanú sumu * za predpokladu, že je zadaná suma> 0 a účet spĺňa minimálne dostupné * kritériá zostatku. * * @param suma * / výber chránenej abstraktnej neplatnosti (BigDecimal amount); } 

A definujme BankingAppWithdrawalService:

verejná trieda BankingAppWithdrawalService {súkromný účet účtu; public BankingAppWithdrawalService (účet účtu) {this.account = účet; } výber z verejnej neplatnosti (BigDecimal suma) {account.withdraw (suma); }}

Teraz sa pozrime na to, ako by v tomto dizajne mohol nový typ účtu porušiť princíp substitúcie Liskov.

3.4. Nový typ účtu

Banka teraz chce svojim zákazníkom ponúknuť vysoko úročený termínovaný vkladový účet.

Aby sme to podporili, predstavíme nový FixedTermDepositAccount trieda. Účet s termínovaným vkladom v reálnom svete je „typom účtu“. To znamená dedičnosť v našom objektovo orientovanom dizajne.

Takže poďme FixedTermDepositAccount podtrieda Účet:

verejná trieda FixedTermDepositAccount rozširuje účet {// Prepísané metódy ...}

Zatiaľ je všetko dobré. Banka však nechce povoliť výbery na účty s termínovaným vkladom.

To znamená, že nový FixedTermDepositAccount trieda nemôže zmysluplne poskytnúť odstúpiť metóda, ktorá Účet definuje. Jedným spoločným riešením je vytvoriť FixedTermDepositAccount hodiť UnsupportedOperationException v metóde nemôže spĺňať:

verejná trieda FixedTermDepositAccount rozširuje účet {@Override chránený neplatný vklad (BigDecimal čiastka) {// vklad na tento účet} @Override chránený neplatný vklad (BigDecimal suma) {hodiť novú UnsupportedOperationException ("Výbery nie sú podporované FixedTermDepositAccount !!"); }}

3.5. Testovanie pomocou nového typu účtu

Aj keď nová trieda funguje dobre, skúsme ju použiť s BankingAppWithdrawalService:

Účet myFixedTermDepositAccount = nový FixedTermDepositAccount (); myFixedTermDepositAccount.deposit (nový BigDecimal (1 000,00)); BankingAppWithdrawalService selectionService = nový BankingAppWithdrawalService (myFixedTermDepositAccount); creationService.withdraw (nový BigDecimal (100,00));

Nie je prekvapením, že banková aplikácia zlyhala s chybou:

FixedTermDepositAccount nepodporuje výbery !!

Je zrejmé, že s týmto dizajnom niečo nie je v poriadku, ak platná kombinácia objektov spôsobí chybu.

3.6. Čo sa pokazilo?

The BankingAppWithdrawalService je klientom spoločnosti Účet trieda. Očakáva to oboje Účet a jeho podtypy zaručujú správanie, ktoré Účet trieda určila pre svoje odstúpiť metóda:

/ ** * Znižuje zostatok na účte o zadanú sumu * za predpokladu, že daná suma> 0 a účet spĺňa minimálne dostupné * kritériá zostatku. * * @param suma * / výber chránenej abstraktnej neplatnosti (BigDecimal amount);

Nepodporovaním však odstúpiť metóda, FixedTermDepositAccount porušuje túto špecifikáciu metódy. Preto ich nemôžeme spoľahlivo nahradiť FixedTermDepositAccount pre Účet.

Inými slovami, FixedTermDepositAccount porušil princíp substitúcie Liskov.

3.7. Nemôžeme spracovať chybu v systéme Windows BankingAppWithdrawalService?

Dizajn by sme mohli upraviť tak, aby klient Účet‘S odstúpiť metóda si musí byť vedomá možnej chyby pri jej volaní. To by však znamenalo, že klienti musia mať špeciálne znalosti o neočakávanom správaní sa podtypu. Týmto sa začína porušovať princíp otvorenia / zatvorenia.

Inými slovami, aby princíp otvoreného / uzavretého fungoval dobre, všetko podtypy musia byť nahraditeľné svojim supertypom bez toho, aby ste museli upravovať kód klienta. Dodržiavanie princípu substitúcie Liskov zabezpečuje túto nahraditeľnosť.

Pozrime sa teraz podrobne na princíp substitúcie Liskov.

4. Zásada substitúcie Liskov

4.1. Definícia

Robert C. Martin to zhŕňa:

Podtypy musia byť pre svoje základné typy nahraditeľné.

Barbara Liskovová, ktorá ju definovala v roku 1988, poskytla matematickejšiu definíciu:

Ak pre každý objekt o1 typu S existuje objekt o2 typu T taký, že pre všetky programy P definované v termínoch T je správanie P nezmenené, keď je o1 nahradené za o2, potom S je podtyp T.

Pochopme tieto definície trochu viac.

4.2. Kedy je podtyp nahraditeľný svojim supertypom?

Podtyp sa nestane automaticky nahraditeľným za svoj supertyp. Aby bol podtyp nahraditeľný, musí sa správať ako svoj supertyp.

Správanie objektu je zmluva, na ktorú sa môžu klienti spoľahnúť. Správanie je určené verejnými metódami, obmedzeniami kladenými na ich vstupy, zmenami stavu, ktorými objekt prechádza, a vedľajšími účinkami vykonávania metód.

Podtypovanie v prostredí Java vyžaduje, aby vlastnosti a metódy základnej triedy boli dostupné v podtriede.

Podtyp správania však znamená, že podtyp nielen poskytuje všetky metódy v nadtype, ale musí dodržiavať behaviorálnu špecifikáciu supertypu. To zaisťuje, že podtyp spĺňa všetky predpoklady klientov o správaní sa nadtypu.

Toto je ďalšie obmedzenie, ktoré princíp substitúcie Liskov prináša do objektovo orientovaného dizajnu.

Poďme teraz reformovať našu bankovú aplikáciu na riešenie problémov, s ktorými sme sa stretli skôr.

5. Refaktoring

Aby sme vyriešili problémy, ktoré sme našli v príklade bankovníctva, začnime pochopením hlavnej príčiny.

5.1. Koreňová príčina

V príklade náš FixedTermDepositAccount nebol podtypom správania Účet.

Dizajn Účet nesprávne predpokladal, že všetko Účet typy umožňujú výbery. V dôsledku toho všetky podtypy Účet, počítajúc do toho FixedTermDepositAccount ktorý nepodporuje výbery, zdedil odstúpiť metóda.

Aj keď by sme to mohli vyriešiť predĺžením zmluvy o Účet, existujú alternatívne riešenia.

5.2. Revidovaný diagram tried

Navrhnime našu hierarchiu účtov inak:

Pretože všetky účty nepodporujú výbery, presunuli sme odstúpiť metóda z Účet triedy do novej abstraktnej podtriedy Výberový účet. Oboje CurrentAccount a Sporiaci účet povoliť výbery. Takže teraz boli vyrobené z podtried nového Výberový účet.

To znamená BankingAppWithdrawalService môže dôverovať správnemu typu účtu a poskytovať odstúpiť funkcie.

5.3. Refaktorované BankingAppWithdrawalService

BankingAppWithdrawalService teraz je potrebné použiť Výberový účet:

verejná trieda BankingAppWithdrawalService {súkromný WithdrawableAccount výberový; public BankingAppWithdrawalService (WithdrawableAccount výberový účet) {this.withdrawableAccount = výberový účet; } výber z verejnej neplatnosti (BigDecimal čiastka) {removeableAccount.withdraw (čiastka); }}

Ako pre FixedTermDepositAccount, ponechávame Účet ako jeho materská trieda. V dôsledku toho dedí iba Záloha správanie, ktoré môže spoľahlivo splniť a ktoré už nezdedí odstúpiť metóda, ktorú nechce. Tento nový dizajn sa vyhýba problémom, ktoré sme videli skôr.

6. Pravidlá

Pozrime sa teraz na niektoré pravidlá / techniky týkajúce sa podpisov metód, invarianty, predbežné podmienky a postconditions, ktoré môžeme dodržiavať a ktoré používame na zaistenie toho, že vytvoríme dobre vychované podtypy.

V ich knihe Vývoj programu v Jave: Abstrakcia, špecifikácia a objektovo orientovaný dizajn, Barbara Liskov a John Guttag zoskupili tieto pravidlá do troch kategórií - pravidlo podpisu, pravidlo vlastností a pravidlo metód.

Niektoré z týchto postupov sú už vynútené prvoradými pravidlami Javy.

Mali by sme si tu všimnúť určitú terminológiu. Širší typ je všeobecnejší - Objekt napríklad môže znamenať AKÝKOĽVEK objekt Java a je širší ako, povedzme, CharSequence, kde String je veľmi konkrétny, a preto užší.

6.1. Pravidlo podpisu - typy argumentov metódy

Toto pravidlo hovorí, že prepísané typy argumentov metódy podtypu môžu byť rovnaké alebo širšie ako typy argumentov metódy supertypu.

Pravidlá prepísania metódy Javy podporujú toto pravidlo vynútením toho, aby sa typy argumentov prepísanej metódy presne zhodovali s metódou supertypu.

6.2. Pravidlo podpisu - typy vrátenia

Návratový typ metódy prepísaného podtypu môže byť užší ako návratový typ metódy supertypu. Toto sa nazýva kovariancia návratových typov. Kovariancia označuje, kedy je podtyp prijatý namiesto supertypu. Java podporuje kovarianciu návratových typov. Pozrime sa na príklad:

public abstract class Foo {public abstract Číslo generateNumber (); // Ostatné metódy} 

The generovat cislo metóda v Foo má návratový typ ako Číslo. Poďme teraz prekonať túto metódu vrátením užšieho typu Celé číslo:

public class Bar extends Foo {@Override public Integer generateNumber () {return new Integer (10); } // Ostatné metódy}

Pretože Celé číslo IS-A Číslo, kód klienta, ktorý očakáva Číslo môže nahradiť Foo s Bar bez problémov.

Na druhej strane, ak je prepísaná metóda v Bar mali vrátiť širší typ ako Číslo, napr. Objekt, ktoré môžu zahŕňať akýkoľvek podtyp Objekt napr. a Nákladné auto. Akýkoľvek kód klienta, ktorý sa spoliehal na návratový typ súboru Číslo nezvládol a Nákladné auto!

Našťastie pravidlá prepísania metódy Javy bránia tomu, aby metóda prepísania vrátila širší typ.

6.3. Pravidlo podpisu - výnimky

Metóda podtypu môže spôsobiť menej alebo užšie (ale nie ďalšie alebo širšie) výnimky ako metóda supertypu.

Je to pochopiteľné, pretože keď kód klienta nahradí podtyp, dokáže spracovať metódu vyvolávajúcu menej výnimiek ako metóda supertypu. Ak by však metóda podtypu spôsobila nové alebo širšie kontrolované výnimky, kód klienta by sa porušil.

Pravidlá prepisovania metód Javy už toto pravidlo presadzujú pre kontrolované výnimky. Avšak prekonávajúce metódy v Jave MÔŽU HODIŤ ľubovoľné RuntimeException bez ohľadu na to, či prepísaná metóda deklaruje výnimku.

6.4. Vlastnosti - invarianty triedy

Triedny invariant je tvrdenie týkajúce sa vlastností objektu, ktoré musí platiť pre všetky platné stavy objektu.

Pozrime sa na príklad:

verejná abstraktná trieda Auto {chránený int limit; // invariant: speed <limit; rýchlosť chráneného int; // postcondition: speed <limit protected abstract void accelerate (); // Ostatné metódy ...}

The Auto trieda určuje invariant triedy, ktorý rýchlosť musí byť vždy pod limit. Vláda invariantov to tvrdí všetky podtypové metódy (zdedené aj nové) musia udržiavať alebo posilňovať triedne invarianty supertypu.

Definujme podtriedu Auto ktorý zachováva invariantnosť triedy:

verejná trieda HybridCar rozširuje Car {// invariant: charge> = 0; súkromný int poplatok; @Override // postcondition: speed <limit protected void accelerate () {// Urýchlenie HybridCar zaisťujúce rýchlosť <limit} // Ostatné metódy ...}

V tomto príklade je invariant v Auto je zakonzervovaný urýchliť metóda v Hybridné auto. The Hybridné auto navyše definuje svoju vlastnú invariantnú triedu poplatok> = 0, a to je úplne v poriadku.

Naopak, ak sa invariant triedy nezachová podtyp, rozbije akýkoľvek kód klienta, ktorý sa spolieha na supertyp.

6.5. Pravidlo Vlastnosti - Obmedzenie histórie

Historické obmedzenie uvádza, že: podtriedametódy (zdedené alebo nové) by nemali umožňovať zmeny stavu, ktoré základná trieda neumožňovala.

Pozrime sa na príklad:

public abstract class Car {// Povolené byť nastavené raz v čase vytvorenia. // Hodnota sa môže zvyšovať až potom. // Hodnota sa nedá vynulovať. chránený počet najazdených kilometrov; verejné auto (najazdené kilometre) {this.mileage = počet najazdených kilometrov; } // Ďalšie vlastnosti a metódy ...}

The Auto trieda určuje obmedzenie pre najazdených kilometrov nehnuteľnosť. The najazdených kilometrov vlastnosť je možné nastaviť iba raz v čase vytvorenia a potom sa už nedá resetovať.

Definujme teraz a Hračkárske auto ktorý sa rozširuje Auto:

verejná trieda ToyCar rozširuje auto {public void reset () {počet kilometrov = 0; } // Ostatné vlastnosti a metódy}

The Hračkárske auto má metódu navyše resetovať ktorý resetuje najazdených kilometrov nehnuteľnosť. Pritom sa Hračkárske auto ignoroval obmedzenie uložené jeho rodičom pre najazdených kilometrov nehnuteľnosť. Týmto sa zlomí akýkoľvek kód klienta, ktorý sa spolieha na dané obmedzenie. Takže Hračkárske auto nie je nahraditeľný Auto.

Podobne, ak má základná trieda nemennú vlastnosť, podtrieda by nemala umožňovať úpravu tejto vlastnosti. Preto by mali byť nemenné triedy konečné.

6.6. Metodické pravidlo - predpoklady

Pred vykonaním metódy by mal byť splnený predpoklad. Pozrime sa na príklad predpokladu týkajúceho sa hodnôt parametrov:

public class Foo {// predpoklad: 0 <num <= 5 public void doStuff (int num) {if (num 5) {throw new IllegalArgumentException ("Input out of range 1-5"); } // nejaká logika tu ...}}

Tu je predpokladom pre doStuff metóda uvádza, že číslo hodnota parametra musí byť medzi 1 a 5. Túto podmienku sme vynútili kontrolou rozsahu vo vnútri metódy. Podtyp môže oslabiť (ale nie posilniť) predpoklad pre metódu, ktorú má prednosť. Keď podtyp oslabuje predpoklad, uvoľňuje obmedzenia uložené metódou supertypu.

Poďme teraz prekonať doStuff metóda s oslabeným predpokladom:

public class Bar extends Foo {@Override // precondition: 0 <num <= 10 public void doStuff (int num) {if (num 10) {throw new IllegalArgumentException ("Input out of range 1-10"); } // nejaká logika tu ...}}

Tu je predpoklad v oslabení oslabený doStuff metóda do 0 <počet <= 10, čo umožňuje širší rozsah hodnôt pre číslo. Všetky hodnoty číslo ktoré sú platné pre Foo.doStuff sú platné pre Bar.doStuff tiež. Následne klient Foo.doStuff nezbadá rozdiel, keď nahradí Foo s Bar.

Naopak, keď podtyp posilňuje predpoklad (napr. 0 <počet <= 3 v našom príklade), uplatňuje prísnejšie obmedzenia ako supertyp. Napríklad hodnoty 4 a 5 pre číslo sú platné pre Foo.doStuff, ale už nie sú platné pre Bar.doStuff.

Týmto by sa zlomil kód klienta, ktorý neočakáva toto nové užšie obmedzenie.

6.7. Metóda Rule - Dodatočné podmienky

Postcondition je podmienka, ktorá by mala byť splnená po vykonaní metódy.

Pozrime sa na príklad:

verejná abstraktná trieda Auto {chránené int rýchlosť; // postcondition: rýchlosť musí znižovať chránenú abstraktnú neplatnú brzdu (); // Ostatné metódy ...} 

Tu je brzda metóda Auto určuje následnú podmienku, ktorú Auto‘S rýchlosť musí znížiť na konci vykonania metódy. Podtyp môže posilniť (ale nie oslabiť) následnú podmienku metódy, ktorá je nadradená. Keď podtyp posilňuje postcondition, poskytuje viac ako metóda supertypu.

Teraz definujme odvodenú triedu Auto ktorý posilňuje tento predpoklad:

verejná trieda HybridCar rozširuje Car {// Niektoré vlastnosti a ďalšie metódy ...@Override // postcondition: rýchlosť sa musí znížiť // postcondition: nabíjanie musí zvýšiť chránenú brzdu void () {// použiť brzdu HybridCar}}

Prepísaný brzda metóda v Hybridné auto posilňuje podmienku dodatočným zabezpečením toho, že poplatok sa tiež zvyšuje. V dôsledku toho je každý kód klienta, ktorý sa spolieha na dodatočnú podmienku protokolu brzda metóda v Auto trieda si nevšimne žiadny rozdiel, keď nahradí Hybridné auto pre Auto.

Naopak, ak Hybridné auto mali oslabiť následnú premenu brzda metódou by to už nezaručovalo, že rýchlosť by sa znížila. Týmto by sa mohol prelomiť kód klienta daný a Hybridné auto ako náhrada za Auto.

7. Kód Vonia

Ako môžeme zistiť podtyp, ktorý nie je v skutočnom svete nahraditeľný svojim supertypom?

Pozrime sa na niektoré bežné pachy kódu, ktoré sú znakmi porušenia princípu substitúcie Liskov.

7.1. Podtyp vyvoláva výnimku pre správanie, ktoré nemôže splniť

Príklad sme videli v príklade našich bankových aplikácií už skôr.

Pred refaktorizáciou Účet triedy mala metódu navyše odstúpiť že jeho podtrieda FixedTermDepositAccount nechcel. The FixedTermDepositAccount trieda to obišla vyhodením UnsupportedOperationException pre odstúpiť metóda. Bol to však iba hack, ktorý zakryl slabosť v modelovaní hierarchie dedičstva.

7.2. Podtyp neposkytuje žiadnu implementáciu pre správanie, ktoré nemôže splniť

Toto je variácia vyššie uvedeného zápachu kódu. Podtyp nemôže splniť správanie, a tak v prepísanej metóde neurobí nič.

Tu je príklad. Definujme a Systém súborov rozhranie:

verejné rozhranie FileSystem {File [] listFiles (reťazcová cesta); void deleteFile (reťazcová cesta) vyvolá IOException; } 

Definujme a ReadOnlyFileSystem ktorý realizuje Systém súborov:

verejná trieda ReadOnlyFileSystem implementuje FileSystem {public File [] listFiles (reťazcová cesta) {// kód na zoznam súborov vráti nový File [0]; } public void deleteFile (reťazcová cesta) vyvolá IOException {// Nerobiť nič. // operácia deleteFile nie je podporovaná v systéme súborov iba na čítanie}}

Tu je ReadOnlyFileSystem nepodporuje vymazať súbor a teda neposkytuje implementáciu.

7.3. Klient vie o podtypoch

Ak je potrebné použiť kód klienta inštancia alebo downcasting, potom je pravdepodobné, že bol porušený princíp otvoreného / zatvoreného aj princíp substitúcie Liskov.

Ilustrujme to pomocou a FilePurgingJob:

verejná trieda FilePurgingJob {private FileSystem fileSystem; public FilePurgingJob (FileSystem fileSystem) {this.fileSystem = fileSystem; } public void purgeOldestFile (reťazcová cesta) {if (! (fileSystem instanceof ReadOnlyFileSystem)) {// kód na detekciu najstaršieho súboru fileSystem.deleteFile (cesta); }}}

Pretože Systém súborov model je v zásade nekompatibilný so súborovými systémami iba na čítanie, ReadOnlyFileSystem dedí a vymazať súbor metóda, ktorú nemôže podporovať. V tomto príklade kódu sa používa znak inštancia začiarknite políčko na vykonanie špeciálnej práce na základe implementácie podtypu.

7.4. Metóda podtypu vždy vráti rovnakú hodnotu

Toto je oveľa jemnejšie porušenie ako ostatné a je ťažšie ho spozorovať. V tomto príklade Hračkárske auto vždy vráti pevnú hodnotu pre zostávajúce palivo nehnuteľnosť:

verejná trieda ToyCar rozširuje auto {@Override chránené int getRemainingFuel () {návrat 0; }} 

Závisí to od rozhrania a od toho, čo táto hodnota znamená, ale všeobecne napevno zakódované, čo by mala byť premenlivá stavová hodnota objektu, je znakom toho, že podtrieda nespĺňa celý svoj supertyp a nie je za ňu skutočne nahraditeľná.

8. Záver

V tomto článku sme sa pozreli na princíp návrhu Liskovovej substitúcie SOLID.

Princíp substitúcie Liskov nám pomáha modelovať dobré hierarchie dedičstva. Pomáha nám zabrániť modelovým hierarchiám, ktoré nezodpovedajú princípu otvoreného / uzavretého.

Akýkoľvek model dedenia, ktorý dodržiava princíp substitúcie Liskov, sa bude implicitne riadiť princípom otvoreného / uzavretého.

Najprv sme sa pozreli na prípad použitia, ktorý sa pokúša dodržiavať zásadu otvoreného / zatvoreného, ​​ale porušuje zásadu substitúcie Liskov. Ďalej sme sa pozreli na definíciu princípu substitúcie Liskov, pojem behaviorálnych podtypov a pravidlá, ktoré musia podtypy dodržiavať.

Nakoniec sme sa pozreli na niektoré bežné pachy kódu, ktoré nám môžu pomôcť odhaliť porušenia v našom existujúcom kóde.

Vzorový kód z tohto článku je ako vždy k dispozícii na stránkach GitHub.


$config[zx-auto] not found$config[zx-overlay] not found