Soukromé a chráněné vlastnosti a metody

Jeden z nejdůležitějších principů objektově orientovaného programování – oddělení vnitřního rozhraní od vnějšího.

To je „nezbytná“ praxe při vývoji čehokoli složitějšího, než je aplikace „ahoj světe“.

Abychom to pochopili, odpoutejme se od vývoje a zaměřme se na skutečný svět.

Zařízení, která používáme, jsou obvykle poměrně složitá. Ale oddělení vnitřního rozhraní od vnějšího umožňuje jejich bezproblémové používání.

Příklad ze skutečného života

Například kávovar. Jednoduché zvenčí:tlačítko, displej, pár otvorů... A výsledek jistě – skvělá káva! :)

Ale uvnitř… (obrázek z návodu k opravě)

Hodně detailů. Ale můžeme to použít, aniž bychom cokoliv věděli.

Kávovary jsou docela spolehlivé, že? Můžeme jeden používat roky, a pokud se něco pokazí, přineste ho na opravu.

Tajemství spolehlivosti a jednoduchosti kávovaru – všechny detaily jsou dobře vyladěné a skryté uvnitř.

Pokud z kávovaru odstraníme ochranný kryt, jeho používání bude mnohem složitější (kde stisknout?) a nebezpečnější (může dojít k úrazu elektrickým proudem).

Jak uvidíme, při programování jsou objekty jako kávovary.

Ale abychom skryli vnitřní detaily, nepoužijeme ochranný obal, ale spíše speciální syntaxi jazyka a konvencí.

Interní a externí rozhraní

V objektově orientovaném programování jsou vlastnosti a metody rozděleny do dvou skupin:

  • Interní rozhraní – metody a vlastnosti, přístupné z jiných metod třídy, ale ne zvenčí.
  • Externí rozhraní – metody a vlastnosti, přístupné i mimo třídu.

Pokud budeme pokračovat v analogii s kávovarem – to, co se skrývá uvnitř:trubka bojleru, topné těleso a tak dále – je jeho vnitřní rozhraní.

Pro fungování objektu se používá vnitřní rozhraní, jeho detaily se navzájem využívají. Například trubka kotle je připevněna k topnému tělesu.

Zvenčí je ale kávovar uzavřen ochranným krytem, ​​takže se k nim nikdo nedostane. Podrobnosti jsou skryté a nepřístupné. Jeho funkce můžeme využívat prostřednictvím externího rozhraní.

Takže vše, co potřebujeme k použití objektu, je znát jeho vnější rozhraní. Možná si vůbec neuvědomujeme, jak to uvnitř funguje, a to je skvělé.

To byl obecný úvod.

V JavaScriptu existují dva typy objektových polí (vlastnosti a metody):

  • Veřejné:přístupné odkudkoli. Obsahují vnější rozhraní. Dosud jsme používali pouze veřejné vlastnosti a metody.
  • Soukromé:přístupné pouze zevnitř třídy. Ty jsou určeny pro vnitřní rozhraní.

V mnoha jiných jazycích také existují „chráněná“ pole:přístupná pouze zevnitř třídy a ta, která ji rozšiřují (jako soukromé, ale navíc s přístupem z dědičných tříd). Jsou také užitečné pro vnitřní rozhraní. Jsou v jistém smyslu rozšířenější než soukromé, protože obvykle chceme, aby k nim získaly přístup dědičné třídy.

Chráněná pole nejsou implementována v JavaScriptu na jazykové úrovni, ale v praxi jsou velmi pohodlná, takže jsou emulovaná.

Nyní vytvoříme kávovar v JavaScriptu se všemi těmito typy vlastností. Kávovar má spoustu detailů, nebudeme je modelovat, aby zůstaly jednoduché (i když bychom mohli).

Ochrana „waterAmount“

Nejprve si uděláme jednoduchý kávovar:

class CoffeeMachine {
 waterAmount = 0; // the amount of water inside

 constructor(power) {
 this.power = power;
 alert( `Created a coffee-machine, power: ${power}` );
 }

}

// create the coffee machine
let coffeeMachine = new CoffeeMachine(100);

// add water
coffeeMachine.waterAmount = 200;

Právě teď vlastnosti waterAmount a power jsou veřejné. Můžeme je snadno získat/nastavit zvenčí na jakoukoli hodnotu.

Pojďme změnit waterAmount majetek chránit, abyste nad ním měli větší kontrolu. Například nechceme, aby to někdo nastavil pod nulu.

Chráněné vlastnosti mají obvykle předponu podtržítka _ .

To není vynuceno na jazykové úrovni, ale mezi programátory existuje dobře známá konvence, že k takovým vlastnostem a metodám by neměl být přístup zvenčí.

Naše vlastnost se tedy bude jmenovat _waterAmount :

class CoffeeMachine {
 _waterAmount = 0;

 set waterAmount(value) {
 if (value < 0) {
 value = 0;
 }
 this._waterAmount = value;
 }

 get waterAmount() {
 return this._waterAmount;
 }

 constructor(power) {
 this._power = power;
 }

}

// create the coffee machine
let coffeeMachine = new CoffeeMachine(100);

// add water
coffeeMachine.waterAmount = -10; // _waterAmount will become 0, not -10

Nyní je přístup pod kontrolou, takže nastavení množství vody pod nulu je nemožné.

Síla pouze pro čtení

Pro power vlastnost, udělejme to pouze pro čtení. Někdy se stává, že vlastnost musí být nastavena pouze v době vytvoření a poté již nikdy upravena.

To je přesně případ kávovaru:výkon se nikdy nemění.

Abychom tak učinili, potřebujeme pouze vytvořit getter, ale nikoli setter:

class CoffeeMachine {
 // ...

 constructor(power) {
 this._power = power;
 }

 get power() {
 return this._power;
 }

}

// create the coffee machine
let coffeeMachine = new CoffeeMachine(100);

alert(`Power is: ${coffeeMachine.power}W`); // Power is: 100W

coffeeMachine.power = 25; // Error (no setter)
Funkce getter/setter

Zde jsme použili syntaxi getter/setter.

Ale většinou get.../set... preferovány jsou funkce, jako je tato:

class CoffeeMachine {
 _waterAmount = 0;

 setWaterAmount(value) {
 if (value < 0) value = 0;
 this._waterAmount = value;
 }

 getWaterAmount() {
 return this._waterAmount;
 }
}

new CoffeeMachine().setWaterAmount(100);

Vypadá to trochu déle, ale funkce jsou flexibilnější. Mohou přijmout více argumentů (i když je právě nepotřebujeme).

Na druhou stranu je syntaxe get/set kratší, takže v konečném důsledku neexistuje žádné striktní pravidlo, je na vás, abyste se rozhodli.

Chráněná pole se dědí

Pokud zdědíme class MegaMachine extends CoffeeMachine , pak nám nic nebrání v přístupu k this._waterAmount nebo this._power z metod nové třídy.

Takže chráněná pole jsou přirozeně dědičná. Na rozdíl od soukromých, které uvidíme níže.

Soukromá „#waterLimit“

Nedávný přírůstek Toto je nedávný přírůstek do jazyka. Není podporováno v JavaScript enginech nebo zatím podporováno částečně, vyžaduje polyfilling.

Existuje hotový návrh JavaScriptu, téměř ve standardu, který poskytuje podporu na jazykové úrovni pro soukromé vlastnosti a metody.

Soukromé položky by měly začínat # . Jsou přístupné pouze zevnitř třídy.

Zde je například soukromý #waterLimit vlastnost a soukromá metoda kontroly vody #fixWaterAmount :

class CoffeeMachine {
 #waterLimit = 200;

 #fixWaterAmount(value) {
 if (value < 0) return 0;
 if (value > this.#waterLimit) return this.#waterLimit;
 }

 setWaterAmount(value) {
 this.#waterLimit = this.#fixWaterAmount(value);
 }

}

let coffeeMachine = new CoffeeMachine();

// can't access privates from outside of the class
coffeeMachine.#fixWaterAmount(123); // Error
coffeeMachine.#waterLimit = 1000; // Error

Na jazykové úrovni # je zvláštní znamení, že pole je soukromé. Nemáme k němu přístup zvenčí nebo z zděděných tříd.

Soukromá pole nejsou v rozporu s veřejnými. Můžeme mít oba soukromé #waterAmount a veřejné waterAmount polí ve stejnou dobu.

Udělejme například waterAmount přístupový objekt pro #waterAmount :

class CoffeeMachine {

 #waterAmount = 0;

 get waterAmount() {
 return this.#waterAmount;
 }

 set waterAmount(value) {
 if (value < 0) value = 0;
 this.#waterAmount = value;
 }
}

let machine = new CoffeeMachine();

machine.waterAmount = 100;
alert(machine.#waterAmount); // Error

Na rozdíl od chráněných jsou soukromá pole vynucována samotným jazykem. To je dobrá věc.

Ale pokud dědíme z CoffeeMachine , pak nebudeme mít přímý přístup k #waterAmount . Budeme se muset spolehnout na waterAmount getter/setter:

class MegaCoffeeMachine extends CoffeeMachine {
 method() {
 alert( this.#waterAmount ); // Error: can only access from CoffeeMachine
 }
}

V mnoha scénářích je takové omezení příliš závažné. Pokud rozšíříme CoffeeMachine , můžeme mít legitimní důvody pro přístup k jeho interním informacím. Proto se častěji používají chráněná pole, i když je syntaxe jazyka nepodporuje.

Soukromá pole nejsou dostupná, protože [name]

Soukromá pole jsou speciální.

Jak víme, obvykle můžeme přistupovat k polím pomocí this[name] :

class User {
 ...
 sayHi() {
 let fieldName = "name";
 alert(`Hello, ${this[fieldName]}`);
 }
}

Se soukromými poli to není možné:this['#name'] nefunguje. To je omezení syntaxe pro zajištění soukromí.

Shrnutí

Z hlediska OOP se vymezení vnitřního rozhraní od vnějšího nazývá zapouzdření.

Poskytuje následující výhody:

Ochrana pro uživatele, aby se nestřelili do nohy

Představte si, že existuje tým vývojářů, kteří používají kávovar. Byl vyroben společností „Best CoffeeMachine“ a funguje dobře, ale byl odstraněn ochranný kryt. Vnitřní rozhraní je tedy odhaleno.

Všichni vývojáři jsou civilizovaní – používají kávovar tak, jak bylo zamýšleno. Ale jeden z nich, John, usoudil, že je nejchytřejší, a provedl několik úprav uvnitř kávovaru. Takže o dva dny později selhal kávovar.

To jistě není Johnova chyba, ale spíše osoba, která odstranila ochranný kryt a nechala Johna, aby s ním manipuloval.

To samé v programování. Pokud uživatel třídy změní věci, které nebyly zvenčí zamýšleny ke změně – následky jsou nepředvídatelné.

Podporovatelné

Situace v programování je složitější než u skutečného kávovaru, protože jej nekoupíme jen jednou. Kód neustále prochází vývojem a zlepšováním.

Pokud striktně vymezíme vnitřní rozhraní, pak vývojář třídy může libovolně měnit její vnitřní vlastnosti a metody, a to i bez informování uživatelů.

Pokud jste vývojář takové třídy, je skvělé vědět, že soukromé metody lze bezpečně přejmenovat, jejich parametry lze změnit a dokonce odstranit, protože na nich nezávisí žádný externí kód.

Pro uživatele, když vyjde nová verze, může to být celková vnitřní oprava, ale stále snadno upgradovat, pokud je externí rozhraní stejné.

Skrytí složitosti

Lidé rádi používají věci, které jsou jednoduché. Alespoň zvenčí. To, co je uvnitř, je jiná věc.

Programátoři nejsou výjimkou.

Je vždy výhodné, když jsou detaily implementace skryté a je k dispozici jednoduché, dobře zdokumentované externí rozhraní.

Ke skrytí vnitřního rozhraní používáme buď chráněné, nebo soukromé vlastnosti:

  • Chráněná pole začínají _ . To je dobře známá konvence, která není vynucována na jazykové úrovni. Programátoři by měli přistupovat pouze k poli začínajícím _ od své třídy a tříd od ní dědících.
  • Soukromá pole začínají # . JavaScript zajišťuje, že k nim máme přístup pouze zevnitř třídy.

V současné době nejsou soukromá pole mezi prohlížeči dobře podporována, ale mohou být polyfilled.