W zeszłym roku przedstawiłem na blogu większość mechanizmów działania TypeScriptu. Do omówienia została mi jeszcze jedna rzecz – Dekoratory. Jest to zdecydowanie bardziej zaawansowany aspekt języka, ale i tak nie powinien być trudny do pojęcia. Nawet jeżeli na początku idea dekoratorów będzie wydawać się skomplikowana, uważam, że warto poświęcić trochę energii na zrozumienie tego zagadnienia.
TypeScript daje możliwość korzystania z kilku rodzajów dekoratorów, ja dziś przedstawię jeden z nich – dekoratory metod. Powinien idealnie sprawdzić się jako wstęp do tematu.
Na początek najważniejsze pytanie. Czym są dekoratory? Dekorator to narzędzie do modyfikowania zachowania klasy lub metody. Za pomocą dekoratora możemy w bardzo prosty sposób dodać do istniejącej klasy lub metody dodatkową logikę.
Jeżeli, na przykład, potrzebujemy aby pewne metody w kilku różnych klasach, oprócz wykonywania swoich podstawowych zadań podczas wywołania, dodatkowo logowały odpowiednią informacje w konsoli, to mamy przynajmniej dwa wyjścia. Pierwsze z nich to dopisać console.log do każdej metody z osobna. Niestety rezultatem tego będzie kod trudny w utrzymaniu. Drugie wyjście to użycie dekoratora metod i tą drogą pójdziemy.
Pokażę teraz w kodzie jak to zrobić, ale zanim przejdę do konkretów muszę zaznaczyć jeszcze jedną ważną rzecz. Aby dekoratory działały w naszym programie, należy w projektowym pliku tsconfig.json, w obiekcie compilerOptions należy dodać następującą linijkę kodu:
1 |
"emitDecoratorMetadata" : true |
Bez tego kompilator mógłby zgłaszać błędy. Jak już się z tym uporam, to mogę zacząć pisanie. W projekcie tworzę dwa pliki: script.ts oraz decorators.ts. W pierwszym będę trzymał główną logikę, drugi będzie modułem zawierającym mój przykładowy dekorator. Mógłbym wszystko zapisać w jednym pliku, ale jeżeli mamy opcję dzielenia kodu na moduły, to dobrze jest z niej korzystać.
Póki co, plik decorators.ts zawiera tylko pustą, eksportowaną na zewnątrz deklarację funkcji:
1 2 3 |
export function log(){ } |
Natomiast zawartość scripts.ts wygląda tak:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import { log } from "./decorators"; class Dog { constructor(name : string) { this.name = name; } public bark(public something : string) : string { return this.name + ", the Dog says: " + something; } } let maja = new Dog("maja"); console.log(maja.bark("szczek szczek")); |
Jak widać, nie dzieje się tu jeszcze nic specjalnego. Na początku importuję funkcję log z modułu decorators. Następnie dodaję do skrypty deklarację prostej klasy Dog. Gdy to mam już gotowe, tworzę instancję klasy Dog i loguję wynik wywołania jej metody bark.
Skrypt ten uruchamiam w terminalu poprzez node’a. Wynik jest dokładnie taki jakbym się spodziewał, w konsoli pojawia się odpowiednia wiadomość. Czas dodać „udekorować” metodę bark.
Aby do metody bark dodać dekorator, muszę zastosować odpowiednią składnie. Przed deklaracją metody, wpisuję nazwę dekoratora (który może być dowolną funkcją dostępną w aktualnym zakresie) poprzedzoną znakiem ‚małpy’ – @.
Tak wygląda metoda bark z dodanym dekoratorem:
1 2 3 4 |
@log public bark(something : string) : string { return this.name + ", the Dog says: " + something; } |
Dodanie dekoratora spowoduje jednak , że zgłoszony zostanie błąd. Stanie się tak ponieważ funkcja log, która ma być dekoratorem nie jest poprawnie zaimplementowana. Muszę najpierw dodać do niej argumenty:
1 2 3 |
export function log(target: any, name: string, descriptor: any){ } |
Jak widać dekorator metody przyjmuje trzy argumenty. Pierwszy, target, to wskaźnik na obiekt, z którego pochodzi metoda. Drugi to argument to łańcuch znaków zawierający nazwę dekorowanej metody. Ostatni argument to metadane związane z metodą.
Aby lepiej zilustrować co jest czym, zmienię kod dekoratora w następujący sposób:
1 2 3 4 5 |
export function log(target: any, name: string, descriptor: any){ console.log(target) console.log(name) console.log(descriptor) } |
Teraz wywołanie skryptu powinno zwrócić w konsoli coś podobnego do tego:
1 2 3 4 5 6 7 |
Dog { bark: [Function] } bark { value: [Function], writable: true, enumerable: true, configurable: true } maja, the Dog says: szczek szczek |
Jak widać wszystkie logi pokazały co trzeba. Co ciekawe na końcu uruchomiła się standardowa funkcjonalność metody bark. To dlatego, że metoda ta, została jakby opleciona dekoratorem.
Mnie najbardziej interesuje pole value z ostatniego argumentu, czyli obiektu descriptor. value zawiera nic innego jak wskaźnik na wywoływaną metodę.
Mając taki dostęp, mogę zmodyfikować dekorowaną metodę. Zmienię swój dekorator w następujący sposób:
1 2 3 4 5 6 7 |
export function log(target: any, name: string, descriptor: any){ console.log("log z dekoratora") descriptor.value = function () { console.log("log z funkcji") return ("zwracana wartość, log z glownego skryptu") } } |
Pierwsza linijka kodu to logika dodana w dekoratorze. Następnie dobieram się do dekorowanej funkcji i nadpisuję ją własną, która też loguję wiadomość, oraz zwraca łańcuch znaków. Ten zwracany łańcuch trafi ostatecznie do console.log, w głównym skrypcie, gdzie wywołuję główną metodę.
Prawda, że sprytne 🙂 .
Na koniec przedstawię trochę bardziej „życiowe” wykorzystanie dekoratora metod. Oto nowy kod modułu decorators:
1 2 3 4 5 6 7 8 |
export function log(target: any, name: string, descriptor: any){ var originalMethod = descriptor.value; descriptor.value = function (...args: any[]) { var result = originalMethod.apply(this, args); console.log(`Wywoluje: ${name}. Oto rezulat: ${result}`); return result; } } |
Tym razem w zmiennej originalMethod tworzę kopię dekorowanej metody. Następnie nadpisuję dekorowaną metodę nową funkcją, w której wywołuję oryginalną metodę, używając apply. Zwracaną wartość przypisuję do nowej zmiennej. Gdy to jest już gotowe loguję wiadomość o tym, że funkcja została wywołana i zwracam rezultat tak aby program mógł operować na nim dalej.
Tak napisany dekorator można teraz z łatwością dodać do nowej metody. Powiedzmy, że moja klasa Dog otrzyma metodę eat:
1 2 3 |
public eat() : string { return this.name + " is not hungry!" } |
Wystarczy, że dodam nad tą deklaracją @log i każde wywołanie eat również będzie logować odpowiednią wiadomość.
Mam nadzieję, że udało mi się pokazać jak potężnym narzędziem mogą być dekoratory. Zachęcam do zabawy z tym mechanizmem na własną rękę. Można na przykład pobawić się innymi dekoratora argumentami. Takie eksperymenty najlepiej pomogą w szybkim zrozumieniu zagadnienia.
To wszystko na dziś. 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 :).
lakonicznie bardzo.