Към съдържанието
wannacode

Капсулация

Проблем

Минус 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. Ще я срещнеш в по-стар код.

Идеята

Да четеш, без да пипаш

Отвън вече не можем и да прочетем баланса. Затова класът дава метод, който само го връща:

bank.js
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 getter

withdraw вече връща true или false: дали тегленето е минало. Така който тегли, разбира дали е успял.

const account = new Account("Мария", 100);
account.withdraw(500);
account.deposit(50);
console.log(account.balance);
Стъпка по стъпка, с думи
  1. #balance е частно: стъклото значи, че отвън до полето няма път.
  2. Тегленето минава през проверката в метода: 500 е повече от 100. Отказано, балансът не се пипа.
  3. Внасянето също се проверява: 50 е положителна сума. Методът я добавя.
  4. Getter-ът balance дава навън копие на баланса. Самото поле #balance остава скрито.

Пробвай

Отказано

Какво ще отпечата програмата?

const account = new Account("Мария", 100);
console.log(account.withdraw(150));
account.deposit(-20);
console.log(account.balance);
Отговор

Идеята

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);
Изход
4

set 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, а балансът пак става -500:

const account = new Account();
account.balance = 100;
account.withdraw(30);
account.balance = -500;
console.log(account.balance);
Изход
-500

Кой ред в класа пробива защитата?

Избери реда с грешката.

Упражнение

Скрити данни

Реши задачите в редактора си. Всички полета са частни, с #.

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} €`);
Изход
false
true
Баланс: 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.

Тест

Провери се

1. Полето е #balance. Откъде може да се променя?
2. Защо сметката няма setter за баланса?
3. В set age(value) какво е value?
4. Какво значи капсулация?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·