Anasayfa » 10. Proxy Pattern

10. Proxy Pattern

by Levent KARAGÖL
14 dakikalık okuma süresi

Bu makalemizde, orijinal nesnenin yerine geçerek bu nesneyle yapılan her türlü etkileşimi kontrol altına alan Proxy tasarım kalıbını inceleyeceğiz. Bu serinin genelinde olduğu gibi öncelikle Proxy tasarımın kalıbının ne olduğunu, hangi durumlarda hangi sorunlara çözüm sunduğunu konuşacak, sonrasında gelen bir talebe istinaden herhangi bir tasarım kalıbı kullanmadan bir çözüm üretecek, ardından bu çözümün problemlerini ve hangi yazılım tasarım prensiplerine aykırı olduğunu tartışacak, son olarak da Proxy tasarım kalıbı ile aynı talebe uygun bir çözüm üretmeye çalışacağız.

Not: Makale genelindeki örneklerin kaynak kodlarına Github üzerinden ulaşabilirsiniz.


Proxy Tasarım Kalıbı Nedir?

Klasik tasarım kalıplarından Structural Design Patterns kategorisinde yer alan ikinci tasarım kalıbımız olan Proxy tasarım kalıbını özellikle Adapter tasarım kalıbının hemen arkasından işlemek istedim. Ne yazık ki bu iki tasarım kalıbı, aslında birbiriyle pek de alakalı olmamasına rağmen, her ikisi de hedef sistemle kaynak sistem arasına girdiği için sıklıkla karıştırılabiliyor. Kodlamalarda Proxy olmasına rağmen Adapter, Adapter olmasına rağmen Proxy olarak isimlendirilmiş pek çok bileşene rastlayabiliyoruz.

Bu karışıklığın önüne geçmek için Proxy tasarım kalıbının en önemli karakteristik özelliği ile başlayalım. Proxy tasarım kalıbı, yerine geçtiği orijinal sınıfın imzasında herhangi bir değişiklik yapmaz. Metot isimleri, parametre isimleri ve parametre sayıları gibi unsurlar orijinal hedefle bire bir aynıdır. Hatta çağrım yapan taraf, orijinal hedefi mi, yoksa aradaki Proxy nesnesini mi çağırdığının farkında olamamalıdır.

Bu tasarım kalıbının kullanım mantığında istemci taraf, hedef nesne yerine bire bir aynı imzaya sahip proxy nesnesini çağırır. Proxy nesnesi de hedef nesneyi çağırarak gelen cevabı istemciye geri döner.

Peki imzada hiç bir değişiklik yapmayan bir katmanı araya neden ekliyoruz? Çünkü bu yöntemle, hedef nesneye erişimi kontrol altına almış ve arada istediğimiz işlemleri gerçekleştirme imkanına kavuşmuş oluyoruz. Bu imkanların birkaçını sayacak olursak;

  • Yapılan işlemlerin loglanması
  • Her bir işlem öncesinde yetki kontrolünün yapılması (Authentication/Authorization)
  • Taleplerin farklı hedeflere yönlendirilmesi (Load Balancing)
  • Dönülen cevaplar için Cache mekanizmasının kurulması (ORM’lerde kullanılan L2 Cache gibi)


Talebimizi Alalım

Hava durumu tahmini yayınlayan bir servise entegre olmamız isteniyor. Bu servis saatlik, günlük ve haftalık tahminler sunuyor. Servis, alınan tahmin sayısına göre ücret talep ettiği için, servise yapılan talep sayısını azaltmak amaçlanıyor. Bu sebeple saatlik tahminler doğrudan servisten sorgulanırken, günlük tahminlerin saatte bir, haftalık tahminlerin de 6 saatte bir alınması, bu süre zarfında takip eden taleplerin de ön bellekten karşılanması talep ediliyor.


İlk Çözüm Önerimizi Sunalım

Alınan günlük ve haftalık tahminleri temsili olarak bir değişkende tutarak takip eden talepler için servise gitmeden dönüş yapılmasını sağlayacağız (Bunun için gerekli Singleton yapısını, konuyu dağıtmamak için şimdilik görmezden gelelim). TypeScript serisindeki ilgili makalede önerilen “tsconfig.json” dosyasını oluşturarak işe başlayalım.

{
  "compilerOptions": {
    "target": "es2020",
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "resolveJsonModule": true,
    "removeComments": true,
    "noUnusedParameters": true,
    "declaration": true,
    "lib": [
      "es2020",
      "esnext.asynciterable",
      "dom"
    ]
  },
  "include": [
    "**/*.ts"
  ],
  "exclude": [
    "node_modules",
    "**/*.spec.ts"
  ]
}


Aşağıdaki sınıf, hava tahminini sunan temsili servis olarak projemizde iş görecek.

export class WeatherForecastService {
    hourlyForecast() {

        return `Saatlik hava tahmini, ${new Date()}`;
    }

    dailyForecast() {

        return `Günlük hava tahmini, ${new Date()}`;
    }

    weeklyForecast() {

        return `Haftalık hava tahmini, ${new Date()}`;
    }
}


Bu servise ulaşıp verileri alan, 1 ve 6 saatlik önbelleğe kaydeden ve sonucu ekrana yazan “WeatherReportService” sınıfımızı da aşağıdaki şekilde projemize ekleyelim.

import {WeatherForecastService} from "./weather-forecast-service";

export class WeatherReportService {

    private lastDailyForecast: string = '';
    private lastDailyForecastTime: number = 0;
    private dailyForecastCacheDuration: number = 1 * 60 * 60 * 1000; // 1 saat

    private lastWeeklyForecast: string = '';
    private lastWeeklyForecastTime: number = 0;
    private weeklyForecastCacheDuration: number = 6 * 60 * 60 * 1000; // 6 saat

    report() {

        const weatherForecastService = new WeatherForecastService();

        // Her durumda güncel saatlik tahmin alınır
        const hourlyForecast = weatherForecastService.hourlyForecast();

        console.log(hourlyForecast);

        const currentTime = Date.now();

        // Eğer son günlük tahmin alınalı 1 saatten fazla zaman geçmişse yeniden tahmin alınır
        if (currentTime - this.lastDailyForecastTime > this.dailyForecastCacheDuration) {
            this.lastDailyForecastTime = currentTime;
            this.lastDailyForecast = weatherForecastService.dailyForecast();
        }

        console.log(this.lastDailyForecast);

        // Eğer son haftalık tahmin alınalı 6 saatten fazla zaman geçmişse yeniden tahmin alınır
        if (currentTime - this.lastWeeklyForecastTime > this.weeklyForecastCacheDuration) {
            this.lastWeeklyForecastTime = currentTime;
            this.lastWeeklyForecast = weatherForecastService.weeklyForecast();
        }

        console.log(this.lastWeeklyForecast);
    }
}


Sınıfımızın içeriği biraz kalabalık olsa da yaptığı iş basit. “WeatherForecastService” altındaki “hourlyForecast“, “dailyForecast” ve “weeklyForecast” metotlarını çağırarak verileri alıyor. Ardından bu verileri ve verileri alış zamanını sınıf seviyesindeki değişkenlerde saklıyor. Takip eden çağrılarda son veri alış zamanından beri geçen süreyi hesap ediyor ve bu süre günlük tahmin için 1 saati, haftalık tahmin için 6 saati geçmediği taktirde tekrar sorgulama yapmaksızın elindeki hazır veriyi ekrana yazıyor.

Son olarak uygulamamızı ayağa kaldıracak en dış kabuk olan “app.ts” dosyamızı aşağıdaki içerik ile oluşturalım.

import {WeatherReportService} from "./weather-report-service";

const weatherReportService = new WeatherReportService();

weatherReportService.report();

console.log();

setTimeout(() => {
    weatherReportService.report();
}, 5000);


Uygulamamızı derleyip çalıştırdığımızda aşağıdaki gibi bir ekran görüntüsü ile karşılaşırız. Görüldüğü üzere 5 saniye sonra yapılan ikinci çağrıda günlük ve haftalık veriler önceki çağrım ile aynı şekilde geldi. Bu da önbelleğin aktif olduğunu gösteriyor.


Genel yapıyı daha net görebilmek adına öneriye ilişkin Class Diagram aşağıdadır.


Peki Bu Çözümün Nesi Yanlış?

Öncelikle ilk bakışta görüleceği üzere “WeatherReportService” sınıfındaki “report” fonksiyonu içerisinde ciddi bir yoğunluk var. Aynı anda hem servis çağrımı, hem de Cache yönetimi yapıyor. Bu durum öncelikle Single Responsibility, ardından da Encapsulation prensiplerini çiğnediğimizin göstergesi olarak kabul edilebilir. Bu tarz yapılarda Open/Closed prensibine bağlı kalmak çok zordur çünkü bütün kod tek bir metot içerisinde toplanmış durumda.


Nasıl Bir Çözüm Sunulabilirdi?

Makalenin konusu olan Proxy Pattern bu talep için güzel bir çözüm önerisi olabilir. Zorunlu olmamakla birlikte, Liskov Substitution prensibine uymak ve talep edildiği durumda Proxy sınıfını aradan çıkarabilmek için “IWeatherForecastService” isimli Interface’i aşağıdaki şekilde projemize dahil edelim.

export interface IWeatherForecastService {
    hourlyForecast(): string;

    dailyForecast(): string;

    weeklyForecast(): string;
}


Ardından “WeatherForecastService” isimli servise bu Interface’i uygulayalım.

import {IWeatherForecastService} from "./iweather-forecast-service";

export class WeatherForecastService implements IWeatherForecastService {
    hourlyForecast() {

        return `Saatlik hava tahmini, ${new Date()}`;
    }

    dailyForecast() {

        return `Günlük hava tahmini, ${new Date()}`;
    }

    weeklyForecast() {

        return `Haftalık hava tahmini, ${new Date()}`;
    }
}


Şimdi de “WeatherReportService” ile “WeatherForecastService” arasına girerek Proxy görevini üstelenecek “WeatherForecastProxy” sınıfını aşağıdaki şekilde projemize ekleyelim.

import {IWeatherForecastService} from "./iweather-forecast-service";
import {WeatherForecastService} from "./weather-forecast-service";

export class WeatherForecastProxy implements IWeatherForecastService {

    private lastDailyForecast: string = '';
    private lastDailyForecastTime: number = 0;
    private dailyForecastCacheDuration: number = 1 * 60 * 60 * 1000; // 1 saat

    private lastWeeklyForecast: string = '';
    private lastWeeklyForecastTime: number = 0;
    private weeklyForecastCacheDuration: number = 6 * 60 * 60 * 1000; // 6 saat

    constructor(private weatherForecastService: WeatherForecastService) {
    }

    hourlyForecast() {
        return this.weatherForecastService.hourlyForecast();
    }

    dailyForecast() {

        const currentTime = Date.now();

        if (currentTime - this.lastDailyForecastTime > this.dailyForecastCacheDuration) {
            this.lastDailyForecastTime = currentTime;
            this.lastDailyForecast = this.weatherForecastService.dailyForecast();
        }

        return this.lastDailyForecast;
    }

    weeklyForecast() {

        const currentTime = Date.now();

        if (currentTime - this.lastWeeklyForecastTime > this.weeklyForecastCacheDuration) {
            this.lastWeeklyForecastTime = currentTime;
            this.lastWeeklyForecast = this.weatherForecastService.weeklyForecast();
        }

        return this.lastWeeklyForecast;
    }
}


Görüldüğü gibi Proxy sınıfı hedef sınıf ile bire bir aynı imzaya sahip (Ki zaten aynı Interface’i uyguluyorlar). Kendine gelen talepleri hedef sınıfa iletiyor. Ancak arada önbellek kontrolü yapıyor ve sadece ihtiyaç duyduğu durumlarda hedef servise çağrımda bulunuyor.

Böylece Cache mekanizması Proxy sınıfı içerisinde kaldı, Encapsulation prensibine uymuş olduk. İleride Cache mekanizmasında yapılacak ek geliştirme ve değişikliklerin, kodun geri kalanına yayılmasını engelledik.

Cache mekanizmasını Proxy sınıfı üstlendiğine göre “WeatherReportService” sınıfımızı aşağıdaki şekilde sadeleştirebiliriz.

import {IWeatherForecastService} from "./iweather-forecast-service";

export class WeatherReportService {

    constructor(private weatherForecastService: IWeatherForecastService) {
    }

    report() {

        const hourlyForecast = this.weatherForecastService.hourlyForecast();
        const dailyForecast = this.weatherForecastService.dailyForecast();
        const weeklyForecast = this.weatherForecastService.weeklyForecast();

        console.log(hourlyForecast);
        console.log(dailyForecast);
        console.log(weeklyForecast);
    }
}


Görüldüğü gibi sınıfımıza sadece raporlama işi düşmüş oldu. Gayet temiz ve Single Responsibility prensibine uygun bir yapıya kavuştuk (Üç çağrım için üç ayrı metot yazılabilirdi ama örneği çok da uzatmayalım).

Son olarak “app.ts” dosyamızı aşağıdaki şekilde güncelleyelim.

import {WeatherReportService} from "./weather-report-service";
import {WeatherForecastProxy} from "./weather-forecast-proxy";
import {WeatherForecastService} from "./weather-forecast-service";

const weatherReportService = new WeatherReportService(new WeatherForecastProxy(new WeatherForecastService()));

weatherReportService.report();

console.log();

setTimeout(() => {
    weatherReportService.report();
}, 5000);


Önerinin başlangıcında bir Interface tanımladığımız için arada Proxy kullanıp kullanmamayı tercih etme seçeneğimiz de var. Eğer ki “WeatherReportService” sınıfından nesne türetirken kurucu metoda aşağıdaki gibi doğrudan hedef sınıfın örneğini geçersek, Proxy aradan çıkarak da uygulama çalışmaya devam edecektir. Burada da Dependency Injection kullanmanın avantajlarını görüyoruz.

const weatherReportService = new WeatherReportService(new WeatherForecastService());


Uygulamamızı derleyip çalıştırdığımızda aşağıdaki gibi bir ekran görüntüsü ile karşılaşırız.


Genel yapıyı daha net görebilmek adına öneriye ilişkin Class Diagram aşağıdadır.



Böylece Proxy tasarım kalıbı konusunun sonuna geldik. Bir sonraki makalemizde Decorator tasarım kalıbını işleyeceğiz.

Herkese iyi çalışmalar…

İLGİNİZİ ÇEKEBİLİR

Bir Yorum Yaz