Bu yazı Design Patterns/Tasarım Desenleri nedir? başlıklı yazı dizisinin bir parçasıdır.

Bu içerik ağırlıklı olarak refactoring.guru sitesindeki içeriğin tercümesi ve derlenmesinden oluşturulmuştur.

Tüm tasarım desenleri ya da diğer adıyla tasarım kalıplarına yönelik ayrıntılı içeriklere yazının sonundaki bağlantılardan ulaşabilirsiniz.

Factory Method Tasarım Deseni Amacı

Kelime anlamı “Fabrika Metodu” olan Factory Method, üst sınıfta nesneler oluşturmak için bir arabirim sağlayan, ancak alt sınıfların oluşturulacak bu nesne türünü değiştirmesine izin veren bir yaratımsal desen (creational pattern) türüdür.

Sorun

Bir lojistik yönetim uygulaması oluşturduğunuzu hayal edin. Uygulamanızın ilk sürümü sadece kamyon nakliyesini desteklediğinden kodunuzun çoğu "Kamyon” sınıfına göre hazırladınız.

Bir süre sonra uygulamanızın çok popüler hale geldi ve deniz taşımacılığı şirketleri deniz taşımacılığını da uygulamaya eklemeniz için her gün ısrarla sizi aramaya başladı.

Adding a new transportation class to the program causes an issue
Uygulamanıza yeni bir sınıf eklemek kodun tamamı başka bir sınıf içerisine gömülüyken çok da kolay olmayabilir.

Harika bir haber değil mi? Peki ya kodda yapılması gereken değişiklikler? Şu an kodunuzun büyük bölümü "Kamyon" sınıfı ile iç içe geçmiş durumda. Gemi sınıfını eklemek bütün kodda değişiklik yapmayı gerektiriyor. Ayrıca gelecekte başka bir nakliye yöntemi eklemek isterseniz bütün değişiklikleri tekrar yapmanız gerekecek.

Sonuç olarak nakliye aracının tipine göre uygulamanın davranışlarını değiştiren koşullu ifadelerle dolu dağınık bir kodunuz olacak.

Çözüm

Fabrika Metodu/Factory Method deseni doğrudan nesne oluşturma çağrılarını (new operatörü kullanarak), özel bir fabrika metodu çağrısına dönüştürmeyi öneriyor. Nesneler hala new opeartörü ile oluşturuluyor ama fabrika metodu içerisinden çağrılıyor. Fabrika metodunun döndürdüğü nesneler de ürün olarak adlandırılıyor.

The structure of creator classes
Alt sınıflar fabrika metodunun döndürdüğü nesne sınıfını değiştirebilir.

Bu değişiklik ilk bakışta anlamsız görülebilir: nesnenin oluşturulması çağrısını prograın bir yerinden alıp başka bir yerine taşıdık. Fakat şöyle düşünün, artık elinizde bir alt sınıf içerisinde ezerek oluşturacağı ürünün sınıfını değiştirebileceğiniz bir metod var.

Fakat burada bir sınırlama söz konusu: Alt sınıfların farklı ürünler döndürebilmesi için bu ürünlerin ortak bir sınıf (class) ya da arayüze (interface) sahip olmaları gerekiyor. Ayrıca ana sınıftaki fabrika metodunun geri döndürdüğü değerin tipi bu arayüz olarak tanımlanmak zorunda.

The structure of the products hierarchy
Tüm ürünler aynı ara yüzü paylaşmak zorunda.

Örneğin hem Kamyon hem de Gemi sınıfları tasi adında bir metod içeren Transport Interface’ini esas almalı. Her iki sınıfta metodu farklı esas alacaktır; kamyonlar malı karadan taşırken gemiler deniz yoluyla taşıyacaktır. KaraLojistigi sınıfı kamyon nesnesi döndürürken DenizLojistigi sınıfı gemi döndürecektir.

The structure of the code after applying the factory method pattern
Tüm ürün sınıfları ortak bir ara yüz paylaştığı sürece ilgili objeleri içiniz rahat şekilde çağıran koda döndürebilirsiniz.

Fabrika metodunu çağıran kod (genellikle istemci kod olarak adlandırılır) farklı alt sınıfların döndürdüğü bu nesneler arasında bir fark görmeyecektir. İstemci her iki objeyi de teorik olarak nakliye aracı olarak görecektir. İstemci tüm nakliye nesnelerinin tasi metoduna sahip olduğunu bilecektir fakat bu metodun nasıl çalıştığı istemcinin umurunda olmayacaktır.

Uygulanabilirlik

Fabrika metodunu kodunuzun çalışması gereken nesnelerin tipi ve bağımlılıklarını tam olarak bilmediğinizde kullanabilirsiniz.

Fabrika metodu ürün oluşturma kodunu, ürünü kullanan koddan ayırır. Böylece yeni ürün oluşturma kodları kodun geri kalanından bağımsız olarak yazılabilir.

Örneğin yeni bir ürün tipi oluşturmak için sadece yeni bir oluşturucu alt sınıf ekler ve onun içerisinde fabrika metodunu ezersiniz.

Kütüphane ya da frameworkünüzün kullanıcılarının, kütüphanenizin iç bileşenlerini kolayca genişletebilmeleri için fabrika metodunu kullanabilirsiniz.

Inheritance (teknik bir programlama terimi olduğu için Türkçe yazmadım) bir kütüphanenin varsayılan davranışını değiştirmenin en kolay yolu olabilir. Fakat bu kütüphane standart bileşen yerine sizin alt sınıfınızın kullanılması gerektiğini nereden bilecek?

Çözüm kütüphane genelinde bileşenleri oluşturan kodu tek bir fabrika metodu haline dönüştürüp bileşeni genişletmeye ek olarak isteyen herkesin bu metodu da ezmesine izin vermektir.

Bunun nasıl çalışacağını düşünelim. Açık kaynak kodlu bir UI frameworkü kullanan bir uygulama yazdığınızı hayal edin. Uygulamanızın yuvarlak düğmeleri olması lazım ama frameworkte yalnızca dikdörtgen düğmeler var. Standart Buton sınıfını genişleterek yeni ve harika YuvarlakButon alt sınıfını oluşturdunuz. Fakat şimdi bu Framework’e varsayılan düğme sınıfı yerine yeni oluşturduğunuz düğme sınıfını kullanması gerektiğini anlatmanız lazım.

Bunu sağlamak için temel framework sınıfını genişleterek UIYuvarlakButonluFramework alt sınıfını oluşturmalı ve ButonOlustur metodunu ezmelisiniz. Temel sınıfta Buton nesnesi döndüren bu metod, yeni oluşturduğunuz alt sınıfta YuvarlakButon nesnesi döndürecektir. Artık UIFramework sınıfı yerine UIYuvarlakButonluFramework sınıfını kullanarak yuvarlak düğmelere sahip olabilirsiniz.

Yeni nesneler oluşturmak yerine mevcut nesneleri tekrar kullanarak sistem kaynaklarından tasarruf etmek için fabrika metodunu kullanabilirsiniz.

Bu ihtiyaç genellikle veritabanı, dosya sistemi ve ağ kaynakları gibi büyük, yüksek sistem kaynağı gerektiren nesnelerde karşınıza çıkar.

Mevcut bir nesneyi kullanmak için nete ihtiyacınız olduğunu düşünelim:

  1. İlk olarak oluşturulan tüm nesnelerin kaydını tutmak için bir depolama alanına ihtiyacınız var
  2. Birisi bir nesne istediğinde program bu havuzda boş bir nesne var mı bakmalı ve istemci koda döndürmeli
  3. Boş bir nesne yoksa yeni bir tane oluşturmalı (ve havuza eklemeli)

Bunu yapmak için bir sürü kod yazmanız ve tekrar eden kodlarla programı kirletmemek için bunu tek bir yerde yapmanız gerekir.

Bunu yapmak için en uygun yer tekrar kullanılmak istenen nesne sınıfının yapıcı (constructor) bölümüdür. Fakat tanım gereği oluşturucular her zaman yeni nesneler döndürmelidir. Mevcut nesne varlıklarını döndüremezler.

Bu nedenle mevcut nesneleri kullanabilen ve yeni nesnelerde oluşturabilen bir metoda ihtiyacınız var. Bu da tam olarak bir fabrika metodunun yapabileceği bir şey.

Diğer tasarım desenleri ile ilişkisi

  • Bir çok tasarım Factory Method kullanılarak başlar (çok karmaşık olmadığı ve alt sınıflarla özelleştirilebildiği için) ve Abstract Factory, Prototype veya Builder (daha esnek fakat daha karmaşık) tiplerine evrimleşir.
  • Abstract Factory sınıfları genellikle birden fazla Factory Method’dan oluşur fakat Prototype kullanarak da bu sınıf metodlarını oluşturabilirsiniz.
  • Factory Method’u Iterator ile birlikte kullanarak alt sınıf koleksiyonlarının sınıflarla uyumlu farklı iterator’ler döndürmesini sağlayabilirsiniz.
  • Prototype inheritance temelli değildir ve böylece onun dezavantajlarından etkilenmez. Öte yandan Prototype kopyalandığı nesne için karmaşık bir ilk başlatma adımı gerektirir. Factory Method inheritance temellidir fakat bir başlatma adımı gerektirmez.
  • Factory Method Template Method’un özelleştirilmiş halidir. Aynı zamanda büyük Template Methodlar için bir adım görevi görür.

Factory Method Kod Örnekleri

Örnek PHP Kodu


<?php

namespace RefactoringGuru\FactoryMethod\Conceptual;

/**
 * Oluşturucu sınıf bir ürün sınıfı döndürmesi ön görülen bir fabrika metodu tanımlıyor.
 * Bu metodun kullanım şeklini genellikle oluşturucunun alt sınıfları sağlar.
 */
abstract class Creator
{
    /**
     * Oluşturucu fabrika metodu için varsayılan bir kullanım şekli de sunabilir.
     */
     abstract public function factoryMethod(): Product;

    /**
     * Ayrıca oluşturucun adını öyle koysak da aslında ana görevi ürün oluşturmak değildir.
     * Genellikle fabrika metodunun döndürdüğü ürün nesnesine bağlı ana iş mantığı içerir.
     * Alt sınıflar fabrika metodunu ezip geri döndürdüğü ürünü değiştirerek
     * iş mantığını doaylı olarak etkileyebilirler.
     */

    public function someOperation(): string
    {
        // Ürün nesnesi oluşturmak için fabrika metodunu çağır
        $product = $this->factoryMethod();
        // Ürünü kullan
        $result = "Oluşturucu: Aynı oluşturucu kodu şununla çalıştı: " .
            $product->operation();
        return $result
    }
}

/**
 * Somut Oluşturucu Oluşturucunun fabrika metodunu ezerek dönen ürün tipini değiştiriyor

 */
class ConcreteCreator1 extends Creator
{
    /**
     * Note that the signature of the method still uses the abstract product
     * type, even though the concrete product is actually returned from the
     * method. This way the Creator can stay independent of concrete product
     * classes.
     */
    public function factoryMethod(): Product
    {
         return new ConcreteProduct1();
    }
}

class ConcreteCreator2 extends Creator
{
    public function factoryMethod(): Product
    {
        return new ConcreteProduct2();
    }
}

/**
 * Urun Interface'i tüm Somut Ürün tiplerinde geçerli bir operasyon tanımlıyor
 */
interface Product
{
    public function operation(): string;
}

/**
 * Somut ürünler Ürün Interface'inin farklı kullanımlarını sağlarlar.
 */
class ConcreteProduct1 implements Product
{
    public function operation(): string
    {
        return "{Result of the ConcreteProduct1}";
    }
}

class ConcreteProduct2 implements Product
{
    public function operation(): string
    {
        return "{Result of the ConcreteProduct2}";
    }
}


/**
 * İstemci kodu bir somut oluşturucu varlığı ile çalışıyor
 * İstemci oluşturucu ile temel interface'i esas alarak çalıştığı için
 * herhangi bir oluşturucu gönderebilirsiniz
 */
function clientCode(Creator $creator)
{
    // ...
    echo "Istemci: Olusturucunun sınıfından haberim yok, fakat yine de sorunsuz çalışıyor.\n"
        . $creator->someOperation();
    // ...
}

/**
 * Uygulama yapılandırması veya bulunduğu ortama göre bir oluşturucu tipi seçiyor
 */

echo "Uygulama: ConcreteCreator1 ile çalışıyorum.\n";
clientCode(new ConcreteCreator1());
echo "\n\n";

echo "Uygulama: ConcreteCreator2 ile çalışıyorum.\n";
clientCode(new ConcreteCreator2());


Örnek Python Kodu

from __future__ import annotations
from abc import ABC, abstractmethod


class Creator(ABC):
    """
    The Creator class declares the factory method that is supposed to return an
    object of a Product class. The Creator's subclasses usually provide the
    implementation of this method.
    """

    @abstractmethod
    def factory_method(self):
        """
        Note that the Creator may also provide some default implementation of
        the factory method.
        """
        pass

    def some_operation(self) -> str:
        """
        Also note that, despite its name, the Creator's primary responsibility
        is not creating products. Usually, it contains some core business logic
        that relies on Product objects, returned by the factory method.
        Subclasses can indirectly change that business logic by overriding the
        factory method and returning a different type of product from it.
        """

        # Call the factory method to create a Product object.
        product = self.factory_method()

        # Now, use the product.
        result = f"Creator: The same creator's code has just worked with {product.operation()}"

        return result


"""
Concrete Creators override the factory method in order to change the resulting
product's type.
"""


class ConcreteCreator1(Creator):
    """
    Note that the signature of the method still uses the abstract product type,
    even though the concrete product is actually returned from the method. This
    way the Creator can stay independent of concrete product classes.
    """

    def factory_method(self) -> Product:
        return ConcreteProduct1()


class ConcreteCreator2(Creator):
    def factory_method(self) -> Product:
        return ConcreteProduct2()


class Product(ABC):
    """
    The Product interface declares the operations that all concrete products
    must implement.
    """

    @abstractmethod
    def operation(self) -> str:
        pass


"""
Concrete Products provide various implementations of the Product interface.
"""


class ConcreteProduct1(Product):
    def operation(self) -> str:
        return "{Result of the ConcreteProduct1}"


class ConcreteProduct2(Product):
    def operation(self) -> str:
        return "{Result of the ConcreteProduct2}"


def client_code(creator: Creator) -> None:
    """
    The client code works with an instance of a concrete creator, albeit through
    its base interface. As long as the client keeps working with the creator via
    the base interface, you can pass it any creator's subclass.
    """

    print(f"Client: I'm not aware of the creator's class, but it still works.\n"
          f"{creator.some_operation()}", end="")


if __name__ == "__main__":
    print("App: Launched with the ConcreteCreator1.")
    client_code(ConcreteCreator1())
    print("\n")

    print("App: Launched with the ConcreteCreator2.")
    client_code(ConcreteCreator2())

Diğer Tasarım Kalıpları/Design Patterns

Yaratımsal Kalıplar (Creational Patterns)

Yapısal Kalıplar (Structural Patterns)

Davranışsal Kalıplar (Behavioral Patterns)