Anasayfa » 5. Factory Method Pattern

5. Factory Method Pattern

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

Bu makalemizde, nesne oluşturma sürecini soyutlayarak, nesnelerin nasıl ve hangi sınıfın örnekleri olarak oluşturulacağına karar verme işlemini merkezileştiren Factory Method tasarım kalıbını inceleyeceğiz. Bu serinin genelinde olduğu gibi öncelikle Factory Method 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 Factory Method 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.


Factory Method Tasarım Kalıbı Nedir?

Bu makaleye başlamadan önce hatırlatmak isterim ki Factory Method Pattern, bir önceki makalemiz olan Simple Factory Pattern gibi nesne oluşturma süreciyle ilgilenirken, soyutlama işlevini bir adım daha ileri taşır. Bu sebeple, Factory Method Pattern’i rahat bir şekilde anlayabilmek için Simple Factory Pattern’i iyi derecede kavramış olmamız gerekiyor. Önceki makaleyi henüz okumayanlara, bu makaleden önce Simple Factory Pattern’i okumalarını önemle tavsiye ediyorum.

Bir önceki makalemizde, gelen talebe karşılık Simple Factory Pattern’i kullanmış ve kodumuzu sonradan gelebilecek yeni bir otel hizmetine karşın “VacationCalculator” sınıfımızda herhangi bir değişikliğe ihtiyaç kalmayacak hale getirmiştik. Ancak sizin de gözünüze takılmış olabileceği gibi, “HotelServiceFactory” sınıfımızdaki “switch/if” yapısından kurtulamamıştık. Factory Method Pattern ile soyutlamayı bir adım ileriye taşıyarak, bu karar mekanizmasından da kurtulmamız mümkün hale gelecektir.

Simple Factory Pattern’de tek bir Factory içerisinde, aynı Interface’i uygulayan sınıflardan hangisinin üretileceğine karar veririz. Factory Method Pattern’de ise her sınıfın kendine ait, aynı Interface’i uygulamış birer Factory’si bulunur. Hangi Factory’nin kullanılacağı, dolayısıyla hangi sınıftan nesnenin üretileceği konusuna da en üst katman karar verir. Böylece Factory içerisinde switch/if türevi bir karar mekanizmasına ihtiyacımız kalmaz ve Factory seviyesinde de Open/Closed prensibine sadık kalmış oluruz.


Talebimizi Alalım

Bir e-ticaret sitesi için ödeme enstrümanı entegrasyonu yapıyoruz. Site ödeme için PayPal ve Stripe seçeneklerini sunuyor. Önyüzden yapılan seçime bağlı olarak iki hizmet sağlayıcıdan biri üzerinden ödemenin alınması isteniyor.


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

Talep oldukça basit. 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.

Bir de 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";

PaymentProcessor.processPayment("PayPal", "12345", 1200);
PaymentProcessor.processPayment("Stripe", "67890", 1800);

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

Nerdeyse anlatmaya gerek olmayacak kadar basit bir kodla görev tamamlandı!

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

Bir önceki makalemizde olduğu gibi, bu çözümde de çok ciddi sıkıntılar var. Eğer ki Square gibi sisteme üçüncü bir ödeme enstrümanı eklemek istesek yapmamız gerekenler;

  1. Square için entegrasyon işlemlerini içeren yeni bir sınıf ekleyeceğiz (Gayet doğal)
  2. Bu yeni sınıf için “PaymentProcessor” sınıfına yeni bir else-if ekleyeceğiz
  3. Yeni eklenen bu enstrümanı “app.ts” dosyasında kullanacağız


Yani uygulamanın en iç katmanından en dış katmanına kadar tüm kodu yine değiştirdik ve Yazılım Tasarım Prensipleri isimli makalemizin girişinde anlatılan göle atılan taşın dalgaları gibi bir durumla karşılaştık. Demek ki bu kod da kötü yazılmış. Ayrıca bir önceki makalede yer alan yazılım tasarım prensipleri burada da aynı şekilde çiğnenmiş oldu.


Nasıl Bir Çözüm Sunulabilirdi?

Aslında bu iş “Adapter Pattern” isimli tasarım kalıbının konusu ancak henüz yapısal tasarım kalıplarına geçmedik. Aynı işi, bir önceki makalemizin konusu olan Simple Factory Pattern ile de yapabilirdik ancak yine “switch/if” mantıksal karar mekanizması sıkıntımız burada da devam ederdi. O halde bu sefer Factory Method Pattern‘i deneyelim.

İki ö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}`);
    }
}


Şimdi geldik Factory tarafına. Simple Factory Pattern’den farklı olarak, Factory Method Pattern’de her ödeme enstrümanı için ayrı birer Factory yazmamız gerekir. Bu iki Factory de ortak bir Interface’i uyguluyor olmalı ki birbirleri ile değiş tokuş yapılabilsin. Öncelikle iki Factory sınıfı için gerekli Interface’i aşağıdaki şekilde projeye ekleyelim.

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

export interface IPaymentProviderFactory {
    createPaymentProvider(): IPaymentProvider;
}


createPaymentProvider” adında tek bir metodu olan oldukça basit bir Interface. Metot çağırıldığı taktirde geriye “IPaymentProvider” Interface’ini uygulayan herhangi bir sınıfın örneğini dönüyor (Şu durumda PayPal ve Stripe sınıfları). Şimdi de bu Interface’leri uygulayan Factory metotlarımızı yazalım.

import {IPaymentProviderFactory} from "./payment-provider-factory";
import {IPaymentProvider} from "./ipayment-provider";
import {PayPal} from "./paypal";

export class PayPalFactory implements IPaymentProviderFactory {
    createPaymentProvider(): IPaymentProvider {
        return new PayPal();
    }
}
import {IPaymentProviderFactory} from "./payment-provider-factory";
import {IPaymentProvider} from "./ipayment-provider";
import {Stripe} from "./stripe";

export class StripeFactory implements IPaymentProviderFactory {
    createPaymentProvider(): IPaymentProvider {
        return new Stripe();
    }
}


Burada biri “PayPal“, diğeri de “Stripe” sınıflarını üreten iki ayrı Factory sınıfı görüyoruz. Sınıflar içerisinde herhangi bir switch/if yapısı yok çünkü her sınıf tek çeşit nesne üretiminden sorumlu. Bu iki Factory sınıfı da aynı Interface’i uyguladığı için birbirleri ile değiştirilebilir yapıdalar.

Şimdi de bunları kullanacak “PaymentProcessor” sınıfımızı aşağıdaki şekilde değiştirelim.

import {IPaymentProviderFactory} from "./payment-provider-factory";

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

        const paymentProvider = factory.createPaymentProvider();

        paymentProvider.processPayment(accountNumber, amount);
    }
}


PaymentProcessor” sınıfımızdaki “processPayment” metodumuz ilk parametre olarak “IPaymentProviderFactory” tipinde bir Factory alıyor ve içerisinde bu Factory nesnesinin “createPaymentProvider” metodunu çağırıyor. Dolayısıyla buraya “PayPalFactory” geçildiği durumda, “paymentProvider” nesnesi “PayPal” sınıfının bir örneği oluyor. “StripeFactory” geçildiği durumda ise “paymentProvider” nesnesi “Stripe” sınıfının bir örneği oluyor. “PaymentProcessor” sınıfımız, nesnenin o anda hangi sınıfın örneği olduğunu bilmiyor ve ilgilenmiyor. Sonuçta eline gelen nesne her halükarda, “IPaymentProvider” Interface’ini uygulamış olacak ve bu sınıfın da “processPayment” isimli metodu olmak zorunda. Dolayısıyla geçilen Factory’e uygun üretilen nesnenin “processPayment” metodunu çağırarak işlemi tamamlıyor.

Son olarak “app.ts” dosyamızı aşağıdaki şekilde değiştirelim.

import {PaymentProcessor} from "./payment-processor";
import {PayPalFactory} from "./paypal-factory";
import {StripeFactory} from "./stripe-factory";

PaymentProcessor.processPayment(new PayPalFactory(), "12345", 1200);
PaymentProcessor.processPayment(new StripeFactory(), "67890", 1800);

En üst katmandan, hangi ödeme enstrümanının kullanılacağına, ilgili Factory sınıfının örneğini “processPayment” metoduna ilk parametre olarak geçerek karar verdik.

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, Square gibi sisteme üçüncü bir ödeme enstrümanı eklemek istesek yapmamız gerekenler;

  1. Square için IPaymentProvider Interface’ini uygulayan ve entegrasyon işlemlerini içeren yeni bir sınıf eklemek
  2. Bu sınıfı üreten ve IPaymentProviderFactory Interface’ini uygulayan yeni bir Factory sınıfı eklemek
  3. “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 olmadı. Ö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 Factory Method tasarım kalıbı konusunun sonuna geldik. Bir sonraki makalemizde Abstract Factory tasarım kalıbını işleyeceğiz.

Herkese iyi çalışmalar…

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

Bir Yorum Yaz