Капсулация
Проблем
Минус 500 €
В миналия урок сложихме проверката за теглене в метод. Само че полето е отворено и всеки може да пише в него направо:
const account = new Account("Мария", 100);account.withdraw(500);account.balance = -500;console.log(account.balance);-500Методът правилно отказа да изтегли 500 €. Следващият ред просто записа баланса на ръка. Проверката е там, но може да се заобиколи. Трябва начин да кажем „балансът се пипа само от сметката“.
Идеята
#balance: само класът пипа
Сложи пред името на полето #: #balance. Полето трябва да е обявено в класа, а вътре се пише this.#balance:
class Account { #owner; #balance;
constructor(owner, balance) { this.#owner = owner; this.#balance = balance; }
// ...}Поле с # е частно (private): до него стига само кодът вътре в класа Account. Ако отвън има ред
account.#balance = -500;програмата изобщо не тръгва:
SyntaxError: Private field '#balance' must be declared in an enclosing classКласът крие данните си и ги показва само през свои методи. Това се казва капсулация. В Java и C# за това има думи, модификатори за достъп (private, public). В JavaScript частното е #, а всичко без # е публично.
По-точно, за любопитните
- Методите също може да са частни:
#check() { … }се вика само от класа, сthis.#check(). - Внимавай с името.
account.balance = -500без#не пипа частното поле: създава ново, публично полеbalanceна обекта, без грешка. - Преди да има
#(до 2022 г.), в JavaScript се ползваше договорката_balance, както в Python. Ще я срещнеш в по-стар код.
Идеята
Да четеш, без да пипаш
Отвън вече не можем и да прочетем баланса. Затова класът дава метод, който само го връща:
class Account { #owner; #balance;
constructor(owner, balance) { this.#owner = owner; this.#balance = balance; }
get owner() { return this.#owner; }
get balance() { return this.#balance; }
deposit(amount) { if (amount > 0) { this.#balance += amount; } }
withdraw(amount) { if (amount > 0 && amount <= this.#balance) { this.#balance -= amount; return true; } return false; }}
const account = new Account("Мария", 100);account.withdraw(30);account.deposit(50);console.log(`${account.owner}: ${account.balance} €`);Мария: 120 €get balance() е getter: метод, който отвън се чете като поле, account.balance, без скоби. Връща стойността на частното #balance. Сменя се балансът само през deposit и withdraw, а те проверяват.
Setter няма, затова ред account.balance = -500; не сменя нищо: JavaScript пренебрегва записа, без грешка. В строг режим (ред "use strict"; най-горе във файла) същият ред спира програмата:
TypeError: Cannot set property balance of #<Account> which has only a getterwithdraw вече връща true или false: дали тегленето е минало. Така който тегли, разбира дали е успял.
const account = new Account("Мария", 100);account.withdraw(500);account.deposit(50);console.log(account.balance);Стъпка по стъпка, с думи
- #balance е частно: стъклото значи, че отвън до полето няма път.
- Тегленето минава през проверката в метода: 500 е повече от 100. Отказано, балансът не се пипа.
- Внасянето също се проверява: 50 е положителна сума. Методът я добавя.
- Getter-ът balance дава навън копие на баланса. Самото поле #balance остава скрито.
Пробвай
Отказано
Какво ще отпечата програмата?
const account = new Account("Мария", 100);console.log(account.withdraw(150));account.deposit(-20);console.log(account.balance);150 € е повече от баланса, затова withdraw връща false и не пипа баланса. Внасянето на -20 € също се отказва, защото сумата трябва да е положителна. Балансът остава 100.
Идеята
Setter: промяна с проверка
Понякога полето трябва да може да се сменя отвън, но не с каквато и да е стойност. Възрастта на животно може да се промени, но не може да е отрицателна:
class Animal { #age = 0;
get age() { return this.#age; }
set age(value) { if (value >= 0) { this.#age = value; } }}
const cat = new Animal();cat.age = 4;cat.age = -3;console.log(cat.age);4set age(value) е setter: методът, който се вика при cat.age = -3. value е стойността отдясно на =, тук -3. Проверката не минава и възрастта остава 4. Отвън записът изглежда като в обикновено поле, но минава през проверката.
Идеята
Кое поле да има setter?
Не всяко поле има нужда от setter, а някои нямат нужда и от getter. Помисли какво трябва да може да се прави с продукта отвън:
- името и цената се задават при създаването и после само се четат: само getter;
- бройката в количката се сменя, но поне 1: getter и setter с проверка.
class Product { #name; #price; #quantity = 1;
constructor(name, price) { this.#name = name; this.#price = price; }
get name() { return this.#name; }
get price() { return this.#price; }
get quantity() { return this.#quantity; }
set quantity(value) { if (value >= 1) { this.#quantity = value; } }}
const coffee = new Product("Кафе", 7.50);coffee.quantity = 3;coffee.quantity = 0;console.log(`${coffee.name} x${coffee.quantity}`);Кафе x3Правилото е: започни с # за всичко и отваряй само каквото трябва. Setter, който записва всичко без проверка, връща проблема от първата карта.
Пробвай
Кое липсва?
class Account {
= 0;
}#balance: с # полето е видимо само за кода в Account.Пробвай
Намери грешката
Полето е #balance, а балансът пак става -500:
const account = new Account();account.balance = 100;account.withdraw(30);account.balance = -500;console.log(account.balance);-500Кой ред в класа пробива защитата?
Избери реда с грешката.
set balance(value) записва всяка стойност без проверка. Полето е частно, но setter-ът го отваря отново. Махни го и задавай началния баланс в конструктора. Сменя се само с deposit и withdraw.
Упражнение
Скрити данни
Реши задачите в редактора си. Всички полета са частни, с #.
1. Човек. Person с име и възраст, конструктор, getter-и name и age и introduce().
Мария е на 34 г.Здравей, аз съм Мария и съм на 34 г.2. Сметка без минус. Account със собственик и баланс. Внася се само положителна сума. Тегленето връща дали е минало и не пуска баланса под 0. Балансът се чете, но не се записва отвън.
const account = new Account("Петър", 50);account.deposit(25);account.deposit(-10);console.log(account.withdraw(100));console.log(account.withdraw(40));console.log(`Баланс: ${account.balance} €`);falsetrueБаланс: 35 €3. Животно. Animal с вид и възраст. Видът се задава в конструктора и само се чете. Възрастта има setter (set age), който отказва отрицателно число и печата съобщение. Конструкторът също минава през тази проверка: в конструктора пиши this.age = age;, без #.
const cat = new Animal("котка", 3);cat.age = 4;cat.age = -1;console.log(`${cat.species}, ${cat.age} г.`);Невалидна възраст: -1котка, 4 г.Още задачи
- Оценка. Клас
Gradeс име на предмет и оценка от 2 до 6. Setter-ът отказва всичко извън тези граници. Пробвай с 5, 7 и 1. - Продукт с отстъпка. Към
Productот урока добави метод за отстъпка в проценти, който приема само от 1 до 90%. Цената няма setter.
Тест