JavaScript’in Takipçi Nesnesi ‘Proxy’

Bir nesneye her atama(set) veya okuma(get) yapıldığında belirli işlemleri nasıl yapacağımızı hiç düşündünüz mü? OOP kullanılabilen bir…

JavaScript’in Takipçi Nesnesi ‘Proxy’

Bir nesneye her atama(set) veya okuma(get) yapıldığında belirli işlemleri nasıl yapacağımızı hiç düşündünüz mü? OOP kullanılabilen bir dille daha önce uğraştıysanız bu soruya haklı olarak “Tabii ki getter ve setter kullanarak.” cevabını vermiş olabilirsinizdir, JavaScript de bu özelliğe sahiptir ancak asıl nesnemize getter/setter eklemek istemediğimizde veya daha fazla işlemi takip etmek istediğimizde ne kullanacağız peki? İşte burada da Proxy nesneleri yardımımıza koşuyor.

Proxy Nedir?

ECMAScript 6 ile hayatımıza giren Proxy nesnesi, bir objeyi baz alarak get, set, yeni property tanımlama gibi temel obje operasyonlarını takip edebileceğimiz yeni bir obje oluşturur. Bu sayede obje üzerindeki işlemlere göre aksiyona geçebilir, veri doğrulayabilir, serileştirebilir, log’layabilir yahut benzeri işlemleri yapabiliriz.

Proxy üzerinden hedef nesnenin özellikleri değiştirildikçe hem proxy’deki veriler hem de hedef nesnenin içindekilerin değişeceği kullanılırken göz önünde bulundurulmalıdır.

Nasıl tanımlanır?

Proxy constructure’ının iki ana parametresi vardır. Bunlardan birincisi kopyasını çıkaracağımız hedef nesne ikincisi ise işlemler tetiklendiğinde çalışacak mantıksal yapımızın olduğu bir handler’dır. Her handler’ın kendine ait parametreleri vardır.

// target yerine orijinal nesnemizi,
// handler yerine takip fonksiyonlarımızın bulunduğu nesneyi yazarız.
// new Proxy(target, handler);
let user = {
name: 'Alihan',
surname: 'SARAC',
age: 21,
};
const handler = {
// ilgili method'lar
};
let userProxy = new Proxy(user, handler);

Hangi obje işlemlerini takip edebiliriz?

Objelerin temel işlemlerinin birçoğunu takip edebiliriz. Bunların bir kısmı proxy üzerinden tetiklenebilirken bir kısmını da Object sınıfının method’ları ile tetikleyebiliriz.

  • get : proxy’den okuma yapıldığında tetiklenir
  • set :proxy’e atama yapılırken tetiklenir
  • has :in operatörü kullanıldığında tetiklenir.
  • apply : fonksiyondan üretilen proxy’lerde fonksiyon çağrıldığında tetiklenir.
  • ownKeys : Object.getOwnPropertyNames ve Object.getOwnPropertySymbols ile proxy kullanıldığında tetiklenir
  • construct : new operatörü kullanıldığında tetiklenir
  • isExtensible : Object.isExtensible ile proxy kullanıldığında tetiklenir.
  • deleteProperty : delete operatörü proxy üzerinde kullanılınca tetiklenir.
  • defineProperty : Object.defineProperty ile proxy kullanıldığında tetiklenir.
  • setPrototypeOf : Object.setPrototypeOf ile proxy kullanıldığında tetiklenir.
  • getOwnPropertyDescriptor : Object.getOwnPropertyDescriptor ile proxy çağrıldığında tetiklenir.
  • preventExtensions : Object.preventExtensions ile proxy kullanıldığında tetiklenir.

Nasıl kullanılır?

Proxy handler method’larının içine tanımlandığı nesnenin güncel halini parametre olarak gönderir. Key(property)’lere özel handler tanımlanamadığı için eğer key’lerle ilgili bir işlem yapılıyorsa key’in ismini de parametre geçer ve ardından ilgili işleme özel parametreleri de gönderir.

En temel 3 şey olan atama, getirme ve silme işlemlerine gelin birlikte bakalım.

Get:

// target: hedef nesnemizin kendisi
// property: okuma işlemi yapılan key
// reciever: proxy'nin kendisi yada ondan kalıtım alan bir nesne.
//
// temel işlemlerde "reciever" kullanmamıza gerek yoktur
// sadece ilk iki parametre alınabilir
get( target, property, reciever){
// mantıksal işlemler
return "geri dönülecek değer";
}

Get fonksiyonunun içinden target[property] diyerek şu an okuma yapılan değeri alabilir, ilgili işlemleri yaptıktan sonra istediğimiz değeri return diyerek kullanıcıya dönebiliriz.

Set:

// target: hedef nesnemizin kendisi
// property: okuma işlemi yapılan key
// value: property'e atanmak istenen değerdir
// reciever: proxy'nin kendisi yada ondan kalıtım alan bir nesne.
//
// temel işlemlerde "reciever" kullanmamıza gerek yoktur
// sadece ilk iki parametre alınabilir
get( target, property, value, reciever){
// mantıksal işlemler
return true;
}

Set fonksiyonu mutlaka geriye atama yapıldığına dair bool bir değer dönmelidir. Fonksiyonunun içinden target[property] diyerek atama yapmalısınız.

Delete:

// target: hedef nesnemizin kendisi
// property: okuma işlemi yapılan key
deleteProperty( target, property){
// mantıksal işlemler
delete target[property];
return true;
}

deleteProperty , delete operatörü kullanıldığında tetiklenir ve silinme yapıldığının anlaşılması için bool bir değer döndürmelidir.

Geri alınabilir Proxy’ler

Eğer Proxy oluştururken Proxy.revocable fonksiyonunu kullanırsak proxy özelliği kapatılabilir bir proxy oluşturmuş oluruz. Oluşturulma şekli ve kullanımı normal proxy ile temel olarak aynıdır, tek fark mu fonksiyonun geriye proxy değil onu verecek ve iptal edecek olan nesneyi vermesidir.

let revocable =  Proxy.revocable(target, handler);
let userProxy = revocable.proxy;

revocable oluşturulduktan sonra revocable.revoke method’u ile proxy nesnesinin proxy’si kapatılır.

Unutulmamalıdır ki proxy kapatıldıktan sonra yukarıdaki “Hangi obje işlemlerini takip edebiliriz?” alanında bahsedilen tüm operasyonlar TypeError fırlatacaktır.

Örnek Kullanım Senaryoları:

Value Validation:

Proxy’lerin en popüler kullanım alanlarından biri de veri doğrulamasıdır. Atama işlemlerinde minimum-maksimum değer, email ve yazım tipi doğrulaması gibi birçok işlemde kullanılabilir.

Senaryomuzda bir kullanıcı olduğunu varsayalım. Yaş değerinin 0'dan büyük olması gerektiğini, silme işleminde sadece kullanıcının silme tarihi geçtiyse silinebileceğini ve sadece test kullanıcılarının şifresinin okunabileceğini varsayalım.

Orijinal nesnemizi tanımlayalım.

let user = {
name: 'alihan',
age: 21,
password: '123456as.',
isTestUser: true,
expireAt: new Date(100000000000),
}

Proxy’nin handler’ını tanımlayalım.

const userProxyHandler = {
get(target, key){
let value = target[key];
if(key === 'password' && !isTestUser){
value = '***';
// gelen okuma password için yapılıyorsa ve kullancı test kullanıcısı değilse şifre '***' şeklinde gösterilir.
}
return value;
}
  set(target, key, value){
if(key === 'age' && value < 0){
throw new Error('yaş değeri 0'a eşit veya büyük olmalıdır');
}
target[key] = value;
return true;
}
  deleteProperty(target, key){
if(Date.now() > target[expireAt]){
throw new Error('Kullanıcı bu tarihte silinemez');
}
delete target[key];
}
}

Değişen key’leri ve eski değerlerini nesne içinde tutma:

Veritabanlarıyla uğraşıyorsanız, hele de benim gibi bir ORM yazmayı deniyorsanız her bir işlemin maliyeti sizin için oldukça önemlidir. İçinde sadece bir alanı değişmiş büyük bir nesneyi sisteme tekrardan kaydetmek gibi bir masraf kesinlikle istemeyeceğimiz bir şeydir bu durumu çözmek için hangi alanlarda değişiklik yapıldığını tutabilir ve sadece o alanı kaydedebiliriz. Proxy‘ler tam da bu iş için yaratılmış. Hadi nasıl yapacağımıza bakalım.

İlk önce handler’a bir set olayı yazmalıyız. Burada objemizin içinde _degisenDegerler adında, obje tipinde bir key tutacağız ve değişiklikleri buradan takip edebileceğiz. Her set işlemi çalıştığında ilgili key ilk önce target içindeki _degisenDegerler ‘e eklenmeli ve şu anki verisi buraya yazılmalı ardından ise target ‘ın içindeki key alanı gelen value ‘yu tutacak şekilde güncellenmeli böylece eski veri _degisenDegerler içine yazılmış ve yerine yenisi atanmış olur. Veritabanına kayıt yapıldıktan sonra da _degisenDegerler içindeki tüm key’ler kaldırılabilir.

let model = {
isim: 'Alihan',
_degisenDegerler: {},
};
let modelProxy = new Proxy(model, {
set(target, key, deger){
target._degisenDegerler[key] = target[key];
target[key] = deger;
return true;
}
});
modelProxy.isim = 'ayşe';
modelProxy.yas=21;
console.log({ modelProxy });
//{
// "modelProxy": {
// "isim": "ayşe",
// "yas": 21,
// "_degisenDegerler": {
// isim: "Alihan"
// yas: undefined
// },
// }
//}

Kaynakça: