Autor podstrony: Krzysztof Zajączkowski

Stronę tą wyświetlono już: 3748 razy

Podstawy deklaracji klas w TypeScript-cie

Tworzenie klas w czystym JavaScript-cie było dość zabawnym doświadczeniem. Dziedziczenie zaś jeszcze zabawniejszym. A wszystko to dlatego, że w JavaScript-cie obiekty to funkcje. Cóż, w TypeScript-cie jest tak samo tylko, że inaczej. To znaczy w kodzie pisanym przez programistę do utworzenia obiektu klasy używa się słowa kluczowego class, jednakże podczas kompilacji wszystko to jest zamieniane na postać czysto JavaScript-ową. Oto prosty przykład deklaracji klasy w TypeScript-cie:

export class Author { // inicjalizacja danych pustym ciągiem znaków name: string = ''; surname: string = ''; // getter jest to funkcja, która zwraca daną wartość a wywołuje się ją tak, jakby to było pole klasy // getter równocześnie może zabezpieczać przed nadpisywaniem jakiejś zmiennej // może również wyliczać jakąś wartość w locie i ją zwracać get nameAndSurname(): string { return this.name + ' ' + this.surname; } } // klasa kiążki, która zawiera tablicę obiektów klasy Author export class Book { title: string; authors: Author[]; nrOfPages: number; price: number; // konstruktor umożliwiający ustawienie danych obiektu przy jego tworzeniu constructor(title: string, authors: Author[], nrOfPages: number, price: number) { this.title = title; this.authors = authors; this.nrOfPages = nrOfPages; this.price = price; } }

Tworzenie obiektu klasy Book będzie wyglądało następująco:

let book = new Book('Rio Anaconda', [ Author.CreateAuthor('Wojciech', 'Cejrowski') ], 100, 45);

Pola i metody klasy domyślnie w TypeScript-cie są dostępne publicznie. Można tworzyć pola i metody, które są typu prywatnego private (dostępne jedynie w obrębie klasy) lub chronione protected (dostępne jedynie w obrębie danej klasy i z poziomu klasy dziedziczącej).

Tak jak w JavaScript-cie tak i w TypeScript-cie konstruktora i metod klasy nie da się przeciążyć jak w innych językach programowania. Można jednakże przekazać do metody klasy argument, będący tablicą różnych typów i w ten sposób rozszerzyć funkcjonalność klasy. Możliwe jest też stosowanie standardowego zastosowania np. zmiennych inicjowanych wartościami domyślnymi np. tak:

constructor(title: string = '', authors: Author[] = [], nrOfPages: number = 100, price: number = 100) { this.title = title; this.authors = authors; this.nrOfPages = nrOfPages; this.price = price; }

Gettery i settery

Warto zapoznać się z możliwością kontroli dostępu do danych z wykorzystaniem getterów i setterów, będących specjalnymi metodami, które mogą być wywoływane tak, jakby były polami klasy. Gettery umożliwiają zwracanie wartości czy to prywatnej, czy to chronionej czy też wyliczanej w locie na podstawie dostępnych w klasie wartości. Settery umożliwiają ustawianie prywatnych i publicznych zmiennych jak również pozwalają na wykonywanie równoczesne szeregu innych operacji, które powinny się wykonać w trakcie ustawiania zmiennych.

Przykład gettera został utworzony już w powyższym przykładzie. Tworzy się go przy użyciu słowa kluczowego get. Getter jest metodą, która nie przyjmuje żadnych wartości a jedynie zwraca wartość. W TypeScript-cie można również podać typ zwracanych danych. Przykładowy getter ma więc postać np. taką:

export class Author { // inicjalizacja danych pustym ciągiem znaków name: string = ''; surname: string = ''; // getter jest to funkccja, która zwraca daną wartość a wywołuje się ją tak, jakby tobyło pole klasy // getter równocześnie może zabezpieczać przed nadpisywaniem jakiejś zmiennej // może rónież wyliczać jakąś wartość w locie i ją zwracać get nameAndSurname(): string { return this.name + ' ' + this.surname; } }

Wywołanie gettera wyglądać będzie następująco:

let author = new Author(); author.name = 'Wojciech'; author.surname = 'Cejrowski'; // wywołanie gettera obiektu klasy Author console.log(author.nameAndSurname);

Jak widać, choć getter jest funkcją, to wywołuje się go tak, jakby był polem klasy. Takie sztuczne pola klasy nazywane są właściwościami.

Settery w odróżnieniu od getterów nie zwracają żadnej wartości, ale przyjmują jeden argument wykorzystywany do ustawienia innych zmiennych. Załóżmy więc, że mam do stworzenia klasę opisującą prosty twór, jakim jest prostokąt. Prostokąt jak to prostokąt ma pewne właściwości, do których należą: długość boku a; długość przekątnej p no i jeszcze dajmy na to pole powierzchni area. Wszystkie te trzy zmienne są z sobą skorelowane, oznacza to, że zmieniając długość boku a prostokąta zmienia się równocześnie długość przekątnej p oraz pole powierzchni area prostokąta. Do opisu prostokąta użyję tylko jednego pola opisującego długość boku a pozostałe zmienne będą getterami i setterami umożliwiającymi przeliczanie, pobieranie lub ustawianie wartości. Oto przykład:

export class Square { a: number; constructor(a: number) { this.a = a; } get p(): number { return this.a * Math.sqrt(2); } set p(pValue: number) { this.a = pValue / Math.sqrt(2); } get area(): number { return this.a * this.a; } set area(areaValue: number) { this.a = Math.sqrt(areaValue); } print() { console.log('Cechy obiektu prostokąta:'); console.log('Długość boku a: ' + this.a); console.log('Długość przekątnej b: ' + this.p); console.log('Pole powierzchni Ppow: ' + this.area); } }

Przykład utworzenia instancji klasy Square oraz wykorzystanie setterów można zobaczyć poniżej:

const square: Square = new Square(10); square.print(); square.p = 10; square.print(); square.area = 10; square.print();

Wynikiem działania tego kodu będzie wyświetlenie w konsoli przeglądarki np. Firefoxa lub Chrome następującej treści:

Cechy obiektu prostokąta:
Długość boku a: 10
Długość przekątnej b: 14.142135623730951
Pole powierzchni Ppow: 100
Cechy obiektu prostokąta:
Długość boku a: 7.071067811865475
Długość przekątnej b: 10
Pole powierzchni Ppow: 49.99999999999999
Cechy obiektu prostokąta:
Długość boku a: 3.1622776601683795
Długość przekątnej b: 4.47213595499958
Pole powierzchni Ppow: 10.000000000000002

Implementacja interfejsów w deklaracji klasy

Klasy mogą implementować interfejs, który będzie wymuszał zadeklarowanie w klasie wszystkich wymaganych przez dany interfejs pól i metod. Taką implementację wykorzystują Angular-owe haki do wymuszenia na programiście obsługi z góry określonej metody w celu zapewnienia obsługi określonej funkcjonalności. Taką funkcjonalnością jest np. uruchomienie metody ngOnInit, która przez Angulara będzie wywoływana, gdy dane klasy zostaną zainicjalizowane.

Oto prosty przykład wykorzystania własnego interfejsu do wymuszenia obsługi określonej funkcjonalności:

export interface TitleI { title: string; printTitle(); } class MusicAlbum implements TitleI { title: string; printTitle() { console.log('Tytuł albumu muzycznego: ' + this.title); } } class Movie implements TitleI { title: string; printTitle() { console.log('Tytuł filmu' + this.title); } } class Book implements TitleI { title: string; printTitle() { console.log('Tytuł książki: ' + this.title); } }

Dziedziczenie w TypeScript-cie

W TypeScript-cie do dziedziczenia używa się słowa kluczowego extends, które pozwala rozszerzyć daną klasę o funkcjonalności innej klasy lub klas. Oto przykład zastosowania tego mechanizmu:

export class Title { title: string; printTitle() { console.log('Tytuł: ' + this.title); } } export class MusicAlbum extends Title { author: Author; nrOfTracks: number; constructor(author: Author, nrOfTracks: number = 0, title: string = '') { super(); // wywołanie konstruktora klasy bazowej } } export class Movie extends Title { author: Author; constructor(author: Author, title: string) { super(); // wywołanie konstruktora klasy bazowej } }

Przy dziedziczeniu możliwe konieczne jest wywołanie konstruktora klasy bazowej za pomocą funkcji super, która jako argumenty przyjmuje takie same argumenty co sam konstruktor klasy bazowej (czyli w tym przypadku żadne).

Zaletą dziedziczenia jest to, że część kodu zostaje wydzielona do oddzielnej klasy i nadaje się ona do wielokrotnego użytku w wielu innych klasach. Wadą zaś jest to, że klasa bazowa nie może spersonalizować danych w niej związanych w sposób łatwy. To może zrobić jedynie klasa dziedzicząca. Chodzi mi tutaj np o opis jaki może zwracać metoda printTitle: Film pod tytułem: "Tajemnica Andromedy", który w przypadku dziedziczenia jest nie możliwy do zrealizowania w rozsądny sposób. Można jedynie stworzyć tutaj opis typu: Tytuł: "Tajemnica Andromedy" ale czego to jest tytuł to może wiedzieć tylko klasa dziedzicząca.

Klasy abstrakcyjne

W TypeScript klasy abstrakcyjne jak sama nazwa już podpowiada to takie klasy, których obiektów nie da się utworzyć. Potrzebne one są np. do wydzielenia części kodu lub/oraz przechowywania różnych obiektów dziedziczących po tej klasie w jednej tablicy, której typ odpowiada typowi klasy abstrakcyjnej. Sam obiekt klasy tego typu nie może zostać utworzony, ponieważ jego istnienie jest pozbawione sensu. Dla przykładu wszystkie figury płaskie mają takie cechy jak:

Lecz każda figura płaska ma inne wzory do obliczania tychże wartości. Utworzenie obiektu figura jest więc mało sensowne, ale utworzenie obiektu klasy np. prostokąt już będzie taki sens miało. Oto przykład deklaracji klasy Shape będąca klasą abstrakcyjną i klasy Square i Circle, które dziedziczą po klasie Shape:

export abstract class Shape { abstract get area(): number; abstract set area(areaValue: number); abstract drawShape(): void; } export class Square extends Shape { a: number; constructor(a: number) { super(); this.a = a; } get p(): number { return this.a * Math.sqrt(2); } set p(pValue: number) { this.a = pValue / Math.sqrt(2); } get area(): number { return this.a * this.a; } set area(areaValue: number) { this.a = Math.sqrt(areaValue); } drawShape() { console.log('Cechy obiektu prostokąta:'); console.log('Długość boku a: ' + this.a); console.log('Długość przekątnej b: ' + this.p); console.log('Pole powierzchni Ppow: ' + this.area); } } export class Circle extends Shape { r: number; constructor(r: number) { super(); this.r = r; } get area(): number { return this.r * this.r * Math.PI; } set area(areaValue: number) { this.r = Math.sqrt(this.r / Math.PI); } drawShape(): void { console.log('Cechy obiektu koła:'); console.log('Promień: ' + this.r); console.log('Pole powierzchni Ppow: ', this.area); } }

Teraz trochę magii:

const square: Square = new Square(10); square.drawShape(); console.log('============================='); const circle: Circle = new Circle(20); circle.drawShape(); console.log('============================='); // tutaj przechowuję w zmiennej tablicowej dwa obiekty różnych typów jako tablicę klasy abstrakcyjnej Shape const shapes: Shape[] = [square, circle]; shapes.forEach((shape: Shape) => { shape.drawShape(); console.log('============================='); });

Wynik działania powyższego kodu:

Cechy obiektu prostokąta:
Długość boku a: 10
Długość przekątnej b: 14.142135623730951
Pole powierzchni Ppow: 100
=============================
Cechy obiektu koła:
Promień: 20
Pole powierzchni Ppow:  1256.6370614359173
=============================
Cechy obiektu prostokąta:
Długość boku a: 10
Długość przekątnej b: 14.142135623730951
Pole powierzchni Ppow: 100
=============================
Cechy obiektu koła:
Promień: 20
Pole powierzchni Ppow:  1256.6370614359173
=============================