Anasayfa » 14. Facade Pattern

14. Facade Pattern

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

Bu makalemizde, karmaşık sistemlerin kullanıcı dostu ve basit bir arabirime sahip olmasını sağlayan Facade tasarım kalıbını inceleyeceğiz. Bu serinin genelinde olduğu gibi öncelikle Facade 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 Facade 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.


Facade Tasarım Kalıbı Nedir?

Facade pattern, sistemde birçok bağımlılık ve karışık işlem bulunduğunda, bu karmaşıklığı soyutlamak için kullanılır. Temel anlamda, alt sistemlerdeki bir dizi işlemi birleştirir ve bu işlemleri tek bir basit arayüz olarak dışarı sunar. Böylece, alt sistemin gerçekte ne kadar karmaşık olduğu konusunda istemcinin endişe etmesine gerek kalmaz. Özet olarak Facade Pattern, karmaşıklığı gizler ve son kullanıcıya basit bir arayüz sağlar.

Microservice mimarilerde kullandığımız Aggregator Service veya prematüre olarak tasarlanmamış (İş mantığı Domain’den Application katmanına kaymamış) DDD (Domain Driven Design) mimarisindeki Application Service, Facade Pattern için birer güncel kullanım örneğidir.

Aslında en basit tasarım kalıplarından biri olan Facade Pattern için çok sayıda karmaşık cümle kurdum. En iyisi örnek üzerinden devam edelim.


Talebimizi Alalım

Basit bir e-ticaret sitesinin back-end tarafını yazıyoruz. Yazdığımız sistem üzerinden kayıtlı kullanıcılar seçtikleri ürünün bedelini kredi kartı ile ödeyerek sipariş verecekler. Ürünün stok bilgisini de tutmamız gerekiyor ki, stokta olmayan ürünün satışı mümkün olmasın. Sistem stoktan seçilen ürünün ağırlığına göre kargolama ücretini hesaplamalı. Ayrıca kullanıcı adresine kargolama, işlem geçmişi tutma ve işlem sonucunda kullanıcıya bildirim gönderme işi ile de ilgilenmemiz gerekiyor. Son olarak kullanıcılar, verdikleri siparişi iptal ederek süreci geri alabilecekler.


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

Talebe bakılırsa 5 farklı süreçten bahsediliyor. Bunlar;

  • Kullanıcı işlemleri (Kullanıcı adres bilgileri, işlem geçmişi)
  • Stok işlemleri (Ürün stok adedi, fiyat ve ağırlık bilgisi)
  • Ödeme işlemleri (Ödeme ve ödeme iptali)
  • Kargo işlemleri (Kargolama ve kargolama iptali)
  • Bildirim işlemleri


Bunların her biri için ayrı servis oluşturursak, Single Responsibility ve Separation of Concerns prensiplerine de bağlı kalmış oluruz.

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,
    "declaration": true,
    "lib": [
      "es2020",
      "esnext.asynciterable",
      "dom"
    ]
  },
  "include": [
    "**/*.ts"
  ],
  "exclude": [
    "node_modules",
    "**/*.spec.ts"
  ]
}


Kullanıcı işlemleri ile başlayalım. Kullanıcı adresine kargolama yapacağımıza ve bildirim göndereceğimize göre, kullanıcı bilgilerini bize dönen bir metot olmalı. Ayrıca yapılan işlemi sipariş geçmişine ekleyen bir metot ve iptal sırasında kullanılmak üzere işlem geçmişini getiren ayrı bir metot daha olmalı. Buna göre “UserService” isimli sınıfımızı aşağıdaki şekilde projemize ekleyelim.

export class UserService {
    getUserDetails(userId: string) {
        return {
            id: userId,
            name: "Esma Hanım",
            address: "İstanbul Merkez. PK: 34000"
        }
    }

    updateOrderHistory(userId: string, orderId: string): void {
        console.log(`${orderId} numaralı sipariş ${userId} kullanıcısının geçmişine işlendi`);
    }

    getOrderHistory(orderId: string) {

        return {
            orderId: orderId,
            paymentId: "ABC123",
            shippingId: "XYZ123",
            productId: "batarya01",
            quantity: 1,
            userId: "user01"
        };
    }
}


Sırada stok işlemleri var. Seçilen ürünün stok adedini, fiyatını ve ağırlığını dönen bir metoda ihtiyacımız var. Ayrıca sipariş sırasında ürün adedini stoktan düşecek, iptal sırasında da yükseltecek birer metoda da ihtiyacımız olacak. “InventoryService” isimli sınıfımızı aşağıdaki şekilde projemize ekleyelim.

export class InventoryService {
    getProduct(productId: string) {

        return {
            id: productId,
            stock: 10,
            weight: 5,
            price: 100
        };
    }

    increaseStock(productId: string, quantity: number): void {
        console.log(`${productId} ürününün stok adedi ${quantity} arttırıldı`);
    }

    decreaseStock(productId: string, quantity: number): void {
        console.log(`${productId} ürününün stok adedi ${quantity} azaltıldı`);
    }
}


Üçüncü servisimiz ödeme işlemleri hakkında olacak. İşlem öncesi ödeme onayı alan, işlem sonrasında ödeme işlemini tamamlayan ve sipariş iptalinde de ödemeyi iptal eden birer metoda ihtiyacımız olacak. “PaymentService” isimli sınıfımızı aşağıdaki şekilde projemize ekleyelim.

export class PaymentService {
    initiatePayment(amount: number): string {
        console.log(`${amount} TL tutarında ödeme işlemi başlatıldı, talep kodu ABC123`);

        return "ABC123"
    }

    confirmPayment(paymentId: string): boolean {
        console.log(`${paymentId} kodlu ödeme işlemi onaylandı`);

        return true;
    }

    cancelPayment(paymentId: string): boolean {
        console.log(`${paymentId} kodlu ödeme işlemi iptal edildi`);

        return true;
    }
}


Dördüncü servisimiz kargo işlemleri hakkında olacak. Kullanıcı adresi ve ürün ağırlığına göre kargo ücretini hesaplayan bir metoda, ayrıca kargolama işlemlerini başlatan ve kargolamayı iptal eden birer metoda ihtiyacımız olacak. “ShippingService” isimli sınıfımızı aşağıdaki şekilde projemize ekleyelim.

export class ShippingService {
    calculateShippingCost(address: string, weight: number): number {
        // Temsili kargo ücreti
        return 4 * weight;
    }

    initiateShipping(address: string, productId: string): string {
        console.log(`${address} adresine ${productId} kodlu ürünün kargosu başlatıldı, sipariş kodu: XYZ123`);

        return "XYZ123";
    }

    cancelShipping(orderId: string): void {
        console.log(`${orderId} numaralı siparişin kargolaması iptal edildi`);
    }
}


Son olarak kullanıcıya gerçekleştirilen işlemle alakalı bildirim gönderen “NotificationService” isimli servisimizi de aşağıdaki şekilde projemize ekleyelim.

export class NotificationService {
    sendNotification(userId: string, message: string): void {
        console.log(`${userId} kullanıcısına "${message}" mesajı gönderildi`);
    }
}


Bir hayli servisimiz oldu ama sonuna da geldik. Artık uygulamamızı ayağa kaldıracak ve tüm bu servisleri kullanacak “app.ts” dosyamızı aşağıdaki içerik ile projemize ekleyebiliriz. Öncelikle sipariş verecek ardından da verdiğimiz siparişi iptal edeceğiz.

import {InventoryService} from "./inventory-service";
import {PaymentService} from "./payment-service";
import {ShippingService} from "./shipping-service";
import {UserService} from "./user-service";
import {NotificationService} from "./notification-service";

const inventoryService = new InventoryService();
const paymentService = new PaymentService();
const shippingService = new ShippingService();
const userService = new UserService();
const notificationService = new NotificationService();

// Kullanıcı bilgilerini çekiyoruz
const user = userService.getUserDetails("user01");

// SİPARİŞ VERME İŞLEMİ
// ----------------------

// Stoktan ürün bilgisini çekiyoruz
const product = inventoryService.getProduct("batarya01");

// Stok durumunu kontrol ediyoruz
if (product.stock > 0) {

    const shippingCost = shippingService.calculateShippingCost(user.address, product.weight);

    // Ödeme işlemini başlatıyoruz
    const paymentId = paymentService.initiatePayment(product.price + shippingCost);

    // Ödeme işlemi onaylanırsa
    if (paymentService.confirmPayment(paymentId)) {

        // Stoktan ürün düşülüyor
        inventoryService.decreaseStock(product.id, 1);

        // Kargo işlemi başlatılıyor
        const orderId = shippingService.initiateShipping(user.address, product.id);

        // Kullanıcı işlem geçmişine sipariş bilgisi ekleniyor
        userService.updateOrderHistory(user.id, orderId);

        // Kullanıcıya bildirim gönderiliyor
        notificationService.sendNotification(user.id, `${orderId} numaralı siparişiniz alınmıştır`);
    }
}


console.log();


// SİPARİŞ İPTAL İŞLEMİ
// ----------------------

// Sipariş geçmişi çekiliyor
const order = userService.getOrderHistory("XYZ123");

// Ödeme iptal ediliyor
paymentService.cancelPayment(order.paymentId);

// Stoktan ürün iade ediliyor
inventoryService.increaseStock(order.productId, order.quantity);

// Kargo iptal ediliyor
shippingService.cancelShipping(order.orderId);

// Kullanıcı işlem geçmişine sipariş iptal bilgisi ekleniyor
userService.updateOrderHistory(order.userId, "CANCEL-XYZ123");

// Kullanıcıya bildirim gönderiliyor
notificationService.sendNotification(order.userId, "Siparişiniz iptal edilmiştir");


Fazla sayıda adımdan geçmek zorunda kaldık ancak her servisin kendine ait birer görevi vardı ve bu şekilde hiçbir servis kendisi ile alakası olmayan işleri üstlenmek zorunda kalmadı. Ayrıca istenenleri de eksiksiz karşılamış olduk.

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.


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

Aslında oldukça güzel başlamıştık. Gerçekten de çeşitli iş mantıklarını kendilerine ait servislere ayırarak, servis seviyesinde karmaşayı önlemiş olduk ki, DDD (Domain Driven Design) mimarisindeki temel mantık bunun üzerinde yürümektedir. Böyle bir yapıdan Microservice mimarisine geçmek de tek ve büyük bir sınıfı parçalamaya göre çok daha kolay olacaktır.

Ancak sıkıntı şu ki, servislerimizi kullanacak istemci katmanlar hem her bir servisin fonksiyonlarını hem de bu fonksiyonların hangi sırada ve ne şekilde birbirleriyle çalıştıklarını bilmek zorundalar. Burada Law of Demeter (Principle of Least Knowledge) prensibine aykırı hareket etmiş oluyoruz. Bu da bize daha fazla doküman, daha fazla soru ve bakım maliyeti olarak geri dönecektir.


Nasıl Bir Çözüm Sunulabilirdi?

Burada yazdığımız sistemi çağıran istemci katman (app.js) ile bizim sistemimiz arasında köprü vazifesi görecek ve tüm bu karmaşa ile iş bilgisini içine hapsedecek ara bir katmana ihtiyacımız var. Bu öyle bir katman olmalı ki, dışarıdan bakıldığında sipariş verme ve iptal etme için iki basit metot içermeli. İçeride ise ihtiyaç duyulan tüm çağrımları kendisi yapmalı.

Facade Pattern’e göre bu işlevi görecek “OrderFacade” isimli sınıfımızı aşağıdaki içerik ile projemize ekleyelim.

import {InventoryService} from "./inventory-service";
import {PaymentService} from "./payment-service";
import {ShippingService} from "./shipping-service";
import {UserService} from "./user-service";
import {NotificationService} from "./notification-service";

export class OrderFacade {

    private inventoryService: InventoryService;
    private paymentService: PaymentService;
    private shippingService: ShippingService;
    private userService: UserService;
    private notificationService: NotificationService;

    constructor() {
        this.inventoryService = new InventoryService();
        this.paymentService = new PaymentService();
        this.shippingService = new ShippingService();
        this.userService = new UserService();
        this.notificationService = new NotificationService();
    }

    placeOrder(userId: string, productId: string): void {

        // Kullanıcı bilgilerini çekiyoruz
        const user = this.userService.getUserDetails(userId);

        // SİPARİŞ VERME İŞLEMİ
        // ----------------------

        // Stoktan ürün bilgisini çekiyoruz
        const product = this.inventoryService.getProduct(productId);

        // Stok durumunu kontrol ediyoruz
        if (product.stock > 0) {

            const shippingCost = this.shippingService.calculateShippingCost(user.address, product.weight);

            // Ödeme işlemini başlatıyoruz
            const paymentId = this.paymentService.initiatePayment(product.price + shippingCost);

            // Ödeme işlemi onaylanırsa
            if (this.paymentService.confirmPayment(paymentId)) {

                // Stoktan ürün düşülüyor
                this.inventoryService.decreaseStock(product.id, 1);

                // Kargo işlemi başlatılıyor
                const orderId = this.shippingService.initiateShipping(user.address, product.id);

                // Kullanıcı işlem geçmişine sipariş bilgisi ekleniyor
                this.userService.updateOrderHistory(user.id, orderId);

                // Kullanıcıya bildirim gönderiliyor
                this.notificationService.sendNotification(user.id, `${orderId} numaralı siparişiniz alınmıştır`);
            }
        }
    }

    cancelOrder(orderId: string): void {

        // Sipariş geçmişi çekiliyor
        const order = this.userService.getOrderHistory(orderId);

        // Ödeme iptal ediliyor
        this.paymentService.cancelPayment(order.paymentId);

        // Stoktan ürün iade ediliyor
        this.inventoryService.increaseStock(order.productId, order.quantity);

        // Kargo iptal ediliyor
        this.shippingService.cancelShipping(order.orderId);

        // Kullanıcı işlem geçmişine sipariş iptal bilgisi ekleniyor
        this.userService.updateOrderHistory(order.userId, "CANCEL-XYZ123");

        // Kullanıcıya bildirim gönderiliyor
        this.notificationService.sendNotification(order.userId, "Siparişiniz iptal edilmiştir");
    }
}


Burada “placeOrder” ve “cancelOrder” isminde iki metodumuz var ve isimlerinden de anlaşılacağı gibi sipariş verme ve iptal işlemlerini üstleniyorlar. Aslında kendi içerisinde herhangi bir işi gerçekleştirmiyorlar. Sadece işlemleri gerçekleştirecek alt sistemdeki metotları gerekli sırada çağırıyor ve gelen parametreleri bir sonraki servise geçiyorlar.

Tüm karmaşayı “OrderFacade” sınıfımızda bıraktığımıza göre “app.ts” dosyamızı aşağıdaki şekilde güncelleyebiliriz.

import {OrderFacade} from "./order-facade";

const orderFacade = new OrderFacade();

// SİPARİŞ VERME İŞLEMİ
orderFacade.placeOrder("user01", "batarya01");

console.log();

// SİPARİŞ İPTAL İŞLEMİ
orderFacade.cancelOrder("XYZ123");


Görüldüğü üzere, yazdığımız sistemi kullanmak isteyen katmanların işi oldukça basitleşti. Sipariş vermek ve iptal etmek için birer metot çağırmaları yeterli hale geldi. Artık yazmamız gereken doküman çok daha kısa olacaktır ve muhtemelen bize soru dahi gelmeyecektir.

Facade Pattern’in henüz gözükmeyen diğer bir avantajı da, ileride göreceğimiz Anti-Corruption Layer Pattern‘in ters tarafta duran bir versiyonu olmasıdır. Buna göre sistem içerisinde yapmak zorunda kalacağımız pek çok değişiklik Facade seviyesinde kalacak ve bizi çağıracak katmanlarda değişikliğe sebep olmayacaktır.

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 Facade tasarım kalıbı konusunun sonuna geldik. Bir sonraki makalemizde Bridge tasarım kalıbını işleyeceğiz.

Herkese iyi çalışmalar…

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

Bir Yorum Yaz