Proxy Pattern Nedir? Nasıl Uygulanır?

Esat KOÇ
4 min readMay 18, 2023

--

Yazılım mühendisliğinde Tasarım Desenleri (“Design Patterns”), sık karşılaşılan benzer tasarım sorunları için tekrarlanabilir örnek çözüm yapılarıdır. Tasarım desenleri kategorik olarak yaratımsal (“creational”), yapısal(“structural”) ve davranışsal(“behavioral”) olmak üzere üçe ayrılır.

Biz bu yazımızda, özellikle kütüphane ve çatı (framework) uygulamalar geliştirilirken kullanılan çok güçlü bir tasarım deseni olan tasarım desenini inceleyeceğiz.

Proxy tasarım deseni, gerçek işi yapacak olan nesneye erişimi kontrol etmenize, erişim öncesi ve/veya sonrası istenilen işlemleri gerçekleştirmek için kullanılan bir desendir.

extracted from https://refactoring.guru/design-patterns/proxy

Diyagrama bakarsak, Service adlı sınıfı esas işi yapan, erişmeye çalıştığımız sınıf olarak düşünebiliriz. ServiceInterface adlı arayüzü implemente ediyor. Esas sınıfımız olan Service sınıfını bağımlılık olarak alan Proxy sınıfı ise aynı arayüzü implemente ediyor. Dolayısıyla, servisimizin tüketicileri aslında hiç bir değişiklik yapmak zorunda kalmadan Proxy servisini çağırabiliyorlar. Client, Service sınıfındaki bir operasyonu çağırdığında, bu istek önce Proxy servisinden geçiyor ve Proxy servisi kendi kontrollerinden sonra esas fonksiyon çağrısını Service sınıfına delege ediyor.

Esas işin yapılmasından önce ve sonra devreye girebiliyor olmamız aşağıdaki durumlarda işe yarayabilir:

  • Esas objenin yaratılması çok maliyetli ise, objenin yaratılmasını optimize etmek (gereksiz yaratımı azaltmak) için kullanılabilir (lazy initialization, virtual proxy).
  • Esas objeye erişimin kısıtlanması gerektiğinde kullanılabilir (access control, protection proxy).
  • Caching mekanizması için kullanılabilir (caching proxy).
  • Erişim öncesi ve/veya istek ve cevabın loglanması için kullanılabilir (logging proxy).

Örnek Uygulama

Şimdi gelin Proxy tasarım deseninin caching amaçlı nasıl uygulandığını öğrenebilmek için daha önce konfigürasyonlarını yaptığımız bir Spring Boot projesi içerisinde koda dökelim.

Uygulamamızda LibraryService adında bir arayüzümüz olsun.

package com.kocesat.project.proxy;

public interface LibraryService {
String getEbookByName(String bookName);
}

İsme göre kitabın online (ebook) versiyonunu dönen bu sınıfımızın bu operasyonunun maliyetli ve zaman alan bir iş olduğunu varsayalım. Esas işi yapacak olan implementasyon sınıfımız aşağıdaki şekilde işin uzun sürdüğünü simüle edecek.

import org.springframework.stereotype.Service;

@Service
@Qualifier("libraryServiceImpl")
public class LibraryServiceImpl implements LibraryService {

@Override
public String getEbookByName(String bookName) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "Lorem ipsum dolor sit amet, consectetur adipiscing elit...";
}
}

Aşağıda ise aynı arayüzü implemente eden proxy servisimizi yazıyoruz.

package com.kocesat.project.proxy;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.Optional;

@Service
@Slf4j
@Qualifier("libraryServiceProxy")
public class LibraryServiceProxy implements LibraryService {
private final LibraryService libraryService;
private final CacheManager cacheManager;

public LibraryServiceProxy(
@Qualifier("libraryServiceImpl") LibraryService libraryService,
final CacheManager cacheManager
) {
this.libraryService = libraryService;
this.cacheManager = cacheManager;
}

@Override
public String getEbookByName(String bookName) {
var time = LocalDateTime.now();
log.info(String.format("[%s] Getting e-book '%s'", time, bookName));

Optional<String> bookOptional = cacheManager.get(bookName);
if (bookOptional.isPresent()) {
log.info(String.format("[%s] Cache hit for book %s", time, bookName));
return bookOptional.get();
}

log.info(String.format("[%s] Cache miss for book %s", time, bookName));
final String ebook = libraryService.getEbookByName(bookName);
cacheManager.put(bookName, ebook);
return ebook;
}
}

Proxy sınıfımızın amacı, LibraryServiceImpl’a gitmeden önce eğer aynı kitap daha önce indirildi ise (yani cache’te yer alıyorsa) in-memory cache içerisinden alıp döndürmek, değilse LibraryServiceImpl’a giderek kitabı indirmek ve cache içerisine koymak. Cache işlemlerini yürütmek için aşağıdaki operasyonları yürüten bir servis yeterli olacaktır. Paylaşacağım github reposunda bu servisin bir implementasyonu yer alıyor ancak siz dilediğiniz cache çözümünü (örn. Redis, Hazelcast, Ehcache, Google Guava cache vb.) kullanabilirsiniz.

package com.kocesat.project.proxy;

import java.util.Optional;

public interface CacheManager {
public Optional<String> get(String key);
public void put(String key, String value);
}

Servisimizi postman üzerinden tetikleyebilmek için bir controller sınıfı yazalım.

package com.kocesat.project.proxy;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("library/ebook")
public class LibraryController {
private final LibraryService libraryService;

public LibraryController(
@Qualifier("libraryServiceProxy") LibraryService libraryService
) {
this.libraryService = libraryService;
}

@GetMapping
public ResponseEntity<String> getEbookByName(@RequestParam("name") String name) {
final String ebook = libraryService.getEbookByName(name);
return ResponseEntity.ok(ebook);
}
}

Dikkat ederseniz, aynı arayüzü (LibraryService.java) implemente eden esas sınıf (LibraryServiceImpl) ve proxy sınıfımız (LibraryServiceProxy) olmuş oldu. Controller sınıfında proxy implementasyonunu alacağımızı @Qualifier anotasyonu ile özellikle belirtiyoruz.

Şimdi ise uygulamamızı ayağa kaldıralım ve postman üzerinden istek atıp, cevap sürelerini gözleyelim.

İlk çağrının 3.12 saniye sürdüğünü görüyoruz. Aşağıdaki loglardan da cache’te yer almadığını teyit ediyoruz.

Şimdi ise aynı kitap ismi ile tekrar çağrı atıyoruz. Servisin dönüş hızını bu sefer 8 milisaniye olarak gözlemliyoruz.

Uygulama loglarından ise cache-hit olduğunu teyit ediyoruz.

Uygulamanın tüm kodları aşağıdaki github reposunda mevcuttur.

Bir sonraki yazımızda görüşmek üzere…

Kaynaklar

--

--