Anasayfa » 6. Abstract Factory Pattern

6. Abstract Factory Pattern

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

Bu makalemizde, nesne oluşturma sürecini soyutlayarak, farklı sınıf ailelerinin birbirleriyle uyumlu şekilde çalışması ve oluşturulması işlemini merkezileştiren Abstract Factory tasarım kalıbını inceleyeceğiz. Bu serinin genelinde olduğu gibi öncelikle Abstract Factory 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 Abstract Factory 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.


Abstract Factory Tasarım Kalıbı Nedir?

Bu makaleye başlamadan önce hatırlatmak isterim ki Abstract Factory Pattern, bir önceki makalemiz olan Factory Method Pattern gibi nesne oluşturma süreciyle ilgilenirken, aynı anda birden fazla aileye ait sınıfların birbirleriyle uyumlu şekilde çalışması konusuyla da ilgilenir. Bu sebeple, Abstract Factory Pattern’i rahat bir şekilde anlayabilmek için Factory Method Pattern’i iyi derecede kavramış olmamız gerekiyor. Önceki makaleyi henüz okumayanlara, bu makaleden önce Factory Method Pattern’i okumalarını önemle tavsiye ediyorum.

Bir önceki makalemizde Factory Method Pattern kullanarak, sonradan gelen yeni sınıflara rağmen eskiden yazılmış olan sınıflara ve Factory’lere dokunmadan, Open/Closed prensibine bağlı kalarak kodumuzu nasıl genişletebileceğimizi işlemiştik. Abstract Factory pattern, bunun yanında birbirleriyle ilişkili veya bağımlı sınıfların bir grup (veya “aile”) oluşturmasına izin vererek, bunların birbirleriyle uyumlu şekilde çalışmasına da imkan sağlar.


Talebimizi Alalım

Bir önceki makalede aldığımız talep üzerine ek geliştirme talebi aldık. Bahsi geçen e-ticaret sitesi Avrupa ve Amerika’ya hizmet veriyor. Ancak iki bölgede kullandığı ödeme enstrümanları ve teslimat firmaları farklılık gösteriyor. Avrupa’dan gelen siparişler için ödeme enstrümanı olarak PayPal ve teslimat için DHL, Amerika’dan gelen siparişler için ödeme enstrümanı olarak Stripe ve teslimat için Fedex ila çalışıyorlar. Bu duruma uygun bir altyapı kurmamız isteniyor.



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

Bir önceki makalede ödeme sistemleri entegrasyonu yazmıştık. Aynı şekilde başlayarak sonradan teslimat firması ile entegrasyonu ekleyebiliriz. Projeye başlarken 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"
  ]
}

İki ayrı ödeme enstrümanı için birer sınıf açarak ilgili işlemleri kendi içlerinde bırakalım. Her bir enstrüman için aşağıdaki isimler ve içeriklerle birer dosya oluşturalım.

export class PayPal {
    processPayment(accountNumber: string, amount: number): void {
        console.log(`Paypal ile ödeme alınıyor, Hesap No: ${accountNumber} Tutar: ${amount}`);
    }
}
export class Stripe {
    processPayment(accountNumber: string, amount: number): void {
        console.log(`Stripe ile ödeme alınıyor, Hesap No: ${accountNumber} Tutar: ${amount}`);
    }
}

Sınıflarımız oldukça basit. Her sınıfta bulunan “processPayment” metodu, temsili ödeme işlemlerini gerçekleştirerek konsola işlem hakkında bilgi yazıyor.

Sıra geldi kullanılacak ödeme enstrümanına karar verecek “PaymentProcessor” sınıfına. Dosyayı aşağıdaki isim ve içerikle oluşturalım.

import {PayPal} from "./paypal";
import {Stripe} from "./stripe";

export class PaymentProcessor {
    static processPayment(provider: string, accountNumber: string, amount: number): void {

        if (provider === "PayPal") {

            const payPal = new PayPal();

            payPal.processPayment(accountNumber, amount);

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

            const stripe = new Stripe();

            stripe.processPayment(accountNumber, amount);

        } else {

            throw new Error("Geçersiz ödeme sistemi");
        }
    }
}

Sınıfımızda “processPayment” isimli statik bir metodumuz var. Kendine geçilen “provider” değerine göre ilgili hizmet sağlayıcı kurum üzerinden tahsilat işlemini gerçekleştiriyor.

Bu yapının bir kopyasını teslimat firmaları için de yazdık mı tamamdır. Her bir firma için aşağıdaki isimler ve içeriklerle birer dosya oluşturalım.

export class Dhl {
    shipOrder(orderId: string): void {
        console.log(`Dhl ile kargo gönderiliyor, Şipariş Kodu: ${orderId}`);
    }
}
export class Fedex {
    shipOrder(orderId: string): void {
        console.log(`Fedex ile kargo gönderiliyor, Şipariş Kodu: ${orderId}`);
    }
}

Bunlar da oldukça basit sınıflar. Her sınıfta bulunan “shipOrder” metodu, temsili teslimat işlemlerini gerçekleştirerek konsola işlem hakkında bilgi yazıyor.

Şimdi de kullanılacak teslimat firmasına karar verecek “ShippingProcessor” sınıfını yazalım.

import {Fedex} from "./fedex";
import {Dhl} from "./dhl";

export class ShippingProcessor {
    static shipOrder(provider: string, orderId: string): void {

        if (provider === "Fedex") {

            const fedex = new Fedex();

            fedex.shipOrder(orderId);

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

            const dhl = new Dhl();

            dhl.shipOrder(orderId);

        } else {

            throw new Error("Geçersiz nakliye sistemi");
        }
    }
}

Son olarak uygulamamızın giriş kapısı olacak “app.ts” isimli dosyamızı aşağıdaki içerik ile oluşturalım.

import {PaymentProcessor} from "./payment-processor";
import {ShippingProcessor} from "./shipping-processor";

// Avrupa için Paypal ve DHL
PaymentProcessor.processPayment("PayPal", "12345", 1200);
ShippingProcessor.shipOrder("DHL", "112233")

// Amerika için Stripe ve Fedex
PaymentProcessor.processPayment("Stripe", "67890", 1800);
ShippingProcessor.shipOrder("Fedex", "445566")

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

Bir önceki makalede yazdığımız kodu biraz daha genişletmiş olduk ancak hala basit ve rahat anlaşılabilir bir kodumuz var.

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

Son iki makalede işlediğimiz, değişikliklerin dalga şeklinde tüm koda yayılması ve diğer prensip problemleri burada da aynen devam ediyor. Bunlara ek olarak bir de Avrupa ve Amerika seçenekleri için doğru sınıfları seçme sorumluluğu da “app.ts” dosyasında kalmış durumda. Üstelik tek sorunumuz, hangi bölgenin hangi ödeme enstrümanı/teslimat firması çiftini kullanması gerektiği bilgisinin en üst katmada kalması, yani Separation of Concerns de değil. Bu katmandan birbirleri ile uyumsuz iki çift seçilmesi durumunda, uygulamamız tasarlanmadığı, yani kurallara aykırı şekilde de çalışabiliyor.


Nasıl Bir Çözüm Sunulabilirdi?

Bir önceki makalemizde olduğu gibi burada da, iki ödeme enstrümanımız da aynı yapıdaki “processPayment” metoduna sahip. Birbirlerinin yerine geçebilmesi için ikisine ortak bir Interface uygulamamız gerekiyor. “IPaymentProvider” isimli Interface’i aşağıdaki şekilde projeye ekleyelim.

export interface IPaymentProvider {
    processPayment(accountNumber: string, amount: number): void;
}

Ardından her iki ödeme enstrümanı sınıfına da bu Interface’i uygulayalım.

import {IPaymentProvider} from "./ipayment-provider";

export class PayPal implements IPaymentProvider {
    processPayment(accountNumber: string, amount: number): void {
        console.log(`Paypal ile ödeme alınıyor, Hesap No: ${accountNumber} Tutar: ${amount}`);
    }
}
import {IPaymentProvider} from "./ipayment-provider";

export class Stripe implements IPaymentProvider {
    processPayment(accountNumber: string, amount: number): void {
        console.log(`Stripe ile ödeme alınıyor, Hesap No: ${accountNumber} Tutar: ${amount}`);
    }
}


Benzer şekilde iki teslimat firması da aynı yapıdaki “shipOrder” metoduna sahip. Birbirlerinin yerine geçebilmesi için ikisine ortak bir Interface uygulamamız gerekiyor. “IShippingService” isimli Interface’i aşağıdaki şekilde projeye ekleyelim.

export interface IShippingService {
    shipOrder(orderId: string): void;
}

Ardından her iki teslimat firması sınıfına da bu Interface’i uygulayalım.

import {IShippingService} from "./ishipping-service";

export class Dhl implements IShippingService {
    shipOrder(orderId: string): void {
        console.log(`Dhl ile kargo gönderiliyor, Şipariş Kodu: ${orderId}`);
    }
}
import {IShippingService} from "./ishipping-service";

export class Fedex implements IShippingService {
    shipOrder(orderId: string): void {
        console.log(`Fedex ile kargo gönderiliyor, Şipariş Kodu: ${orderId}`);
    }
}


Böylece her iki ödeme enstrümanı ve teslimat firması birbirlerinin yerine geçebilir hale geldi. Bu iki çift, Avrupa için “Paypal” ve “DHL“, Amerika için ise “Stripe” ve “Fedex” şeklinde birlikte çalışması gerekiyor, iş kuralımız bu şekilde. Bunu sağlayan özel birer Factory sınıfı yazacağız ancak öncesinde bu iki Factory sınıfının da birbirlerinin yerine geçebilmesi için, şablon görevi görecek bir abstract sınıf tanımlayacağız. “ECommerceServiceFactory” ismindeki bu sınıfı aşağıdaki şekilde projemize ekleyelim.

import {IPaymentProvider} from "./ipayment-provider";
import {IShippingService} from "./ishipping-service";

export abstract class ECommerceServiceFactory {
    abstract createPaymentProvider(): IPaymentProvider;
    abstract createShippingService(): IShippingService;
}


Bu soyut bir sınıf, yani doğrudan nesne üretilerek kullanılamaz. Ancak başka sınıflar tarafından kalıtım yoluyla devralınabilir ve bu sınıflar, buradaki tüm abstract metotları kendi içlerinde barındırmak zorundadır (Kullanım şeklini bir çeşit Interface gibi düşünebiliriz). Biz bu sınıfı, her iki bölge için de yazacağımız Factory sınıflarına şablon olması amacıyla kullanacağız. Sınıfımızda “createPaymentProvider” ve “createShippingService” isminde iki soyut metodumuz var ve bunların az önce tanımladığımız Interface’leri uygulayan birer nesne dönmeleri gerekiyor.

Öncelikle Avrupa için bu sınıfı devralan uygun bir Factory sınıfını aşağıdaki şekilde projemize ekleyelim.

import {ECommerceServiceFactory} from "./ecommerce-service-factory";
import {PayPal} from "./paypal";
import {Dhl} from "./dhl";

export class EUECommerceServiceFactory extends ECommerceServiceFactory {
    createPaymentProvider() {
        return new PayPal();
    }

    createShippingService() {
        return new Dhl();
    }
}


Avrupa için eklediğimiz “EUECommerceServiceFactory” ismindeki Factory sınıfımız, devraldığı abstract “ECommerceServiceFactory” sınıfı sebebiyle kendi içerisinde “createPaymentProvider” ve “createShippingService” metotlarını içermesi ve belirtilen Interface’leri uygulamış nesneleri dönmesi zorunlu. Dolayısıyla “createPaymentProvider” metodu geriye “PayPal” sınıfının bir örneğini, “createShippingService” metodu da geriye “Dhl” sınıfının bir örneğini dönüyor.

Benzer şekilde Amerika için aynı sınıfı devralan uygun bir Factory sınıfını aşağıdaki şekilde projemize ekleyelim.

import {ECommerceServiceFactory} from "./ecommerce-service-factory";
import {Stripe} from "./stripe";
import {Fedex} from "./fedex";

export class USECommerceServiceFactory extends ECommerceServiceFactory {
    createPaymentProvider() {
        return new Stripe();
    }

    createShippingService() {
        return new Fedex();
    }
}


Bu sınıf da aynı şekilde, devraldığı abstract “ECommerceServiceFactory” sınıfı sebebiyle kendi içerisinde “createPaymentProvider” ve “createShippingService” metotlarını içermesi ve belirtilen Interface’leri uygulamış nesneleri dönmesi zorunlu. Dolayısıyla “createPaymentProvider” metodu geriye “Stripe” sınıfının bir örneğini, “createShippingService” metodu da geriye “Fedex” sınıfının bir örneğini dönüyor.

Daha da önemlisi, bu iki Factory sınıfı aynı abstract sınıfı devraldığı için, birbirleri ile değiştirilebilir yapıya sahip. Liskov Substitution prensibine uygun şekilde devam ediyoruz.

Şimdi geldik, en kritik noktaya. Tüm bu sınıfların yönetimini yapacak “ECommerceService” isimli sınıfımızı aşağıdaki şekilde projemize ekleyelim.

import {ECommerceServiceFactory} from "./ecommerce-service-factory";
import {IPaymentProvider} from "./ipayment-provider";
import {IShippingService} from "./ishipping-service";

export class ECommerceService {

    private paymentProvider: IPaymentProvider;
    private shippingService: IShippingService;

    constructor(private eCommerceServiceFactory: ECommerceServiceFactory) {

        this.paymentProvider = eCommerceServiceFactory.createPaymentProvider();
        this.shippingService = eCommerceServiceFactory.createShippingService();
    }

    processPayment(accountNumber: string, amount: number): void {

        this.paymentProvider.processPayment(accountNumber, amount);
    }

    shipOrder(orderId: string): void {

        this.shippingService.shipOrder(orderId);
    }
}

Bu sınıf kurucu metodundan, az önce tanımladığımız abstract sınıf türünde bir Factory nesnesi alıyor. Aldığı bu nesnenin, Avrupa’ya mı, yoksa Amerika’ya mı ait olduğunu bilmiyor ve ilgilenmiyor. Tek bildiği ve ilgilendiği, gelen nesnenin “ECommerceServiceFactory” abstract sınıfını devralması sebebiyle içerisinde “createPaymentProvider” ve “createShippingService” metotlarını bulundurması ve bunların da “IPaymentProvider” ve “IShippingService” Interface’ini uygulamış birer nesne dönmesinin zorunlu olduğu. Dolayısıyla kurucu metot içerisinde bu metotları çağırarak, kullanılması uygun olan nesneleri (Avrupa için PayPal ve DHL, Amerika için Stripe ve Fedex) elde ediyor. Ardından kendi içerisindeki “processPayment” ve “shipOrder” metotlarında bu nesneleri kullanıyor.

Hangi kıtada hangi firmaların kullanılması gerektiği konusu bu sınıfın değil, ilgili kıta için oluşturulmuş Factory sınıflarının sorumluluğunda, dolayısıyla sonradan eklenecek yeni bir kıta için bu sınıfta herhangi bir değişiklik yapmamıza gerek kalmayacaktır. Burada Separation of Concerns‘e uymamız, bizi Open/Closed prensibine de yaklaştırdı.

Ayrıca burada ilk kez Dependency Injection kullanımına da şahit oluyoruz. Kurucu metotta aldığımız “ECommerceServiceFactory” abstract sınıfı sayesinde, hangi kıtaya ait Factory sınıfını kullanmamız gerektiği kararını bir üst katmana devrediyoruz. Bu da bizi Dependency Inversion prensibine götürüyor.

Tüm bunlarla birlikte, sonradan eklenebilecek yeni bir ödeme enstrümanı veya teslimat firması sebebiyle mevcut kodlarımızı açarak değiştirmek zorunda kalmadan uygulamamızı genişletme imkanına sahip olacağız.

Son olarak “ECommerceService” sınıfını kullanacak ve uygulamamızın giriş kapısı olacak “app.ts” dosyamızı da aşağıdaki şekilde güncelleyelim.

import {ECommerceService} from "./ecommerce-service";
import {EUECommerceServiceFactory} from "./eu-ecommerce-service-factory";
import {USECommerceServiceFactory} from "./us-ecommerce-service-factory";

// Avrupa için Paypal ve DHL
const euECommerceService = new ECommerceService(new EUECommerceServiceFactory());
euECommerceService.processPayment("12345", 1200);
euECommerceService.shipOrder("112233");

// Amerika için Stripe ve Fedex
const usECommerceService = new ECommerceService(new USECommerceServiceFactory());
usECommerceService.processPayment("67890", 1800);
usECommerceService.shipOrder("445566");

En üst katmandan, hangi kıtaya ait işlem yapmamız gerektiğini seçtik ve geri kalan her bir karar, daha alt kademedeki ilgili sınıflar tarafından alındı.

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


Bir önceki öneri ile aynı çıktıyı elde ettik. Ancak önceki öneriden farklı olarak, Asya için Alipay ödeme yöntemi ve UPS teslimat firması seçeneğini sisteme eklemek istesek yapmamız gerekenler;

  1. Alipay için IPaymentProvider Interface’ini uygulayan ve entegrasyon işlemlerini içeren yeni bir sınıf eklemek
  2. UPS için IShippingService Interface’ini uygulayan ve entegrasyon işlemlerini içeren yeni bir sınıf eklemek
  3. Asya için ECommerceServiceFactory abstract sınıfını devralan ve yukarıdaki iki firmayı kullanan yeni bir Factory sınıfı eklemek
  4. “app.ts” dosyasında bu yeni Factory’i kullanmak


şeklinde olacaktır. Bu tasarım kalıbında “switch/if” türevi bir mantıksal mekanizmaya ihtiyacımız kalmadığı gibi önceden yazılan kodları da değiştirmedik. Yeni fonksiyonalite için sadece yeni dosyalar ekledik. Dolayısıyla Open/Closed prensibine daha sıkı sarılmış olduk. Önceden yazılmış kodları değiştirirken, bunları bozma ihtimalimiz de ortadan kalktı.

Her ne kadar projede oluşturduğumuz dosya sayısında artış olsa ve ilk geliştirme maliyetimiz yükselse de, orta ve uzun vadede bakım ve ek geliştirme maliyetlerini düşürdük. Yaptığımız her çalışma bizi Loose Coupling prensibine biraz daha yaklaştırdı.

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



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

Herkese iyi çalışmalar…

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

Bir Yorum Yaz