W poprzednim artykule skupiliśmy się na omówieniu podstawowych typów danych – prostych i złożonych w języku TypeScript. Dzisiaj zajmiemy się składnią klas, które ściśle związane są z pojęciem OOP. Zaznaczam, że celem artykułu nie jest wyjaśnienie paradygmatu programowania obiektowego.
Klasy
W TypeScript składnia klasy prezentuje się bardzo podobnie do tych, do jakich zdążył przyzwyczaić nas język C# czy Java:
class Square {
radius: number;
constructor(radius: number) {
this.radius = radius;
}
getArea(): number {
return this.radius * 2 * 3.14;
}
}
const square = new Square(5);
console.log(square.getArea());
Wywołanie powyższego kodu powinno w konsoli wyświetlić pole koła o promieniu równym 5. Jak możemy zauważyć, definicja klasy w języku TypeScript jest znacznie bardziej przejrzysta, aniżeli w przypadku wykorzystania mechanizmu prototypowania.
Modyfikatory dostępu
Język TypeScript udostępnia nam trzy modyfikatory dostępu:
- public
- private
- protected
Modyfikator umieszczamy przed deklaracją zmiennej lub metody. Domyślnie wszystkie składowe klasy są publiczne.
Hermetyzacja
Hermetyzacja, inaczej enkapsulacja (z ang. encapsulation) jest bardzo istotnym założeniem programowania obiektowego. Dzięki modyfikatorom możemy ograniczyć dostęp do istotnych składowych obiektu. Przykład:
class Square {
private radius: number;
constructor(radius: number) {
this.radius = radius;
}
}
const square = new Square(5);
console.log(square.radius);
Próba kompilacji powyższego kodu zakończy się komunikatem błędu:
Property ‚radius’ is private and only accessible within class ‚Square’.
Zmienna radius
jest określona jako prywatna, więc jest niedostępna z zewnątrz klasy. W jaki sposób uzyskać do niej dostęp – odczytać lub zmienić jej zawartość? TypeScript udostępnia nam dwa rozwiązania – przy pomocy funkcji lub przy pomocy gettera i settera.
- Przy użyciu funkcji:
public getRadius(): number {
return this.radius;
}
public setRadius(value: number): void {
if (value > 0) {
this.radius = value;
}
}
- Przy użyciu get i set:
get radius(): number {
return this._radius;
}
set radius(value: number) {
if (value > 0) {
this._radius = value;
}
}
W rzeczywistości getter i setter wykorzystuje Object.defineProperty
, toteż dostęp do nich uzyskujemy tak, jak gdybyśmy odnosili się do zwykłego pola (w powyższym przypadku – square.radius
).
Dziedziczenie
Kolejnym, ściśle związanym pojęciem z programowaniem obiektowym jest mechanizm dziedziczenia. W TypeScript składnia dziedziczenia jest taka jak w języku Java – wykorzystujemy słowo kluczowe extends
:
class Polygon extends Shape {
constructor(param: number) {
super(param);
}
}
Aby wywołać konstruktor rodzica, wykorzystujemy słowo super
. W podobny sposób możemy wywołać metodę rodzica – super.method()
. Ważne – TypeScript nie wspiera dziedziczenia wielobazowego.
Wraz z dziedziczeniem wiążę się pojęcie polimorfizmu. W TypeScript (do czego przyzwyczaić muszą się programiści C++ czy C#) wszystkie metody są domyślnie wirtualne.
Uwaga! Nie popełniaj uczelnianego błędu – zanim wykorzystasz dziedziczenie, rozważ użycie kompozycji.
Klasy abstrakcyjne
Do oznaczania klas abstrakcyjnych wykorzystujemy słowo kluczowe abstract
. Klasy abstrakcyjne mogą zawierać deklaracje metod abstrakcyjnych. Metoda abstrakcyjna nie zawiera ciała i musi zostać przeciążona w klasie pochodnej. Oczywiście – obiektów klasy abstrakcyjnej utworzyć się nie da.
abstract class Shape {
abstract draw(): void;
}
Konstruktor
W TypeScript konstruktor ma jeszcze jedną, dodatkową zaletę. Poniższy zapis:
class Polygon {
private param: number;
constructor(param: number) {
this.param = param;
}
}
Jest równoważny zapisowi skróconemu:
class Polygon {
constructor(private param: number) {}
}
Dzięki czemu unikamy zbędnego generowania kodu. Ważne – nie ma możliwości przeciążania konstruktorów.
Readonly
Wraz z TypeScript 2.0 pojawiło się nowe słowo kluczowe – readonly
. Dzięki niemu możemy deklarować składowe tylko do odczytu.
class Polygon {
readonly param: number;
}
Wartość właściwości param
musi byc ustawiona od razu (w miejscu deklaracji) lub w konstruktorze. Dostępny jest też zapis skrócony konstruktora:
class Polygon {
constructor(private readonly param: number) {}
}
Static
Oczywiście, nie zabrakło również możliwości deklarowania pól statycznych (słowo kluczowe static
).
class Polygon {
static count = 0;
constructor() {
Polygon.count++;
}
}
Podsumowanie
Dzisiejszy wpis miał na celu przedstawienie w możliwie najkrótszy sposób składni deklarowania klas w języku TypeScript. Tak jak podkreśliłem na samym początku artykułu, starałem się nie zagłębiać w definicje stricte dotyczące programowania zorientowanego obiektowo – dzięki temu wpis jest relatywnie krótki i pozwala na zapoznanie się ze wszystkimi pojęciami w niedługim czasie.