TypeScript’in Sihirli Özelliği: Decorator

TypeScript’in stage 2 özelliği olan “decorator“ konusuna gelin yakından bakalım

TypeScript’in Sihirli Özelliği: Decorator

Daha önce Java, C# gibi dillerle uğraştıysanız sınıf, fonksiyon veya parametrelerin önüne yazılan ve kodun çalışmasını etkileyebilen, köşeli parantez benzeri şeyler kullanılan garip yazımlı kelimelerle karşılaşmışsınızdır. Peki bunların ne olduğunu veya TypeScript ile yapılarınızı nasıl sarmalayacağınızı yahut onlara bazı anlamlar yükleyebileceğinizi hiç merak ettiniz mi? Gelin bunlara birlikte bakalım.

Decorator Nedir?

Decorator, sınıflarla ve içindeki methotlarla, parametrelerle, property’lerle, erişimcilerle etkileşime girebilen bir tür özel fonksiyonlardır. Bu bahsettiğimiz şeyleri değiştirmemize, içine yeni değerler eklememize, değerlerine göre farklı aksiyonlar almamıza ve de bunlara meta data olarak adlandırdığımız bilgi yükleme olanaklarını bize sağlar.

JavaScript’te Dekoratörler henüz stage 2 seviyesinde olup TypeScript’te ise deneysel olarak kullanılmaktadır. TS 5.0 ile stage 3 seviyesine ilerlemiş ve kullanımında değişiklikler yapılmıştır. Bu yazı stage 2 özelliklerini anlatmaktadır.

Decorator’lar ile annotations ve meta-programming yapılması mümkün hale gelmiştir. Meta data’nın ne olduğunu ve bununla neler yapılabileceğini “C# Attribute’lara Derinlemesine Bakış” adlı yazımdan detaylıca öğrenebilirsiniz.

Bunlar dışında nesne üzerindeki verileri doğrulayabilir, fonksiyonunuz çağrıldığında ondan önce çalışacak bir sarmayalıcı oluşturabilirsiniz.

Nasıl kullanılır?

Decorator’ler hala dile tam olarak eklenmediği için TypeScript Compailer’a decorator kullanacağımızı belirtmemiz gerekiyor. Derleme komutunuza --target ES5 --experimentalDecorators ekleyerek ya da tsconfig.json dosyanıza aşağıdaki key’i ekleyerek kullanıma hazır hale gelebilirsiniz.

"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}

Dekorator’ler @decoratorİsmi şeklinde çağrılır. Run time anında decoratorİsmi fonksiyonuna ifade ettiği alanın bilgileri verilerek çağrılır. İlgili alanların önüne ya da bir üst satırına yazılarak kullanılabilirler ve içlerine parametre alabilirler.

@veritabanıTabloİsmi('users')
class Kullanıcı {
@benzersizOlmalı
email: string;

@yöneticiMi
set birimNumarası() {}
@methodDekoratörü
fly( @büyükOlmalıdır(10) meters: number ) {}
}

Çoklu Decorator kullanımı

Aynı alan için bieden fazla dekorator kullanımı mümkündür.

Aralarına boşluk bırakarak tek satırda yazılabilir.

@a @b @c
function kaydet(){}

veya alt alta kullanabilirsiniz.

@a
@b
@c
function kaydet(){
console.log('kaydet fonksiyonu çalişti');
}

Değerlendirilme ve Çağrılma Sıraları

Sınıf dekoratörü new anahtar kelimesi kullanıldığında diğerleri ise bulunduğu method tetiklendiği anda ilk önce değerlendirilir ardından ise çağrılır. Değerlendirme işlemi Decorator Factory kısmında bahsedeceğimiz fonksiyonların gövdesinden ibarettir. İlk önce alana tanımlı olan Factory gövdeleri çalıştırılır ardından ise Factory’nin geri döndüğü ana dekoratör.

Üyeler arasındaki sıralama

Dekoratörlerin birbirleri arasındaki çağırılma sırası şu şekildedir:

1- Parametre dekoratörleri ardından method, erişimci ve son olarak property dekoratörleri sınıftan üretilen instance için çalıştırılır.

2- Parametre dekoratörleri ardından method, erişimci ve son olarak property dekoratörleri sınıfın statik üyeleri için çalıştırılır.

3- Kurucu fonksiyonların parametre dekoratörleri çalıştırılır.

4- Sınıfın dekoratörü çalıştırılır.

Yani daha akılda kalıcı olacak şekilde söylersek dekoratörler içeriden dışarıya doğru sıralanır. parameter -> method -> accessor -> property -> constructor -> class

Aynı üyedeki birden fazla dekoratörün sıralaması

class Kullanıcı{
@a @b @c
function kaydet(){
console.log('kaydet fonksiyonu çalıştı');
}
}
let k = new Kullanıcı();
k.kaydet();

Bunların çalıştırma sırası alana en yakın olandan en uzak olana doğrudur. Matematikdeki birleşim özelliği bunun en güzel açıklamasıdır a(b(c(kaydet))) veya (a o b o c)(kaydet) fakat değerlendirme sırası ise en dışarıdan en içeriye doğrudur.

Nasıl Kendi Decorator’ımızı Oluştururuz?

Her dekoratör ait olduğu üyeyi ve ona ait diğer özellikleri içine alır böylece o üyeyi manipüle edebiliriz.

Class Decorator

Tek parametresi vardır o da sınıfın constructor fonksiyonudur. Burada sınıfın içine yeni method veya property ekleyebilir yada olanları ezebiliriz.

// toString fonksiyonunu ezen ve JSON stringfy yapan dekoratör
function toString<T extends Consturctor>(BaseClass: T) {
return class extends BaseClass {
toString() {
return JSON.stringify(this);
}
};
}

@toString
class C {
public foo = "foo";
public num = 24;
}

Dekoratörün geri dönderdiği değer sınıfla değiştirilecektir.

Method Decorator

Üç parametresi vardır bunlar sırasıyla: hedef aldığı fonksiyon, fonksiyonun ismi ve method için Property Descriptor tipindeki bit tanımlayıcı.

Methodu yeniden atamak, method çağrıldığında log’lama ve benzeri işlemler yapmak için kullanılabilir.

Descriptor içinde şu key’ler bulunur:

  • value
  • writable
  • enumerable
  • configurable
function logger(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;

descriptor.value = function (...args) {
console.log('params: ', ...args);
const result = original.call(this, ...args);
console.log('result: ', result);
return result;
}
}

class C {
@logger
add(x: number, y:number ) {
return x + y;
}
}

const c = new C();
c.add(1, 2);
// -> params: 1, 2
// -> result: 3

Kendisini veya yeni bir PropertyDescriptor geri dönmelidir.

Property Decorator

İki paraketresi vardır ve bunlar sırasıyla bulunduğu sınıfın kendisi ve hedef aldığı property’nin ismidir.

Genellikle doğrulama yapmak için kullanılır.

function resit(target: any, key: string) {
const setter = function (newVal: number) {
if (newVal < 18) {
throw new Error("Yaş 18'den büyük olmalıdır.");
}
this[key] = newVal;
};

// Property'i tanımlama
Object.defineProperty(target, key, {
set: setter,
enumerable: true,
configurable: true,
});
};

class C{
@resit
yas: number
};

Geri dönüş değeri yoktur.

Accessor Decorator

Method dekoratörleriyle aynı parametreleri alır. TypeScript get ve set erişimcileri için tek bir Property Descriptor üretir bu yüzden işlem yapılırken bu göz önünde bulundurulmalıdır. Bu nedenle descriptor yapısı method dekoratörlerinden farklıdır.

Descriptor içinde şu key’ler bulunur:

  • get
  • set
  • configurable
  • enumerable

Genellikle erişim yapılabilecek durumları düzenlemede, erişim için tetiklendiğinde log’lama yapmak gibi işlemlerde kullanılır.

Kendisini veya yeni bir PropertyDescriptor geri dönmelidir.

Parameter Decorator

Üç tane parametresi bulunur ve bunlar sırasıyla: bulunduğu sınıf, bulunduğu method’un ismi ve o method’un kaçıncı parametresi olduğunun index’idir.

genellikle parametrelerin doğrulanması için kullanılır.

function resit(target: any, key: string, index: number) {
const originalMethod = target[key];

// Hedef methodun yerine yeni bir method tanımlıyoruz
target[key] = function (...args: any[]) {
const paramValue = args[index];
if (paramValue < 18) {
throw new Error("Parametre değeri 18'den büyük olmalıdır.");
}
return originalMethod.apply(this, args);
};
}

class C{
createUser(isim: string, @resit yas: number) {
//...
}
}

Geriye bir değer döndürmez.

Decorator Factory

Şu ana kadar nasıl dekoratör oluşturabileceğimizi sistemin bize hangi bilgileri sağladığını ve bunlarla ne yapabileceğimizi gördük peki kendimiz de dışarıdan kendimiz parametre almak istediğimizde ya da dekoratör çağrılmadan önce de bir şeyler yapmak istedimizde ne yapmalıyız sorusuya baş başayız.Bu sorunun cevabı oldukça basit. “Dekoratörü de bir fonksiyonla sarmalıyız.”

Tek yapmamız gereken almak istediğimiz parametreleri dışarıdan almak ve geriye bir dekoratör döndermek.

function büyükMü(sayi){
return function(target, propertyIsmi){
if(target[propertyIsmi] > sayi)
throw new Error(`${propertyIsmi} ${sayi}'dan büyük olmalıdır.${propertyIsmi}:${target[propertyIsmi]}`);
}
}
class Kullanici{
@büyükMü(18)
yas: number;
}
Not: Decorator factory ile oluşturulan dekoratörler mutlaka () ile kullanılmalıdır aksi taktirde sadece factory gövdesi çalışmaktadır.

Örnek Senaryo:

Şu ana kadar öğrendiklerimizle basit bir routing uygulaması yazabiliriz. Bu uygulama gelen Class ve methodlara verilen dekoratörlere göre gelen istekleri yönlendirecektir. Class’lara path(p: string) isteğin geleceği path’in ana kısmını method’lara ise get , post , delete gibi dekoratörlerle dinleyeceği methodu ve varsa ana path üzerine eklenecek şeyi bilirecektir. Ana uygulama tüm class’ların metalarını toplayıp gelen istekreki path’i ve method’u parçalayarak ilgili method’u çağıracaktır.

Örnek sınıf:

@path('/users')
class UserController{
@get('/:id')
getUser(request, response){}

@get()
getUsers(req, res){}

@post()
createUser(req, res){}
}

Kaynakça: