Anasayfa » 9. Adapter Pattern

9. Adapter Pattern

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

Bu makalemizde, birbirleriyle uyumlu olmayan iki bileşen arasına girerek köprü vazifesini üstlenen, bu iki bileşenin birbirleriyle iletişim kurmasına ve çalışabilmesine imkan sağlayan Adapter tasarım kalıbını inceleyeceğiz. Bu serinin genelinde olduğu gibi öncelikle Adapter 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 Adapter 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.


Adapter Tasarım Kalıbı Nedir?

Klasik tasarım kalıplarından Structural Design Patterns kategorisinde yer alan ilk tasarım kalıbımız olan Adapter tasarım kalıbı, var olan ve değiştirilmesi mümkün olmayan veya değiştirilmek istenmeyen iki bileşenin arasına girerek, bu iki bileşenin birbirleri ile iletişim kurmasını ve birlikte çalışabilmesine imkan sağlamayı amaçlar.

Burada biraz yavaşlayalım ve biraz detaya girelim çünkü yukarıdaki tanıma uymayan ancak Adapter Pattern olduğunu iddia eden pek çok kodlama ile karşılaşıyoruz.

Örneğin cebimizdeki telefonlar 5V doğru akım ile şarj ediliyor. Evimizde duvarda yer alan prizde ise 220V alternatif akım var. Bir kablo ile telefonu prize takarsak, telefonumuz hasar görür (Muhtemelen biz de öyle). Peki duvardaki prizin 5V doğru akım olmasını elektrik idaresinden talep edebilir miyiz? Elbette edemeyiz. Ya cep telefonu üreticisine cihazın 220V ile şarj edilir hale getirilmesini talep edebilir miyiz? Elbette bunu da edemeyiz. Peki ne yaparız? Gider bir dükkandan 220V alternatif akımı 5V doğru akıma çeviren bir adaptör alırız. Bir ucunu duvardaki prize, diğer ucunu da cep telefonuna takıp şarj olmasını sağlarız.

Adapter tasarım kalıbında da yapılmak istenen tam olarak budur. Eğer ki Adapter tasarım kalıbından bahsediyorsak, her iki tarafta da birlikte çalışması talep edilen sistemler hazır demektir. Araya yazılacak kod bloğunda, iki taraftan da değişiklik yapmasını talep edemeyiz. Zaten araya Adapter yazmamızın sebebi, bu sistemlerde değişiklik yapılması ihtiyacını ortadan kaldırmaktır.


Talebimizi Alalım

Bir e-ticaret sitesi, ödeme enstrümanı olarak PayPal ve Amazon Pay ile entegrasyon çalışması yapmamızı istiyor. Aşağıda entegre olmamız gereken her iki servisin de temsili sınıfları yer alıyor (Bunları projemize ekleyerek entegre olacağız). Aşağıdaki servislerde de görüldüğü üzere, PayPal servisi “processPayment” isminde bir fonksiyon içeriyor ve “accountNumber” ile “amount” parametrelerini bekliyor. Amazon Pay servisi ise sırayla çağrılması gereken iki metot içeriyor. Ödeme alabilmek için ilk olarak “getPaymentToken” metoduna “accountNumber” parametresini geçmemiz ve buradan bir “token” değeri almamız gerekiyor. Ardından aldığımız bu token değerini ve “amount” parametresini “processPayment” metoduna geçmemizi bekliyor.

export class PayPalService {
    processPayment(accountNumber: string, amount: number): void {
        console.log(`Paypal ile ödeme alınıyor, Hesap No: ${accountNumber} Tutar: ${amount}`);
    }
}
export class AmazonPayService {
    getPaymentToken(accountNumber: string): string {
        console.log(`Amazon Pay'de ${accountNumber} numaralı hesap için token alınıyor`);
        return "XYZ123";
    }

    processPayment(paymentToken: string, amount: number): void {
        console.log(`Amazon Pay ile ödeme alınıyor, Token: ${paymentToken} Tutar: ${amount}`);
    }
}


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

İki servisin yapısı birbirinden farklı ama küçük bir if ile bu sorunu kolayca çözebiliriz. Öncelikle yukarıdaki iki dosyayı projemize ekleyelim. Ardından 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"
  ]
}


Ödeme işlemlerini gerçekleştirecek “PaymentProcessor” sınıfımızı aşağıdaki içerikle projemize ekleyelim.

import {PayPalService} from "./paypal-service";
import {AmazonPayService} from "./amazon-pay-service";

export class PaymentProcessor {
    executePayment(accountCode: string, amount: number, provider: string): void {

        if (provider === "paypal") {

            const payPalService = new PayPalService();

            payPalService.processPayment(accountCode, amount);

        } else if (provider === "amazonpay") {

            const amazonPayService = new AmazonPayService();

            const token = amazonPayService.getPaymentToken(accountCode);

            amazonPayService.processPayment(token, amount);
        }
    }
}


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 {PaymentProcessor} from "./payment-processor";

let paymentProcessor = new PaymentProcessor();

// Paypal ile ödeme işlemi
paymentProcessor.executePayment("ACC123", 100, "paypal");

// Amazon Pay ile ödeme işlemi
paymentProcessor.executePayment("ACC456", 200, "amazonpay");


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

Geçilen parametreye göre uygun ödeme enstrümanı servisinde işlem gerçekleştiren projemizi tamamlamış olduk.

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ış?

PaymentProcessor” sınıfındaki “executePayment” metodu birden fazla ödeme enstrümanına ait iş bilgisi içeriyor ve bundan sonra eklenebilecek her bir ödeme enstrümanımızın iş bilgisi de bu fonksiyon içerisine ekleniyor olacak. Burada Single Responsibility prensibini çiğnediğimiz açıkça görülüyor. Ayrıca bundan sonra eklenmesi gereken herhangi bir ödeme enstrümanı için bu sınıfı değiştirmemiz gerekecek. Burada da Open/Closed prensibine aykırı düşüyoruz.

Bu tarz entegrasyon işlemlerinde, iki sistemi birbirine adapte etmek için yazılması gereken kodlar oldukça karmaşık olabilir. Bu tarz kodları bir sınıf içerisine hapsetmek ve karmaşanın dışarı çıkmamasını önlemek önemlidir. Aslında Encapsulation prensibinden bahsediyoruz ki, burada da ciddi bir eksikliğimiz var. Yine yapılan değişiklikler bütün projeye yayılıyor.


Nasıl Bir Çözüm Sunulabilirdi?

Liskov Substitution ile işe başlayalım. İhtiyaca göre yazılacak Adapter’ların birbirleri ile değiştirilmesi gerekiyor. Bu yüzden ortak bir Interface’e ihtiyacımız var. “IPaymentAdapter” ismindeki Interface’imizi aşağıdaki şekilde projemize ekleyelim.

export interface IPaymentAdapter {
    processPayment(accountCode: string, amount: number): void;
}


Ardından “PayPalService” ile kendi sistemimiz arasında Adapter vazifesi görecek “PaypalAdapter” sınıfımızı aşağıdaki içerikle projemize ekleyelim.

import {PayPalService} from "./paypal-service";
import {IPaymentAdapter} from "./ipayment-adapter";

export class PaypalAdapter implements IPaymentAdapter {
    private payPalService: PayPalService;

    constructor(paypPalService: PayPalService) {
        this.payPalService = paypPalService;
    }

    processPayment(accountCode: string, amount: number): void {
        this.payPalService.processPayment(accountCode, amount);
    }
}


Burada anlatılacak pek bir karmaşa yok. “processPayment” metodu ile servisteki “processPayment” metodu çağırılıyor. Zaten metotların parametreleri de bire bir aynı.

Sırada “AmazonPayService” ile kendi sistemimiz arasında Adapter vazifesi görecek “AmazonPayAdapter” sınıfı var. Bu sınıfı da aşağıdaki içerikle projemize ekleyelim.

import {AmazonPayService} from "./amazon-pay-service";
import {IPaymentAdapter} from "./ipayment-adapter";

export class AmazonPayAdapter implements IPaymentAdapter {
    private amazonPayService: AmazonPayService;

    constructor(amazonPayService: AmazonPayService) {
        this.amazonPayService = amazonPayService;
    }

    processPayment(accountCode: string, amount: number): void {
        let paymentToken = this.amazonPayService.getPaymentToken(accountCode);

        this.amazonPayService.processPayment(paymentToken, amount);
    }
}


Burada üzerinde durulması gereken küçük bir detay var. “AmazonPayService” sınıfı ödeme işlemi için sırasıyla iki metodu arka arkaya çağırmamızı istiyordu. Buradaki “processPayment” metodunda öncelikle servisteki “getPaymentToken” metodunu çağırarak gerekli token bilgisini alıp bir sonraki metoda geçiyoruz. Yani burada “AmazonPayService” sınıfı özelinde işletilmesi gereken özel bir durum var ancak bu özel durumu mümkünse Adapter içerisindeki “processPayment” metodu içerisinde, değilse de “AmazonPayAdapter” sınıfı içerisinde tutuyoruz ve dışarı çıkmasına izin vermiyoruz.

Burada sarıldığımız Encapsulation prensibi, karmaşanın kodun geri kalanına yayılmasının ve diğer kısımlarda da değişiklik gereksinimi oluşmasının önüne geçer. Böylece istediğimiz kadar farklı ödeme enstrümanı ekleyelim veya farklı sistemlerle entegre olalım, karmaşa Adapter dışına çıkmadığı sürece programın geri kalan kısmı bundan etkilenmez.

Adapter’larımız hazır olduğuna göre artık “PaymentProcessor” sınıfımızı aşağıdaki şekilde değiştirebiliriz.

import {IPaymentAdapter} from "./ipayment-adapter";

export class PaymentProcessor {
    executePayment(accountCode: string, amount: number, provider: IPaymentAdapter): void {
        provider.processPayment(accountCode, amount);
    }
}


Entegrasyon ile ilgili iş mantıkları Adapter sınıfları içerisinde kaldığı için “PaymentProcessor” sınıfımızda, Interface yerine geçilen sınıfın “processPayment” metodunu çağırmaktan başka bir iş kalmadı.

Son olarak hangi Adapter sınıfını kullanacağımıza karar verecek “app.ts” dosyamızı aşağıdaki şekilde güncelleyelim.

import {PaymentProcessor} from "./payment-processor";
import {PaypalAdapter} from "./paypal-adapter";
import {AmazonPayAdapter} from "./amazon-pay-adapter";
import {PayPalService} from "./paypal-service";
import {AmazonPayService} from "./amazon-pay-service";

let paymentProcessor = new PaymentProcessor();
let paypalAdapter = new PaypalAdapter(new PayPalService());
let amazonPayAdapter = new AmazonPayAdapter(new AmazonPayService());

// Paypal ile ödeme işlemi
paymentProcessor.executePayment("ACC123", 100, paypalAdapter);

// Amazon Pay ile ödeme işlemi
paymentProcessor.executePayment("ACC456", 200, amazonPayAdapter);


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


İkinci önerimizde Adapter Pattern’i kullanarak, Encapsulation prensibine bağlı kaldık ve iki sistemi birbirine adapte edebilmek için ihtiyaç duyulan kod karmaşasını, ilgili Adapter içerisine hapsederek arasına girdiğimiz iki sistemde de değişiklik gereksinimi ortaya çıkmasına engel olduk.

Buradaki adaptasyon işlemi oldukça basitti. Gerçek dünyada entegrasyon için başka süreçlerden geçmek, veritabanı veya başka API servisleri gibi dış kaynaklara ulaşmak gerekebilir ancak tüm bu karmaşa Adapter sınıfı içerisinde kaldığı müddetçe hem cebimizdeki telefonu, hem de elektrik idaresini üzmemiş oluruz.

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




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

Herkese iyi çalışmalar…

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

Bir Yorum Yaz