W ostatnim poście przedstawiłem podstawowy używania klas ze specyfikacji EcmaScript6 wewnątrz kodu TypeScript’owego. Dziś pociągnę temat i pokażę kolejne dwa przydatne mechanizmy.
Pierwszy z nich to tak zwane gettery i settery, czyli specjalne metody służące do manipulowania zawartościom pół w klasie. Drugi mechanizm to modyfikatory dostępu, dzięki którym w łatwy sposób można oznaczyć pole lub metodę jako prywatne lub publiczne.
Modyfikatory Dostępu
W TypeScript istnieją trzy rodzaje modyfikatorów dostępu: public, protected oraz private. Jeden z nich wykorzystałem już w poprzednim poście, przy okazji dodawanie argumentów do konstruktora. Jednak taki modyfikator daje więcej niż tylko możliwość skrócenia deklaracji konstruktora 🙂 .
Jakie jeszcze korzyści nam dają? Przede wszystkim, modyfikatory dostępu pozwalają na ograniczenie… dostępu, do pól klasy. W zależności od tego, jaki modyfikator wybierzemy, dane pole może być dostępne tylko wewnątrz klasy, wewnątrz klasy i klas od niej pochodnych, lub zarówno w klasie jak i poza nią.
W klasycznych językach obiektowych takich jak Java czy C#, dodawanie modyfikatorów dostępu do elementów klasy jest wymagane. W TS’a nie ma takiego wymogu, modyfikatory można je pominąć. Domyślnie wszystkie pola są publiczne (public). Takie pola i metody tego typu są dostępne zarówno w klasie jak i poza nią. Oto przykład:
1 2 3 4 5 6 7 8 9 10 11 12 |
class Cat { public color:string = 'black and white'; constructor(public name:string){ } public introduceCat():void{ console.log("I am "+this.name+" the cat."); } } var bonifacy:Cat = new Cat('bonifacy'); bonifacy.introduceCat(); console.log("this cat is",bonifacy.color) |
Klasa Cat posiada dwa pola (color oraz name) i jedną metodę (introduceCat). Wszystkie są publiczne. Dzięki temu mogę bez problemu wywołać te metodę i odnieść się do tych pól, poza obszarem klasy. Dokładnie tak jak robię to w przykładzie.
Tak jak pisałem, domyślnie wszystkie elementy są publiczne. Oznacza to, że w przykładzie powyżej, mógłbym usunąć modyfikator public przy color oraz introduceCat, efekt kodu byłby taki sam.
Co jednak stałoby się gdybym zmienił dostęp do pola color, na private? Momentalnie mój edytor zgłosiłby błąd w ostatniej linijce przykładowego kodu. W tej samej, w której chcę wywołać zawartość pola color, poza obszarem klasy. Mało tego, gdybym chciał skorzystać z opcji autouzupełniania w tym miejscu, edytor pokaże mi tylko publiczne elementy klasy. Prywatne pole nie będzie wypisane:
Oto jak mogę wybrnąć z tej sytuacji:
1 2 3 4 5 6 7 8 9 10 11 |
class Cat { private color:string = 'black and white'; constructor(public name:string){ } public introduceCat():void{ console.log("I am "+this.name+" the cat. I am "+this.color); } } var bonifacy:Cat = new Cat('bonifacy'); bonifacy.introduceCat(); |
Pole color, jest teraz wykorzystywane wewnątrz metody znajdującej się w klasie. Takie wykorzystanie prywatnego elementu jest w pełni dozwolone. Oczywiście, prywatne mogą być również metody. Jeżeli jakaś logika potrzebna jest tylko wewnątrz klasy, metodą ją trzymająca powinna być prywatna. Oto prosty przykład:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Cat { private color:string = 'black and white'; constructor(public name:string){ } private printIntroduction():string { let msg:string = "I am "+this.name+" the cat. I am "+this.color; return msg } public introduceCat():void{ console.log(this.printIntroduction()); } } var bonifacy:Cat = new Cat('bonifacy'); bonifacy.introduceCat(); |
Przeniosłem logikę sklecającą łańcuch znaków do osobnej, prywatnej, metody printIntroduction. Teraz publiczna metoda introduceCat tylko wywołuje tę metodę. Trochę przerysowany przykład, ale ładnie ilustruje to co chcę przekazać 🙂 .
Ostatni rodzaj modyfikatora dostępu to protected. Niestety w tym momencie nie mogę w pełni przedstawić jego funkcjonalności. Najpierw musiałbym wytłumaczyć dziedziczenie w klasach, a to temat na osobny post 🙂 . Póki co trzeba zapamiętać, że element protected, w przeciwieństwie do private, może być też używany w klasach które dziedziczą po klasie, w której go definiujemy.
Metody get oraz set
Kolejnym mechanizmem dostępnym w TS, o którym chcę dziś napisać są metody get oraz set, czyli tak zwane gettery oraz settery 🙂 .
Czym są te metody i co w nich takiego wyjątkowego? Można wyobrazić je sobie jako bardziej interaktywne pola i co najważniejsze wcale nie muszą występować w parze (choć najczęściej tak właśnie jest).
Najlepiej wytłumaczyć to na przykładzie. Chciałbym aby moja klasa Cat posiadała pole state, które zaimplementuję za pomocą metod get oraz set:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
enum catState { hungry = 1, fed, happy, } class Cat { private _state:catState = catState.hungry; constructor(public name:string){} public get state(){ return this._state; } public set state(newState){ this._state = newState } public introduceCat():void{ console.log("I am "+this.name+" the cat"); } } var bonifacy = new Cat('bonifacy'); console.log(bonifacy.state); bonifacy.state = catState.fed; console.log(bonifacy.state); bonifacy.introduceCat(); |
Na ten przykład składa się parę zmian, więc warto przestudiować go dokładniej. Przede wszystkim dodałem nowy typ na bazie enumeratora: catState. Chce aby pole state w mojej klasie miało właśnie ten typ.
Do klasy dodałem też nowe prywatne pole _state, którego typ to właśnie catState. Ale nie jest jeszcze moje pole state. Faktyczna implementacja nowego pola zaczyna się po konstruktorze. Znajdują się tam dwie metody set oraz get. Po ich deklaracji, znajduje się nazwa nowego pola czyli state. Metody te są publiczne, ale mogą mieć dowolny inny modyfikator dostępu. Jedyna zasada jest taka aby obie miały taki sam.
Teraz za każdym razem gdy odwołam się do pola state, wywołam odpowiednie get lub set. get wywołuję gdy tylko odczytuję treść pola, a set gdy chcę zmienić jego zawartość. W tej chwili logika tych funkcji jest bardzo prosta, po prostu przechowuję lub wywołuję wartość przechowywaną w prywatnej zmiennej, ale nic nie stoi na przeszkodzie aby dodać tam więcej kodu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
enum catState { hungry = 1, fed, happy, } class Cat { private _state:catState = catState.hungry; constructor(public name:string){} get state(){ console.log("getting state!") return this._state; } set state(newState){ console.log("setting state to",newState,"!") this._state = newState } public introduceCat():void{ console.log("I am "+this.name+" the cat"); } } var bonifacy = new Cat('bonifacy'); console.log(bonifacy.state); bonifacy.state = catState.fed; console.log(bonifacy.state); bonifacy.introduceCat(); |
Teraz za każdym razem gdy kod wejdzie w interakcję z polem state, wylogowany zostanie adekwatny komunikat. Może to nie jest zbyt użyteczny przykład, ale obrazuje możliwości tego rozwiązania.
Załóżmy, że nie chcę aby stan kota zmieniał się bezpośrednio z hungry na happy. W końcu głodny kot nie może być szczęśliwy 🙂 . Mogę zaimplementować odpowiednią logikę wewnątrz settera. Gdy zostanie wykonane nieodpowiednie przypisanie, zostanie zgłoszony błąd:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
enum catState { hungry = 1, fed, happy, } class Cat { private _state:catState = catState.hungry; constructor(public name:string){} get state(){ console.log("getting state!") return this._state; } set state(newState){ if(newState === catState.happy && this._state === catState.hungry){ throw("ERROR! A hungry cat cannot be happy :("); } else { this._state = newState; } } public introduceCat():void{ console.log("I am "+this.name+" the cat"); } } var bonifacy = new Cat('bonifacy'); console.log(bonifacy.state); bonifacy.state = catState.happy; console.log(bonifacy.state); bonifacy.introduceCat(); |
Ten fragment kodu zgłosi błąd po uruchomieniu. Myślę, że teraz już naprawdę wyraźnie widać jakie możliwości dają gettery i settery. Prawda jest taka, że używanie tych metod jest całkowicie opcjonalne. Tak naprawdę to zależy wyłącznie od stylu programisty. Niektórzy wolą taką logikę trzymać w osobnych funkcjach, argumentując to faktem iż gettery i settery powodują, że obiekty „puchną”. Innym to nie przeszkadza i wolą mieć logikę dotyczącą danego obiektu w jednym miejscu. Tak jak pisałem to zależy od upodobania, najważniejsze aby być konsekwentnym.
Czas powoli kończyć ten post, powoli robi się zbyt długi 🙂 . W kolejnym opiszę bodajże najważniejszą cechę klas czyli dziedziczenie. Omówię też czym są klasy abstrakcyjne i wspomnę o implementacji interfejsów do klas. Będzie to już ostatni post o klasach w TypeScript.
Jeżeli chcesz być na bieżąco z postami na blogu zachęcam do polubienia mojej strony na facebooku. Zawsze zamieszczam tam informacje o wszystkich nowościach. Jest to też dobre miejsce na kontakt ze mną. Na wszystkie pytania zawsze odpowiem :).
Super materiały, czytam z zainteresowaniem 🙂
Przy okazji raportuję literówkę/błąd do poprawy: „zawartościom pół” → „zawartością pól”