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ą.

CEL LEKCJI

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.
WPROWADZENIE

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.

POZNAJ SKŁADNIĘ

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.

src/index.ts — osobny przykład
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);
wynik w konsoli
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.

src/index.ts — osobny przykład
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);
wynik w konsoli
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ą.

PRZYKŁAD Z OMÓWIENIEM

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.

src/index.ts — osobny przykład
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);
wynik w konsoli
true
false
true
13

Jak program dochodzi do wyniku

  1. #monety ma prywatność egzekwowaną przez JavaScript. private w TypeScripcie samo nie zapewnia takiej bariery runtime.
  2. Getter udostępnia odczyt liczby, ale nie możliwość bezpośredniego ustawienia salda.
  3. Walidacja sprawdza również bezpieczny zakres liczb całkowitych.
  4. 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ę.

EKSPERYMENT

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[].

src/index.ts — osobny przykład
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(", "));
wynik w konsoli
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.

CZĘSTE BŁĘDY

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.
ZADANIA

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.

ZAD 1Odczyt licznika★☆☆

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
src/index.ts — osobny przykład
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);
wynik w konsoli
1

Kod korzystający z klasy ma jasną drogę modyfikacji.

ZAD 2Limit długości nicku★★☆

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
src/index.ts — osobny przykład
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);
wynik w konsoli
Gracz
Pixel

Nieudana zmiana nie niszczy poprawnego stanu. Długość dotyczy jednostek UTF-16, więc dla emoji wymagałaby innej reguły.

ZAD 3Licznik utworzonych drużyn★★☆

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
src/index.ts — osobny przykład
class Druzyna {
  static liczba = 0;
  constructor(public nazwa: string) { Druzyna.liczba++; }
}
new Druzyna("A"); new Druzyna("B");
console.log(Druzyna.liczba);
wynik w konsoli
2

Licznik jest wspólny. W wersji produkcyjnej można ukryć go i udostępnić getterem static.

ZAD 4Bateria robota★★★

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
src/index.ts — osobny przykład
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);
wynik w konsoli
true false 30

Obiekt zachowuje swój warunek: poziom nigdy nie spada poniżej zera.

PODSUMOWANIE

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.