Anasayfa » 12. Flyweight Pattern

12. Flyweight Pattern

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

Bu makalemizde, ortak özelliklere sahip yüksek sayıda benzer nesnenin oluşturulması gereken durumlarda hafıza optimizasyon tekniği olarak kullanılan Flyweight tasarım kalıbını inceleyeceğiz. Bu serinin genelinde olduğu gibi öncelikle Flyweight 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 Flyweight 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.


Flyweight Tasarım Kalıbı Nedir?

Özellikle nesne tabanlı programlamada, bellekte çok sayıda nesne bulunduran yazılımlarda, bu nesnelerin içerdiği özelliklerin bir kısmı komple bağımsız ve farklı değerlere sahip olurken, bir kısmı da ortak değerlere sahip olabilirler. Flyweight tasarım kalıbı, bu ortak değerleri bellekte tek bir noktada toplayarak, tüm nesnelerin bu ortak noktadan referans yoluyla bu değerlere ulaşmasını hedefler. Böylece çok sayıda aynı değeri bellekte saklamak yerine, tek bir değer saklanarak bellek kullanımında ciddi bir kazanç sağlanabilir.

Kabul etmemiz gerekir ki, yazılım geliştiricilerin büyük çoğunluğu, üzerinde çalıştıkları projenin yapısı sebebiyle nesne tabanlı programlamadan uzak olma eğilimindedir. Çoğu zaman veri kaynağından değer çekip ekranda gösterme veya tam tersi veri kaynağını besleme işlerini üstleniriz ve bu tarz işlerde nesne tabanlı programlamaya pek ihtiyaç yoktur, hatta bazen zararlı dahi olabilir.

Bu sebeple Flyweight tasarım kalıbının nerede kullanılabileceğinin ilk başta akla gelmemesi gayet normaldir. Aklımızda daha rahat canlandırabilmek için birkaç örnek verelim.

  • Twitter gibi tek bir kişinin yaptığı paylaşımların doğrudan paylaşıldığı ortamlar (Hızlı etkileşim için bunlar bellekte tutulabilir)
  • Ekranda çizilen ve tekrar düzenlenebilen nesnelerin yer aldığı Photoshop benzeri çizim programları
  • Her türlü oyun (Özellikle real time strategy türevi çok sayıda karakterin var olduğu oyunlar)
  • Zengin metin formatının desteklendiği Word benzeri kelime işlemciler
  • Her bir hücresinde statik içerik veya formül içeren Excel benzeri hesap tabloları
  • Müzik notaları ve diğer işaretlerin kullanıldığı Musescore benzeri yazılımlar
  • Sürükle-bırak yöntemi ile içerik üretilmesine imkan sağlayan CMS uygulamaları
  • Gerçek koşullarının modellendiği simülasyon yazılımları
  • AutoCad ve Blender türevi CAD/CAM yazılımları


Talebimizi Alalım

Terminalde kullanılmak üzere bir metin editörü projesinde görev alıyoruz. Kullanıcıların klavyeden bastıkları her harf bir dizi içerisinde tutulacak ve ekranda gösterilecek. Kullanıcılar ekrana yazılan her bir harfin aşağıdaki niteliklerini özelleştirebilecekler.

  • Font boyutu
  • Font rengi
  • Artalan rengi (Vurgu rengi)
  • Font ailesi
  • Stili (İtalik, kalın)
  • Efekti (Üst karakter, alt karakter)

Biz sadece girilen karakterleri nitelikleri ile birlikte bir dizide saklamaktan sorumluyuz. Bunların ekranda gösterilmesiyle ekibin geri kalanı ilgilenecek.


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

Nesne tabanlı bir yaklaşımla gidersek, her harfi bir nesne olarak düşünebiliriz ve burada sayılan nitelikler de bu nesnenin birer özelliği olur. Ekstra bir özellik olarak da basılan tuşun değerini nesne içerisinde saklayabiliriz. 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"
  ]
}


Her bir karaktere ait bilgileri tutacak “Character” sınıfını aşağıdaki şekilde projemize ekleyelim.

export class Character {
    constructor(public fontSize: number,
                public fontColor: string,
                public backgroundColor: string,
                public fontFamily: string,
                public style: string,
                public effect: string,
                public value: string) {
    }
}


Son olarak uygulamamızı ayağa kaldıracak “app.ts” dosyamızı aşağıdaki içerik ile oluşturalım ve dizimize birkaç harf ekleyelim.

import {Character} from "./character";

let characterList: Character[] = [];

characterList.push(new Character(12, "black", "none", "Arial, sans-serif", "italic, bold", "none", "A"));
characterList.push(new Character(12, "black", "none", "Arial, sans-serif", "bold", "superscript", "B"));
characterList.push(new Character(14, "red", "yellow", "Times New Roman, serif", "italic", "none", "C"));
characterList.push(new Character(16, "red", "none", "Times New Roman, serif", "italic, bold", "subscript", "D"));
characterList.push(new Character(18, "red", "none", "Arial, sans-serif", "regular", "none", "E"));
characterList.push(new Character(20, "white", "black", "Times New Roman, serif", "bold", "none", "F"));


Bu yöntemle diziye istenildiği kadar harf eklenebilir ve istenen her bir harfin niteliği özgürce özelleştirilebilir.

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

Burada güzel bir tecrübe ile karşı karşıyayız. Önerideki sıkıntımız Don’t Repeat Yourself prensibine aykırı olması ancak ilk bakışta görülmesi oldukça zor. Hatta görebilmek için soyut düşünmek gerekiyor çünkü zaten tek bir sınıfımız var ve hiçbir kod kendini tekrar etmiyor. Burada tekrar eden, oluşturduğumuz nesnelerin özellikleri için bellekte işkal ettiğimiz alan.

Bir metin belgesi düşünün. Elbette başlıklar, dip notlar ve benzeri farklı formatlara sahip yazıları da içerecektir ancak metin belgesindeki karakterlerin en az %95’i aynı özelliklere sahiptir. Yani bizim oluşturduğumuz her bir nesnedeki niteliklerin tamama yakını birbirleri ile aynı. Sadece “value” değerleri birbirinden farklı. Birazdan göreceğimiz gibi bunları ayrı bir nesneye taşısak ve aynı özelliklerdeki tüm karakterlerde bu tek referansı kullansaydık, bellekte kapladığımız alanda ciddi bir optimizasyon sağlamış olacaktık.

Ama bu işe girişmeden önce mevcut önerinin bellekte ne kadar yer kapladığını kontrol edelim. “app.ts” dosyamızı aşağıdaki şekilde değiştirelim.

import {Character} from "./character";

const writeMemoryUsage = function (time: string) {

    console.log(time);
    console.log("--------------------------------------------------------------");

    const usedBefore = process.memoryUsage();

    for (let key in usedBefore) {
        console.log(`${key}: ${Math.round((usedBefore[key as keyof NodeJS.MemoryUsage] / 1024 / 1024) * 100) / 100} MB`);
    }

    console.log();
};

let characterList: Character[] = [];

writeMemoryUsage("Before");

for (let i = 0; i < 5000000; i++) {

    characterList.push(new Character(12, "black", "none", "Arial, sans-serif", "italic, bold", "none", "A"));
    characterList.push(new Character(12, "black", "none", "Arial, sans-serif", "bold", "superscript", "B"));
    characterList.push(new Character(14, "red", "yellow", "Times New Roman, serif", "italic", "none", "C"));
    characterList.push(new Character(16, "red", "none", "Times New Roman, serif", "italic, bold", "subscript", "D"));
    characterList.push(new Character(18, "red", "none", "Arial, sans-serif", "regular", "none", "E"));
    characterList.push(new Character(20, "white", "black", "Times New Roman, serif", "bold", "none", "F"));
}

writeMemoryUsage("After");


Burada “writeMemoryUsage” isminde, bellekte kapladığımız alanı gösteren bir fonksiyon ekledik ve karakterleri oluşturmadan hemen önce ve sonrasında bu fonksiyonu çağırarak terminal ekranına yazdırılmasını sağladık. Ayrıca bir döngü ile yazılan karakter sayısını 30 milyona çektik (Düz metin belgesi olsaydı yaklaşık 30 MB civarında yer kaplayacaktı).

Fonksiyonumuzda “process” ifadesini kullandığımız için TypeScript bizden Node.JS için tip tanım dosyalarını isteyecektir. Bunun için terminalde aşağıdaki komutu çalıştırmamız yeterli olacaktır.

npm install @types/node --save-dev


Artık uygulamamızı derleyip çalıştırabiliriz. Aşağıdaki gibi bir ekran görüntüsü ile karşılaşmış olmamız gerekiyor.


Burada 2.5GB’ın biraz üzerinde bir bellek kullanımı görüyoruz.


Nasıl Bir Çözüm Sunulabilirdi?

Flyweight tasarım kalıbı bu tarz durumlar için güzel bir öneri olabilir. Bu tasarım kalıbını kullanmak için öncelikle ortak olan (Intrinsic) ve ortak olmayan (Extrinsic) özellikleri birbirinden ayırmamız gerekiyor. Böylece ortak olan özellikleri ayrı bir nesneye taşıyıp, aynı özellikleri kullanan tüm karakterlerde bu nesnenin bir referansını kullanma imkanına kavuşuruz.

Bizim örneğimizde ortak olmayan (Extrinsic) tek özelliğimiz “value” değeri. Bu özellik “Character” sınıfının içerisinde kalacak. Diğer tüm özellikler ise farklı bir nesneye taşınacak. Öncelikle bu ortak nesne için “ICharacterProperties” isimli bir Interface tanımlayarak işe başlayalım.

export interface ICharacterProperties {
    fontSize: number;
    fontColor: string;
    backgroundColor: string;
    fontFamily: string;
    style: string;
    effect: string;
}


Ardından bu Interface’i uygulayan ortak özellikleri içerisinde barındıracak olan “CharacterProperties” sınıfını aşağıdaki şekilde projemize ekleyelim.

import {ICharacterProperties} from "./icharacter-properties";

export class CharacterProperties implements ICharacterProperties {
    constructor(public fontSize: number,
                public fontColor: string,
                public backgroundColor: string,
                public fontFamily: string,
                public style: string,
                public effect: string) {
    }
}


Ortak özellikler kendilerine ait bir sınıfa geçtiğine göre “Character” sınıfını aşağıdaki gibi sadeleştirebiliriz.

import {ICharacterProperties} from "./icharacter-properties";

export class Character {
    constructor(public characterProperties: ICharacterProperties, public value: string) {
    }
}


Sıra geldi duruma göre yeni bir “CharacterProperties” nesnesi oluşturacak, duruma göre de var olan bir “CharacterProperties” nesnesini referans olarak geçecek Factory sınıfımıza.

import {CharacterProperties} from "./character-properties";
import {Character} from "./character";

export class CharacterFlyweightFactory {

    private characterPropertiesTable: { [key: string]: CharacterProperties } = {};

    public createCharacter(fontSize: number, fontColor: string, backgroundColor: string, fontFamily: string, style: string, effect: string, value: string): Character {

        const key = `${fontSize}-${fontColor}-${backgroundColor}-${fontFamily}-${style}-${effect}`;

        if (!(key in this.characterPropertiesTable)) {
            this.characterPropertiesTable[key] = new CharacterProperties(fontSize, fontColor, backgroundColor, fontFamily, style, effect);
        }

        return new Character(this.characterPropertiesTable[key], value);
    }
}


Burada biraz yavaşlamakta fayda var. Öncelikle sınıf seviyesinde “characterPropertiesTable” isminde, üretilen tüm “CharacterProperties” nesnelerinin tutulduğu bir nesne yer alıyor. Bu nesne bir hash table gibi görev yapıyor ve kendisine verdiğimiz “key” değerine karşılık gelen nesneyi içerisinde barındırıyor. Böylece ihtiyaç durumunda “key” değeri üzerinden sorgulayarak böyle bir nesnenin olup olmadığını kontrol edebiliyor ve yoksa yeni bir nesne oluşturabiliyoruz.

Bu yöntemin çalışabilmesi için, oluşturulan her bir “CharacterProperties” nesnesi için tekillik sağlayacak özel bir değere ihtiyacımız var. Bunu elde etmenin pek çok yolu var. Nesne “JSON.stringify” ile serialize edilebilir veya nesnenin hash değeri çıkartılabilir. Ama burada örneği karmaşıklaştırmamak adına, geçilen tüm özellikleri yan yana koyarak bir string ifade elde ettik. Böylece altı özelliğin herhangi birinde değişiklik olduğu anda yeni bir key değeri üretilmiş olacak.

Ürettiğimiz key değerinin tabloda olmadığı durumda yeni bir nesne oluşturarak tabloya eklenmesini sağladık. Ardından tabloda yer alan nesneyi “Character” sınıfının kurucu metoduna parametre olarak geçtik. Bu bir nesne olduğu için referans tipinde işlem görecek ve sınıfa sadece referansı geçecektir. Böylece aynı özelliklere sahip tüm “Character” nesneleri, “characterPropertiesTable” içerisindeki aynı nesnenin referansını almış olacaktır.

Çalışmalarımızın işe yarayıp yaramadığını görmek için “app.ts” dosyamızı aşağıdaki şekilde değiştirelim.

import {CharacterFlyweightFactory} from "./character-flyweight-factory";
import {Character} from "./character";

const writeMemoryUsage = function (time: string) {

    console.log(time);
    console.log("--------------------------------------------------------------");

    const usedBefore = process.memoryUsage();

    for (let key in usedBefore) {
        console.log(`${key}: ${Math.round((usedBefore[key as keyof NodeJS.MemoryUsage] / 1024 / 1024) * 100) / 100} MB`);
    }

    console.log();
};

const factory = new CharacterFlyweightFactory();

let characterList: Character[] = [];

writeMemoryUsage("Before");

for (let i = 0; i < 5000000; i++) {

    characterList.push(factory.createCharacter(12, "black", "none", "Arial, sans-serif", "italic, bold", "none", "A"));
    characterList.push(factory.createCharacter(12, "black", "none", "Arial, sans-serif", "bold", "superscript", "B"));
    characterList.push(factory.createCharacter(14, "red", "yellow", "Times New Roman, serif", "italic", "none", "C"));
    characterList.push(factory.createCharacter(16, "red", "none", "Times New Roman, serif", "italic, bold", "subscript", "D"));
    characterList.push(factory.createCharacter(18, "red", "none", "Arial, sans-serif", "regular", "none", "E"));
    characterList.push(factory.createCharacter(20, "white", "black", "Times New Roman, serif", "bold", "none", "F"));
}

writeMemoryUsage("After");


Uygulamamızı derleyip çalıştırdığımızda aşağıdaki gibi bir ekran görüntüsü ile karşılaşırız. Görüldüğü üzere bellek kullanımında 1 GB civarında bir azalma kendini gösteriyor. Paylaşılan nesnenin yapısı kompleksleştikçe, bu fark daha da açılacaktır.


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



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

Herkese iyi çalışmalar…

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

Bir Yorum Yaz