Hermetyzacja — pilnuj zasad swojego obiektu
Portfel nie powinien pozwalać na wydanie nieistniejących monet. Ukryj stan i wystaw metody, które sprawdzają zasady przed zmianą.
Po tej lekcji potrafisz
- Dobierzesz public, private, protected i readonly.
- Użyjesz get, set i static w konkretnym przykładzie.
- Odróżnisz private TypeScriptu od pola # prywatnego podczas wykonania.
Ktoś dopisał sobie milion monet
Publiczne pole można ustawić z dowolnego miejsca w kodzie. Trudno wtedy ustalić, czy zasady były sprawdzone. Hermetyzacja oznacza, że obiekt udostępnia kontrolowane operacje zamiast dowolnej zmiany całego wnętrza.
Małe kroki, konkretne efekty
Dostęp do pól ma znaczenie
public jest domyślne i pozwala na dostęp z zewnątrz. private ogranicza dostęp w kodzie TypeScript do danej klasy. protected dopuszcza też klasy pochodne. readonly pozwala ustawić pole przy deklaracji lub w konstruktorze, a potem zabrania przypisania przez ten typ.
class Konto {
private monety = 10;
constructor(public readonly nick: string) {}
get saldo(): number { return this.monety; }
}
const konto = new Konto("Ala");
console.log(konto.nick, konto.saldo);
Ala 10
Getter odczytujemy jak pole, bez nawiasów. Brak settera oznacza, że konto.saldo nie może być przypisywane w zwykłym kodzie TypeScript.
Setter sprawdza, static należy do klasy
Setter może odrzucić zmianę lub poprawić wartość — tę decyzję trzeba opisać. static jest wspólne dla klasy, a nie osobne dla każdej instancji.
class Ustawienia {
static readonly maksimum = 100;
private poziom = 50;
get glosnosc(): number { return this.poziom; }
set glosnosc(n: number) {
if (!Number.isFinite(n)) { return; }
this.poziom = Math.max(0, Math.min(Ustawienia.maksimum, n));
}
}
const u = new Ustawienia();
u.glosnosc = 150;
console.log(u.glosnosc, Ustawienia.maksimum);
100 100
W tym przykładzie liczba poza zakresem zostaje przycięta. Nie jest to uniwersalna zasada setterów. Metoda Math.min wybiera mniejszą wartość, Math.max większą.
Portfel, który nie schodzi poniżej zera
Obie operacje zwracają boolean informujący o powodzeniu. Monety są nieujemnymi liczbami całkowitymi; wartość stanu jest prywatna także w działającym JavaScripcie.
class Portfel {
#monety = 0;
get saldo(): number { return this.#monety; }
wplac(ile: number): boolean {
if (!Number.isSafeInteger(ile) || ile <= 0 || !Number.isSafeInteger(this.#monety + ile)) { return false; }
this.#monety += ile;
return true;
}
wydaj(ile: number): boolean {
if (!Number.isSafeInteger(ile) || ile <= 0 || ile > this.#monety) { return false; }
this.#monety -= ile;
return true;
}
}
const p = new Portfel();
console.log(p.wplac(20));
console.log(p.wydaj(30));
console.log(p.wydaj(7));
console.log(p.saldo);
true
false
true
13
Jak program dochodzi do wyniku
- #monety ma prywatność egzekwowaną przez JavaScript. private w TypeScripcie samo nie zapewnia takiej bariery runtime.
- Getter udostępnia odczyt liczby, ale nie możliwość bezpośredniego ustawienia salda.
- Walidacja sprawdza również bezpieczny zakres liczb całkowitych.
- Nieudane wydanie 30 monet nie zmienia stanu.
Zanim przejdziesz dalej
Co zwróci wydaj(-5) i czy saldo wzrośnie?
Sprawdź swoje rozumowanie
Zwróci false. Warunek odrzuca niedodatnią kwotę przed odejmowaniem, więc ujemna wypłata nie zamieni się we wpłatę.
Zmień kod i zobacz, co się stanie
readonly nie zamraża zawartości
readonly przy polu tablicowym blokuje podmianę pola, ale nie metody tablicy. Do ograniczenia operacji na elementach potrzebny jest też typ readonly number[].
class Historia {
readonly punkty: number[] = [];
dopisz(n: number): void { this.punkty.push(n); }
}
const h = new Historia();
h.dopisz(5);
const podglad: readonly number[] = h.punkty;
console.log(podglad.join(", "));
5
podglad nie pozwala wywołać push przez tę zmienną, ale nadal wskazuje tę samą tablicę. Ograniczenie typów nie jest głębokim zamrożeniem obiektu.
Gdy wynik zaskakuje
| Pomyłka | Co się dzieje i jak to poprawić |
|---|---|
| private jako zabezpieczenie przed użytkownikiem aplikacji | To ograniczenie w kodzie, nie autoryzacja operacji. # chroni dostęp do pola, ale też nie zastępuje reguł serwera. |
| Getter wywołany z () | Getter czytasz jak właściwość: p.saldo. |
| static odczytywane przez instancję | Użyj nazwy klasy, np. Ustawienia.maksimum. |
Teraz Twoja kolej
Rozwiązanie jest jedną z możliwości. Zanim je odkryjesz, uruchom własną wersję dla podanych danych i przynajmniej jednego przypadku brzegowego.
Prywatny licznik startuje od 0. Zwiększ go metodą i odczytaj getterem.
Mała podpowiedź
Oddziel prywatne pole od nazwy gettera.
Przykładowe rozwiązanie z wyjaśnieniem
class Licznik {
private n = 0;
dodaj(): void { this.n++; }
get wartosc(): number { return this.n; }
}
const l = new Licznik(); l.dodaj();
console.log(l.wartosc);
1
Kod korzystający z klasy ma jasną drogę modyfikacji.
Setter nick ma przyjmować tekst długości 3–12 po trim. Niepoprawna zmiana zachowuje poprzedni nick „Gracz”. Sprawdź „ab” i „ Pixel ”.
Mała podpowiedź
Najpierw przygotuj tekst, potem sprawdź długość.
Przykładowe rozwiązanie z wyjaśnieniem
class Profil {
private nazwa = "Gracz";
get nick(): string { return this.nazwa; }
set nick(n: string) {
const tekst = n.trim();
if (tekst.length >= 3 && tekst.length <= 12) { this.nazwa = tekst; }
}
}
const p = new Profil(); p.nick = "ab"; console.log(p.nick);
p.nick = " Pixel "; console.log(p.nick);
Gracz
Pixel
Nieudana zmiana nie niszczy poprawnego stanu. Długość dotyczy jednostek UTF-16, więc dla emoji wymagałaby innej reguły.
Każde new Druzyna ma zwiększać licznik static. Po dwóch obiektach oczekujemy 2.
Mała podpowiedź
Konstruktor aktualizuje pole klasy.
Przykładowe rozwiązanie z wyjaśnieniem
class Druzyna {
static liczba = 0;
constructor(public nazwa: string) { Druzyna.liczba++; }
}
new Druzyna("A"); new Druzyna("B");
console.log(Druzyna.liczba);
2
Licznik jest wspólny. W wersji produkcyjnej można ukryć go i udostępnić getterem static.
Prywatny poziom startuje od 100. zuzyj przyjmuje dodatnią liczbę całkowitą i odrzuca koszt większy niż zapas. Dla 70, 40 oczekujemy true, false i poziomu 30.
Mała podpowiedź
Wszystkie warunki sprawdź przed odejmowaniem.
Przykładowe rozwiązanie z wyjaśnieniem
class Bateria {
#poziom = 100;
get poziom(): number { return this.#poziom; }
zuzyj(n: number): boolean {
if (!Number.isInteger(n) || n <= 0 || n > this.#poziom) { return false; }
this.#poziom -= n; return true;
}
}
const b = new Bateria();
console.log(b.zuzyj(70), b.zuzyj(40), b.poziom);
true false 30
Obiekt zachowuje swój warunek: poziom nigdy nie spada poniżej zera.
Co zabierasz do następnej lekcji
- Udostępniaj działania zgodne z regułami obiektu.
- get i set wyglądają przy użyciu jak pola, static należy do klasy.
- private, # i readonly mają różne zadania i zakres ochrony.