Teknik Detaylar

Flyweight Tasarım Deseni Nedir?

← Teknik Detaylar
2021-09-14 · 9 dk okuma
Flyweight Tasarım Deseni Nedir?
Bu yazıyı yapay zekâ ile tartış
Sayfayı kopyala

💡 Özet (TL;DR):

  • Amacı: Benzer nesnelerin ortak özelliklerini (içsel durum/intrinsic state) paylaşarak bellek (RAM) kullanımını minimize eden yapısal (structural) bir tasarım desenidir.
  • Çözdüğü Sorun: Çok sayıda nesne oluşturulması gereken durumlarda (örneğin oyunlardaki partikül sistemleri) nesne başına düşen bellek maliyetini azaltarak uygulamanın yetersiz bellek nedeniyle çökmesini engeller.
  • Yöntem: Nesne durumu ikiye ayrılır: Değişmeyen veriler (İçsel Durum) Flyweight nesnesinin içinde kalır; değişen veriler (Dışsal Durum) ise dışarıya taşınır ve Flyweight metotlarına parametre olarak geçilir.

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 bellek (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şaalı 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şatabileceğinizi 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ızda kısa süre sonra yavaşlama ve donmalar başladı; birkaç dakikalık oyun süresinden sonra oyun kilitlenip kapandı. Debug kayıtlarını kurcalayarak saatler harcadıktan sonra oyunun yetersiz RAM miktarı yüzünden çöktüğünü fark ettiniz. Arkadaşınızın bilgisayarı sizinkine göre daha güçsüz olduğu için siz bu sorunla karşılaşmazken o kısa sürede sorun yaşadı.

Sorunun partikül sisteminden kaynaklandığını fark ettiniz. 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 sahipti.

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 metotlara 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 State) 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 dizi (array) alanları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.

Flyweight tasarım deseni

Daha şık bir çözüm ise bir bağlam sınıfı (context class) oluşturup dışsal durum (extrinsic state) ve flyweight nesnesinin referansını bu sınıfta tutmaktır. Game sınıfımız içinde ise bu sınıflardan oluşan tek bir dizi (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 Durum Ayrımı: İçsel vs. Dışsal Durum

Özellikİçsel Durum (Intrinsic State)Dışsal Durum (Extrinsic State)
TanımNesnenin kimliğini oluşturan ve değişmeyen sabit veriler.Nesnenin içinde bulunduğu bağlama göre değişen dinamik veriler.
Örnek (Oyun Partikülü)Merminin rengi, dokusu (sprite/texture), görsel boyutu.Merminin o anki X/Y koordinatları, hızı, hareket vektörü.
Depolama YeriFlyweight nesnesinin içinde saklanır.İstemci/kapsayıcı (container) sınıfta saklanır.
PaylaşımBirden fazla nesne tarafından ortaklaşa paylaşılır.Paylaşılamaz; her nesne örneği için benzersizdir.

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 metot, 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 durum hedef makinedeki 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 ile İliş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 indirgeyebiliyorsanı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();

Ö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

Oluşumsal Kalıplar (Creational Patterns)

Factory Method, Abstract Factory, Builder, Prototype, Singleton

Yapısal Kalıplar (Structural Patterns)

Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy

Davranışsal Kalıplar (Behavioral Patterns)

Chain of Responsibility, Command, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor