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.

Flyweight tasarım deseninin amacı

Flyweight her bir nesnede tüm verileri tutmak yerine, ortak kısımları paylaşarak mevcut hafıza (RAM) miktarına daha fazla nesne sığdırmanıza olanak sağlayan bir yapısal tasarım desenidir.

Sorun

Uzun çalışma saatlerinden sonra biraz eğlenmek için basit bir oyun yapmaya karar verdiniz. Bu oyunda oyuncular küçük bir harita üzerinde dolaşarak birbirlerine vuracaklar. Madem yapıyorum, şöyle gerçekçi bir partikül sistemi ekleyeyim, şaşalı bir şey yapayım dediniz. Çok oyunculu bu oyunda her yerden fırlayan mermiler, roketler ve patlamalardan etrafa saçılan şarapnel parçaları ile muazzam bir oyuncu deneyimi yaşatabilirim diye düşündünüz.

Oyunu tamamladınız, build aldınız ve test etmesi için arkadaşınıza gönderdiniz. Oyun sizin makinenizde harika çalışmasına rağmen arkadaşınız da kısa süre sonra yavaşlama ve donmalar başladı, bir kaç dakikalık oyun süresinden sonra oyun kitlenip kapandı. Debug kayıtlarını kurcalayarak saatler harcadıktan sonra oyunun yetersiz RAM miktarı yüzünden çöktüğünü farkettiniz. Arkadaşınızın bilgisayarı sizinkine göre daha güçsüz olduğu için siz bu sorunla karşılamazken o kısa sürede sorun yaşadı.

Sorunun partikül sisteminden kaynaklandığını farkettiniz. Kurşun, roket veya şarapnel parçası gibi her bir partikül ayrı bir nesne olarak ekranda yer alıyor ve ciddi miktarda veri içeriyordu. Oyuncunun ekranında partikül sayısı artıp sınırı zorladıkça yeni partiküller artık RAM’e sığmıyor ve oyunun çökmesine neden oluyordu.

Flyweight tasarım deseni hangi sorunu çözer?

Çözüm

Particle sınıfını yakından incelediğinizde, renk ve sprite alanlarının diğerlerine göre çok daha fazla yer kapladığını, daha da kötüsü bu alanların aslında tüm partiküllerde aynı ya da benzer olduğunu gördünüz. Örneğin tüm mermiler aynı renk ve sprite’a shaipti.

Flyweight tasarım deseni nedir?

Partikülün koordinat, hareket vektörü ve hızı gibi diğer özellikleri her partikül için benzersizdi ve zamanla değişiyordu. Bu veri partikülün yaşamı boyunca değişkenken renk ve sprite özellikleri her partikül için sabitti.

Bir nesnenin bu gibi sabit verilerine genellikle içsel durum ( intrinsic state) denir. Bu bilgiler nesnenin içinde saklanır, diğer nesneler bu verileri okuyabilir fakat değiştiremez. Nesnenin diğer durumları genellikle nesnenin dışındaki diğer nesneler tarafından değiştirilir ve bunlara da dışsal durum ( extrinsic state ) denir.

Flyweight deseni dışsal durumu nesnenin içinde saklamamanızı, bunun yerine bu durum verisine ihtiyacı olan metodlara aktarmanızı tavsiye eder. Nesnenin içinde sadece içsel durum yer alır ve farklı bağlamlarda ( context ) yeniden kullanılabilir. Sonuç olarak sadece içsel durumları farklı olan, daha az sayıda nesneye ihtiyacınız olur.

Flyweight tasarım deseni ne sağlar?

Tekrar oyunumuza geri dönelim. Dışsal durumu partikül sınıfımızın dışına aldığımızı düşünürsek oyundaki tüm partikülleri sadece 3 nesne ile ifade edebiliriz: mermi, roket ve şarapnel parçası. Tahmin edeceğiniz üzere sadece içsel durumu saklayan bu nesnelere flyweight denir.

Dışsal durum ( Extrinsic Sate) saklama alanı

Nesnenin içinde sadece içsel durumu bıraktık peki ya dışsal durum değişkenleri nereye gidecek? Bir nesnenin bu değerleri saklıyor olması lazım değil mi? Çoğu durumda bunlar deseni uygulamadan önce nesneleri toparlayan kapsayıcı ( container ) nesnesinin içine gider.

Bizim oyunumuzda bu partikülleri particles alanında saklayan ana Game nesnesidir. Dışsal durumu bu sınıfın içine taşımak için her partikülün koordinatları, vektörleri ve hızını içeren bir dizi array alanına ihtiyacımız olacaktır. Fakat bununla bitmez, bir alanda da bu partikülü temsil eden spesifik flyweight nesnesinin referansını saklamanız gerekir. Bu

Flyweight tasarım deseni

Daha şık bir çözüm ise bir bağlam sınıfı (contenxt class) oluşturup dışsal durum ( extrinsic satate) ve flyweight nesnesinin referansını bu sınıfta tutmak. Game sınıfımız içinde ise bı sınfılardan oluşan tek bir array değişkeni tutmak olacaktır.

Ama bir dakika, bunun bize ne faydası oldu? İlk başta oluşturduğumuz nesne sayısı kadar bu bağlam nesnelerinden oluşturmamız gerekmeyecek mi? Teknik olarak evet, fakat bu nesneler öncekilere göre çok daha küçük. Sonuç olarak en fazla hafıza işgal eden bölüm flyweight nesnesinin içine aktarıldı. Şimdi binlerce küçük bağlam nesnesi tek bir büyük flyweight nesnesine referans vererek aynı verinin binlerce kopyası yerine sadece tek bir kopya kullanabilecek.

Flyweight ve değişmezlik

Flyweight nesnesi farklı bağlamlar (context) içinden kullanılabileceği için durumunun (state) değiştirilemez olduğundan emin olmalısınız. Bir flyweight nesnesi constructor parametreleri ile bir kere oluşturulduktan sonra değiştirilememeli, herhangi bir public alan veya setter metodu içermemelidir.

Flyweight Factory

Birden çok flyweight nesne içinden gerekene daha rahat ulaşabilmek için mevcut flyweight nesnelerini bir havuzda tutacak bir factory method oluşturabilirsiniz. Bu metod, oluşturulmak istenen flyweight nesnesinin içsel durumunu bir parametre olarak alır, mevcut flyweight nesneleri arasında bu içsel durumla örtüşen var mı bakar ve eğer bulursa onu döndürür, bulamazsa yeni bir nesne oluşturur ve onu da bu havuza dahil eder.

Uygulanabilirlik

Belirli bir nesnenin çok fazla sayıda kopyası oluşturulup hafızada da daha az yer kaplaması isteniyorsa Flyweight tasarım kalıbı kullanılabilir.

Bu desenin sağladığı faydalar nerede ve nasıl kullanıldığı ile doğrudan ilgilidir. En kullanışlı olduğu durumlar;

  • Uygulamanın birbirine benzer çok fazla sayıda nesneyi oluşturması gerekiyorsa
  • Bu hedef makinadaki tüm hafızayı tüketiyorsa ( veya örneğin backend uygulamalarında mevcut hafıza ile daha fazla istek karşılanabilmesi isteniyorsa )
  • nesneler birbirinin aynı durumlara sahip ve bu durumları ayrıştırıp nesneler arasında paylaştırmak mümkünse

Diğer tasarım desenleri/kalıpları ile ilişkisi

  • Composite tasarım deseninde ortak kullanılan yaprakları Flyweight olarak tasarlayarak hafızadan tasarruf edebilirsiniz.
  • Flyweight çok sayıda küçük nesne oluşturmayı sağlarken Facade tek bir nesnenin geniş bir sistemi kapsamasını sağlar.
  • Eğer nesnelerin paylaştıkları durumları tek bir flyweight nesnesine indirgeyebiliyosanız Flyweight‘lar Singleton olabilir. Fakat bu iki desen arasındaki iki önemli fark unutulmamalıdır.
    1. Sadece tek bir Singleton örneği olması gerekirken, Flyweight sınıflarının farklı iç durumları tanımlayan birden fazla örneği olabilir.
    2. Singleton nesneler değiştirilebilir (mutable) olabilirken, Flyweight nesneleri değiştirilemezdir. (immutable)

Flyweight Deseni Kod Örnekleri

Örnek PHP Kodu

<?php

namespace RefactoringGuru\Flyweight\Conceptual;

/**
 * The Flyweight stores a common portion of the state (also called intrinsic
 * state) that belongs to multiple real business entities. The Flyweight accepts
 * the rest of the state (extrinsic state, unique for each entity) via its
 * method parameters.
 */
class Flyweight
{
    private $sharedState;

    public function __construct($sharedState)
    {
        $this->sharedState = $sharedState;
    }

    public function operation($uniqueState): void
    {
        $s = json_encode($this->sharedState);
        $u = json_encode($uniqueState);
        echo "Flyweight: Displaying shared ($s) and unique ($u) state.\n";
    }
}

/**
 * The Flyweight Factory creates and manages the Flyweight objects. It ensures
 * that flyweights are shared correctly. When the client requests a flyweight,
 * the factory either returns an existing instance or creates a new one, if it
 * doesn't exist yet.
 */
class FlyweightFactory
{
    /**
     * @var Flyweight[]
     */
    private $flyweights = [];

    public function __construct(array $initialFlyweights)
    {
        foreach ($initialFlyweights as $state) {
            $this->flyweights[$this->getKey($state)] = new Flyweight($state);
        }
    }

    /**
     * Returns a Flyweight's string hash for a given state.
     */
    private function getKey(array $state): string
    {
        ksort($state);

        return implode("_", $state);
    }

    /**
     * Returns an existing Flyweight with a given state or creates a new one.
     */
    public function getFlyweight(array $sharedState): Flyweight
    {
        $key = $this->getKey($sharedState);

        if (!isset($this->flyweights[$key])) {
            echo "FlyweightFactory: Can't find a flyweight, creating new one.\n";
            $this->flyweights[$key] = new Flyweight($sharedState);
        } else {
            echo "FlyweightFactory: Reusing existing flyweight.\n";
        }

        return $this->flyweights[$key];
    }

    public function listFlyweights(): void
    {
        $count = count($this->flyweights);
        echo "\nFlyweightFactory: I have $count flyweights:\n";
        foreach ($this->flyweights as $key => $flyweight) {
            echo $key . "\n";
        }
    }
}

/**
 * The client code usually creates a bunch of pre-populated flyweights in the
 * initialization stage of the application.
 */
$factory = new FlyweightFactory([
    ["Chevrolet", "Camaro2018", "pink"],
    ["Mercedes Benz", "C300", "black"],
    ["Mercedes Benz", "C500", "red"],
    ["BMW", "M5", "red"],
    ["BMW", "X6", "white"],
    // ...
]);
$factory->listFlyweights();

// ...

function addCarToPoliceDatabase(
    FlyweightFactory $ff, $plates, $owner,
    $brand, $model, $color
) {
    echo "\nClient: Adding a car to database.\n";
    $flyweight = $ff->getFlyweight([$brand, $model, $color]);

    // The client code either stores or calculates extrinsic state and passes it
    // to the flyweight's methods.
    $flyweight->operation([$plates, $owner]);
}

addCarToPoliceDatabase($factory,
    "CL234IR",
    "James Doe",
    "BMW",
    "M5",
    "red",
);

addCarToPoliceDatabase($factory,
    "CL234IR",
    "James Doe",
    "BMW",
    "X1",
    "red",
);

$factory->listFlyweights();
 Output.txt: Execution result
FlyweightFactory: I have 5 flyweights:
Chevrolet_Camaro2018_pink
Mercedes Benz_C300_black
Mercedes Benz_C500_red
BMW_M5_red
BMW_X6_white

Client: Adding a car to database.
FlyweightFactory: Reusing existing flyweight.
Flyweight: Displaying shared (["BMW","M5","red"]) and unique (["CL234IR","James Doe"]) state.

Client: Adding a car to database.
FlyweightFactory: Can't find a flyweight, creating new one.
Flyweight: Displaying shared (["BMW","X1","red"]) and unique (["CL234IR","James Doe"]) state.

FlyweightFactory: I have 6 flyweights:
Chevrolet_Camaro2018_pink
Mercedes Benz_C300_black
Mercedes Benz_C500_red
BMW_M5_red
BMW_X6_white
BMW_X1_red

Örnek Python Kodu

import json
from typing import Dict


class Flyweight():
    """
    The Flyweight stores a common portion of the state (also called intrinsic
    state) that belongs to multiple real business entities. The Flyweight
    accepts the rest of the state (extrinsic state, unique for each entity) via
    its method parameters.
    """

    def __init__(self, shared_state: str) -> None:
        self._shared_state = shared_state

    def operation(self, unique_state: str) -> None:
        s = json.dumps(self._shared_state)
        u = json.dumps(unique_state)
        print(f"Flyweight: Displaying shared ({s}) and unique ({u}) state.", end="")


class FlyweightFactory():
    """
    The Flyweight Factory creates and manages the Flyweight objects. It ensures
    that flyweights are shared correctly. When the client requests a flyweight,
    the factory either returns an existing instance or creates a new one, if it
    doesn't exist yet.
    """

    _flyweights: Dict[str, Flyweight] = {}

    def __init__(self, initial_flyweights: Dict) -> None:
        for state in initial_flyweights:
            self._flyweights[self.get_key(state)] = Flyweight(state)

    def get_key(self, state: Dict) -> str:
        """
        Returns a Flyweight's string hash for a given state.
        """

        return "_".join(sorted(state))

    def get_flyweight(self, shared_state: Dict) -> Flyweight:
        """
        Returns an existing Flyweight with a given state or creates a new one.
        """

        key = self.get_key(shared_state)

        if not self._flyweights.get(key):
            print("FlyweightFactory: Can't find a flyweight, creating new one.")
            self._flyweights[key] = Flyweight(shared_state)
        else:
            print("FlyweightFactory: Reusing existing flyweight.")

        return self._flyweights[key]

    def list_flyweights(self) -> None:
        count = len(self._flyweights)
        print(f"FlyweightFactory: I have {count} flyweights:")
        print("\n".join(map(str, self._flyweights.keys())), end="")


def add_car_to_police_database(
    factory: FlyweightFactory, plates: str, owner: str,
    brand: str, model: str, color: str
) -> None:
    print("\n\nClient: Adding a car to database.")
    flyweight = factory.get_flyweight([brand, model, color])
    # The client code either stores or calculates extrinsic state and passes it
    # to the flyweight's methods.
    flyweight.operation([plates, owner])


if __name__ == "__main__":
    """
    The client code usually creates a bunch of pre-populated flyweights in the
    initialization stage of the application.
    """

    factory = FlyweightFactory([
        ["Chevrolet", "Camaro2018", "pink"],
        ["Mercedes Benz", "C300", "black"],
        ["Mercedes Benz", "C500", "red"],
        ["BMW", "M5", "red"],
        ["BMW", "X6", "white"],
    ])

    factory.list_flyweights()

    add_car_to_police_database(
        factory, "CL234IR", "James Doe", "BMW", "M5", "red")

    add_car_to_police_database(
        factory, "CL234IR", "James Doe", "BMW", "X1", "red")

    print("\n")

    factory.list_flyweights()

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)