Anasayfa » 4. Simple Factory Pattern

4. Simple Factory Pattern

by Levent KARAGÖL
18 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 Simple Factory tasarım kalıbını inceleyeceğiz. Bu serinin genelinde olduğu gibi öncelikle Simple 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 Simple 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.


Simple Factory Tasarım Kalıbı Nedir?

Simple Factory tasarım kalıbı, 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 bir tasarım kalıbıdır. Yazılım geliştiricilere nesneleri doğrudan üretmek yerine, onları oluşturan bir sınıf veya fonksiyon üzerinden elde etme imkanı tanır. Bu sayede, hem kodun daha modüler, esnek ve kolay yönetilebilir olması sağlanır, hem de nesne üretmek için yazılması gereken kodların birden fazla lokasyonda tekrar tekrar yazılmasının önüne geçilmiş olunur.


Talebimizi Alalım

Otel için tatil ücret hesaplaması yapan bir uygulama yazmamız istendi. Otelde “oda“, “kahvaltı“, “akşam yemeği” ve “havaalanından aktarım için servis” hizmetleri mevcut. Her hizmetin kendi içerisinde çok kompleks fiyatlandırma formülleri var. Tatile çıkacak kişiler, gün ve kişi sayısını belirtip, bu hizmetlerden hangilerinden faydalanmak istediklerini söylüyorlar. Ayrıca tatile çıkacak kişinin VIP üyeliğinin olup olmadığı da fiyat hesaplamasına etki ediyor. Her bir hizmetin fiyatı belirlenirken aşağıdaki değişkenler hesaba katılıyor.

  • Oda: Kişi sayısı, kalınacak gün sayısı, VIP üyeliği
  • Kahvaltı: Kişi sayısı, kalınacak gün sayısı
  • Akşam yemeği: Kişi sayısı, kalınacak gün sayısı
  • Aktarım Servisi: Kişi sayısı, VIP üyeliği


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

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"
  ]
}


Dört ayrı hizmet seçeneği olduğuna göre, her bir hizmet için birer sınıf açsak ve kompleks hesaplamaları kendi içlerinde bıraksak güzel olur sanki. Her bir hizmet için aşağıdaki isimler ve içeriklerle toplam 4 dosya oluşturalım.

export class Room {

    constructor(private peopleCount: number, private duration: number, private hasVip: boolean) {
    }

    calculatePrice(): number {

        // Odaya ait çok kompleks bir fiyatlandırma algoritması
        return (this.peopleCount * this.duration * 10) * (this.hasVip ? 0.8 : 0);
    }
}
export class Breakfast {

    constructor(private peopleCount: number, private duration: number) {
    }

    calculatePrice(): number {

        // Kahvaltıya ait çok kompleks bir fiyatlandırma algoritması
        return (this.peopleCount * this. Duration * 3);
    }
}
export class Dinner {

    constructor(private peopleCount: number, private duration: number) {
    }

    calculatePrice(): number {

        // Akşam yemeğine ait çok kompleks bir fiyatlandırma algoritması
        return (this.peopleCount * this. Duration * 5);
    }
}
export class Shuttle {

    constructor(private peopleCount: number, private hasVip: Boolean) {
    }

    calculatePrice(): number {

        // Servis hizmetine ait çok kompleks bir fiyatlandırma algoritması
        return (this.peopleCount * 8) * (this.hasVip ? 0.5 : 0);
    }
}


Sınıflarımız oldukça basit. Fiyat hesaplamasını etkileyen değişkenler kurucu metottan alınıp sınıf seviyesindeki private değişkenlerde tutuluyor ve her sınıfta bulunan “calculatePrice” metodu bu değerleri kullanarak temsili kompleks fiyat algoritmasında hesaplama yaparak ödenmesi gereken fiyatı geri dönüyor.

Sıra geldi hesaplamayı yaptıracak ara sınıfımıza. “vacation-calculator.ts” isimli dosyamızı aşağıdaki içerik ile oluşturalım.

import {Room} from "./room";
import {Breakfast} from "./breakfast";
import {Shuttle} from "./shuttle";
import {Dinner} from "./dinner";

export class VacationCalculator {

    public static calculatePrice(peopleCount: number, duration: number, hasVip: boolean, hasBreakfast: boolean, hasShuttle: boolean, hasDinner: boolean): number {

        let totalPrice: number = 0;

        const room: Room = new Room(peopleCount, duration, hasVip);

        totalPrice += room.calculatePrice();

        if (hasBreakfast) {

            const breakfast: Breakfast = new Breakfast(peopleCount, duration);

            totalPrice += breakfast.calculatePrice();
        }

        if (hasDinner) {

            const dinner: Dinner = new Dinner(peopleCount, duration);

            totalPrice += dinner.calculatePrice();
        }

        if (hasShuttle) {

            const shuttle: Shuttle = new Shuttle(peopleCount, hasVip);

            totalPrice += shuttle.calculatePrice();
        }

        return totalPrice;
    }
}

Burada da “calculatePrice” isimli statik bir metodumuz var. Kendine geçilen seçeneklere göre ilgili sınıfın fiyatlandırma yapmasını sağlıyor ve dönülen tüm fiyatları toplayarak genel toplamı geri dönüyor.

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

import {VacationCalculator} from "./vacation-calculator";

const totalPrice = VacationCalculator.calculatePrice(4, 3, true, true, true, true);

console.log("Toplam Ücret: ", totalPrice);


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


Tüm kompleks hesaplamalar ilgili hizmetin kendi sınıfında duruyor. Encapsulation filan da yapmış olduk, bitti bu iş!

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

Bu çözümde çok ciddi sıkıntılar var. Yazılım tasarım prensiplerini bir kenara bırakalım. Eğer ki gezi turu gibi otelin bir hizmetini daha fiyatlandırma dahil etmek istesek yapmamız gerekenler;

  1. Gezi turu için fiyatlandırma algoritmasını içeren yeni bir sınıf ekleyeceğiz (Gayet doğal)
  2. Bu yeni sınıf için “VacationCalculator” sınıfına yeni bir else-if ekleyeceğiz
  3. Bu else-if için “calculatePrice” metoduna yeni bir boolean değer ekleyeceğiz
  4. Yeni eklenen bu boolean değeri “app.ts” dosyasından geçeceğiz

Çok fazla katman olmadığı için az gibi gözükebilir ama uygulamanın en iç katmanından en dış katmanına kadar tüm kodu değiştirdik. Tıpkı Yazılım Tasarım Prensipleri isimli makalemizin girişinde anlatılan göle atılan taşın dalgaları gibi bir durumla karşı karşıyayız. Demek ki bu kod kötü yazılmış.

Tasarım prensipleri gözüyle bakarsak, Separation of Concerns ve Single Responsibility konularında sıkıntımız var. Aslında burada gözükmeyen DRY (Don’t Repeat Yourself) konusunda da sıkıntımız var ancak uygulama çok küçük olduğu için belli olmuyor.

Dört ayrı hizmet için dört ayrı sınıf, ihtiyaç duydukları değişkenleri kurucu metotlarından alıyorlar. Bu sınıflardan nesne üretecek sınıf, her bir sınıfın neye ihtiyaç duyduğunu bilmek zorunda. Yani burada “VacationCalculator” sınıfımız sadece hesaplama yaptırmakla kalmıyor, her bir hizmetin ihtiyaç duyacağı değerlere de hakim oluyor. Tüm sınıflar tek bir noktada oluşturulduğu için şu anda çok fazla göze batmıyor ancak genelde uygulamalarımızda birden fazla yerde nesneleri üretmek zorunda kalırız. Üstelik nesneleri üretmek genelde bu kadar kolay da olmaz. Pek çok değere ihtiyaç duyulabilir ve hatta üretimden sonra temel ayarların yapılması için üretilen nesnenin birkaç metodu da çağrılabilir.

Eğer bu nesne üretme işini merkezi bir yere çekmezsek, bu tarz nesne üretim işlemlerini pek çok yere kopyalamaya başlarız. İhtiyaç duyulan bir değişiklikle de tüm kodu gezmeye, yeni değişiklikleri uygulamaya başlarız.

Düzgün tasarlanmış yazılım mimarilerinde bu tarz nesne üretim işleri “Factory” adı verilen ve bu iş için özelleştirilmiş sınıflarda yapılır ve tüm bu karmaşa factory sınıflarının içlerinde bırakılır.


Nasıl Bir Çözüm Sunulabilirdi?

Hedefimiz birbirleri ile mümkün mertebe soyutlanmış parçalardan oluşan yazılımlar üretmekse, Interface ve Abstrat Class’lar bizim vazgeçilmezimiz olacaktır. Dört hizmetin sınıfında asıl işlemi, yani hesaplamayı yapan “calculatePrice” isimli bir metot var. Ortak bir mekanizma kurabilmek için tüm hizmet sınıflarının uygulayacağı “IHotelService” isimli Interface’i aşağıdaki şekilde oluşturalım.

export interface IHotelService {
    calculatePrice(): number;
}


Ardından dört hizmet sınıfında bu Interface’i uygulayalım.

import {IHotelService} from "./ihotel-service";

export class Room implements IHotelService {

    constructor(private peopleCount: number, private duration: number, private hasVip: boolean) {
    }

    calculatePrice(): number {

        // Odaya ait çok kompleks bir fiyatlandırma algoritması
        return (this.peopleCount * this.duration * 10) * (this.hasVip ? 0.8 : 0);
    }
}
import {IHotelService} from "./ihotel-service";

export class Breakfast implements IHotelService {

    constructor(private peopleCount: number, private duration: number) {
    }

    calculatePrice(): number {

        // Kahvaltıya ait çok kompleks bir fiyatlandırma algoritması
        return (this.peopleCount * this. Duration * 3);
    }
}
import {IHotelService} from "./ihotel-service";

export class Dinner implements IHotelService {

    constructor(private peopleCount: number, private duration: number) {
    }

    calculatePrice(): number {

        // Akşam yemeğine ait çok kompleks bir fiyatlandırma algoritması
        return (this.peopleCount * this. Duration * 5);
    }
}
import {IHotelService} from "./ihotel-service";

export class Shuttle implements IHotelService {

    constructor(private peopleCount: number, private hasVip: boolean) {
    }

    calculatePrice(): number {

        // Servis hizmetine ait çok kompleks bir fiyatlandırma algoritması
        return (this.peopleCount * 8) * (this.hasVip ? 0.5 : 0);
    }
}

Dört sınıf da aynı Interface’i uyguladığına göre bu Interface’i parametre olarak alan veya geri dönen bir fonksiyon, bunlardan herhangi birini alsa veya dönse hataya sebep olmayacaktır. Yani sınıflarımız birbirlerinin yerine geçmeye başladı ve Liskov Substitution‘a yaklaşmaya başladık.

Bu prensipten yararlanarak, geçilen Enum’a göre dört sınıftan biri için nesne üreten “HotelServiceFactory” sınıfımızı aşağıdaki içerikle oluşturalım.

import {Room} from "./room";
import {Breakfast} from "./breakfast";
import {Shuttle} from "./shuttle";
import {Dinner} from "./dinner";
import {IHotelService} from "./ihotel-service";

export enum ServiceType {
    Room,
    Breakfast,
    Dinner,
    Shuttle
}

export type vacationInfo = {
    peopleCount: number,
    duration: number,
    hasVip: boolean,
    services: ServiceType[]
};

export class HotelServiceFactory {

    static createService(serviceType: ServiceType, vacationInfo: vacationInfo): IHotelService {

        switch (serviceType) {
            case ServiceType.Room:
                return new Room(vacationInfo.peopleCount, vacationInfo.duration, vacationInfo.hasVip);
            case ServiceType.Breakfast:
                return new Breakfast(vacationInfo.peopleCount, vacationInfo.duration);
            case ServiceType.Dinner:
                return new Dinner(vacationInfo.peopleCount, vacationInfo.duration);
            case ServiceType.Shuttle:
                return new Shuttle(vacationInfo.peopleCount, vacationInfo.hasVip);
            default:
                throw new Error("Geçersiz hizmet tipi");
        }
    }
}

Burada biraz durup kodu inceleyelim. Öncelikle “ServiceType” isimli bir enum tanımladık ve dört hizmet tipi için dört ayrı içerik ekledik. Ayrıca “vacationInfo” isminde bir tip tanımı ekledik. Parametreleri tek tek geçmektense, “Dto” mantığında nesne içerisinde aldık. Böylece ileride yeni bir hizmet tipi eklendiğinde, Dto nesnemizin içeriği değiştirmek yeterli olacak ve fonksiyon parametreleri ile oynamak zorunda kalmayacağız.

Son olarak Factory içerisine “createService” isimli bir metot ekledik. Bu metot “IHotelService” tipinde dönüş yapıyor. Yani aldığı enum değerine göre dört hizmet tipinden biri için nesne üretip geri dönse, dönüş yaptığı yerde herhangi bir hataya sebep olmayacaktır.

Nesneleri üretmek Factory sınıflarının temel görevi olduğundan, hangi sınıfın hangi parametreyi beklediğini bilmesi, Separation of Concerns‘i etkileyen bir durum değildir.

Kodumuzu bir sınıf daha ekleyerek kalabalıklaştırmış olduk ancak karşılığını şimdi göreceğiz. “VacationCalculator” sınıfımızı aşağıdaki şekilde güncelleyelim.

import {HotelServiceFactory, vacationInfo} from "./hotel-service-factory";

export class VacationCalculator {

    public static calculatePrice(vacationInfo: vacationInfo): number {

        let totalPrice: number = 0;

        for (const service of vacationInfo.services) {

            const serviceInstance = HotelServiceFactory.createService(service, vacationInfo);

            totalPrice += serviceInstance.calculatePrice();
        }

        return totalPrice;
    }
}


Burada ciddi bir değişime şahit oluyoruz. “VacationCalculator” sınıfı hizmetlerden komple soyutlanmış hale geldi. Hangi hizmetin seçildiği, hangi parametreleri beklediği, hatta kaç hizmet seçildiğiyle bile ilgilenmiyor. Burada Law of Demeter (Principle of Least Knowledge) prensibinin etkilerini görüyoruz. Sınıfımız ilgilenmediği konular yüzünden değişikliğe uğramak zorunda da kalmayacaktır. Yani yeni bir hizmet eklenmesi veya parametrelerinin değişmesi bu sınıfta değişikliğe sebep olmaz. Bu sınıf gelecekteki değişimlere karşın değişikliklere kapalıdır. Bu da bizi Open/Closed prensibine götürür.

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

import {VacationCalculator} from "./vacation-calculator";
import {ServiceType} from "./hotel-service-factory";

const totalPrice = VacationCalculator.calculatePrice({
    peopleCount: 4,
    duration: 3,
    hasVip: true,
    services: [ServiceType.Room, ServiceType.Breakfast, ServiceType.Dinner, ServiceType.Shuttle]
});

console.log("Toplam Ücret: ", totalPrice);

Artık kullanılan hizmetler için boolean değer geçmek yerine “services” isimli diziye satın alınmak istenen hizmetlerin enum değerlerini geçiyoruz. Böylece gelecekte eklenecek yeni bir hizmet için sadece en üst katmadan bu yeni servisin Enum değerini geçip, ara katmanlara dokunmadan Factory katmanında bu değeri karşılamak yeterli olacaktır.

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


Burada sunduğumuz ikinci çözüm önerisinde bir Interface, bir Dto ve bir Factory sınıfı eklemek, sonradan yapılacak değişikliklerde bakım maliyetimizi ciddi anlamda düşürmüş oldu.

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



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

Herkese iyi çalışmalar…

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

5 yorum

Nebahat 28 Haziran 2023 - 20:42

Öncelikle emeğinize sağlık örnekler çok güzel olmuş, ben kodu derleyip çalıştırıldığımda, “Uncaught ReferenceError: exports is not defined”, hatası alıyorum (index.html üzerinden app.js çağırıyorum), internetten araştırdığımda tsconfig.json dosyasında bazı parametrelerin değiştirilmesi ile ilgili öneriler var pekçok farklı kombinasyon denedim fakat başarılı olamadım, sizin fikriniz varmıdır sorun nedir. (Not: Typescript bilgim sizin makalelerden okuduğum kadar) Teşekkür ederim.

Cevap Yaz
Levent KARAGÖL 29 Haziran 2023 - 01:17

Design Patterns kategorisindeki kodlarda kullandığımız tsconfig.json dosyası, Node.JS ile sunucu tarafında çalışacak şekilde ayarlı. Bu dosyadaki “module”: “commonjs” ifadesi, derleme sonrasında oluşan JavaScript dosyalarının web tarayıcılar üzerinde çalışmasına engel oluyor. Bunun yerine https://www.leventkaragol.com/typescript/12-typescriptde-tsconfig-ayarlari/#penci-tsconfigjson-Dosyasi-Ornekleri adresindeki makalede yer alan “tsconfig.json Dosyası Örnekleri” başlıklı bölümdeki ilk örnek tsconfig.json dosyasını kullanabilirsiniz. Bu dosya web tarayıcılar üzerinde çalışabilecek şekilde JavaScript dosyalarının üretilmesini sağlayacaktır.

Cevap Yaz
Nebahat 1 Temmuz 2023 - 20:15

Bahsettiğiniz tsconfig.json dosyasını denediğimde de sonuç alamadım

Cevap Yaz
Levent KARAGÖL 3 Temmuz 2023 - 19:24

Bu projeler node.js için hazırlandığından, herhangi bir module loader kullanmadan derleme sonucu oluşan dosyaları doğrudan web sayfasında kullanmak için ne yazık ki bir miktar angaryaya ihtiyacımız var.

Öncelikle html dosyasına script eklerken aşağıdaki şekilde type=”module” ifadesi eklenmesi gerekli, böylece tarayıcı es6 modülü olarak dosyaları yükleyebilecektir.


<script type="module" src="breakfast.js"></script>
<script type="module" src="dinner.js"></script>
<script type="module" src="room.js"></script>
<script type="module" src="shuttle.js"></script>
<script type="module" src="vacation-calculator.js"></script>
<script type="module" src="ihotel-service.js"></script>
<script type="module" src="hotel-service-factory.js"></script>
<script type="module" src="app.js"></script>

Ayrıca oluşan dosyalardaki import ifadelerinde sadece dosya isimleri yazıyor ancak tarayıcı “.js” uzantısını da isteyecektir, aksi taktirde her bir dosya için 404 hatası verecektir. Bu yüzden;

* hotel-service-factory.js
* vacation-calculator.js
* app.js

dosyalarının başında yer alan import ifadelerine “.js” uzantısı eklemek gerekecektir.

Örnek :

import { HotelServiceFactory } from "./hotel-service-factory";

yerine

import { HotelServiceFactory } from "./hotel-service-factory.js";

Bu işlemlerin ardından sayfa açılarak işlem sonucu tarayıcı konsolunda belirecektir.

Cevap Yaz
Nebahat 5 Temmuz 2023 - 17:03

“tsc app.ts” komutu ile derlediğim app.ts dosyasından aşağıdaki şekilde app.js dosyası oluşuyor, yani import ifadesi oluşan dosyalarda yok, ben sonradan eklediğimde de aynı hatayı vermeye devam ediyor.

“use strict”;
Object.defineProperty(exports, “__esModule”, { value: true });
var vacation_calculator_1 = require(“./vacation-calculator”);
var hotel_service_factory_1 = require(“./hotel-service-factory”);
var totalPrice = vacation_calculator_1.VacationCalculator.calculatePrice({
peopleCount: 4,
duration: 3,
hasVip: true,
services: [hotel_service_factory_1.ServiceType.Room, hotel_service_factory_1.ServiceType.Breakfast, hotel_service_factory_1.ServiceType.Dinner, hotel_service_factory_1.ServiceType.Shuttle]
});
console.log(“Toplam Ücret: “, totalPrice);

Cevap Yaz

Bir Yorum Yaz