Anasayfa » 13. Composite Pattern

13. Composite Pattern

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

Bu makalemizde, bir grup nesnenin tek bir nesne gibi yönetilmesini sağlayan Composite tasarım kalıbını inceleyeceğiz. Bu serinin genelinde olduğu gibi öncelikle Composite 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 Composite 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.


Composite Tasarım Kalıbı Nedir?

Composite pattern, genellikle hiyerarşik bir ağaç yapısı oluşturulması gereken durumlarda işlemleri basitleştirmek amacıyla kullanılır. Bu sayede, bireysel nesneler ve nesne grupları arasında fark gözetmeksizin tek bir arayüz üzerinden işlem yapmayı mümkün kılar.

Örneğin, bir işletmenin organizasyonel yapısı, dosya ve dizinlerin ağaç yapısı, HTML DOM yapısı veya UI hiyerarşik yapısı, bu tasarım kalıbı için güzel birer örnek olabilir.

Composite pattern, genellikle aşağıdaki iki tip bileşeni içerir.

  • Leaf (Yaprak): Bu bileşen, hiyerarşinin en altındaki nesneleri temsil eder. Yani başka hiçbir nesneye sahip olmayan veya referans göstermeyen nesnelerdir.
  • Composite: Bu nesneler, başka nesnelere (hem leaf hem de composite) sahip olabilen ve onlara referans gösterebilen nesnelerdir.

Bu iki bileşen tipi genellikle ortak bir arayüz veya abstract sınıfı (Component) uygularlar. Bu sayede istemci kodu, bir nesnenin composite veya leaf olduğunu bilmeden onunla çalışabilir. Bu da, istemci kodunun karmaşıklığını azaltır ve genişletilebilirliğini artırır.


Talebimizi Alalım

Kurumsal kaynak planlama yazılımında hiyerarşik departmanların bulunduğu bir yapıya ihtiyaç duyuluyor. Bu yapıda her departman, organizasyon şemasında olduğu gibi başka bir departmanın altında bulunabiliyor. Her departmanın da kendi çalışanları bulunuyor ve bu çalışanların maaşları yazılımda tutuluyor. Böyle bir yapıda seçilen herhangi bir departmanda ve altındaki tüm alt departmanlarda çalışan kişilerin toplam maaşının hesaplanması isteniyor.


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

Sanki recursive çağrım kokusu alıyor gibiyiz. Resmi netleştirmek adına örnek üzerinden konuşalım. Üç farklı departman olduğunu ve ikinci departmanın birincinin altında olduğunu, üçüncü departmanın da ikincinin altında olduğunu varsayalım. Benzer şekilde üç farklı çalışan olduğunu ve her çalışanın sırayla bu departmanlardan birinde çalıştığını kabul ederek kodumuzu yazalım.

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 departmana ait bilgileri tutacak “Department” sınıfını aşağıdaki şekilde projemize ekleyelim. Her departmanın kendine has bir “id” değeri ve eğer varsa üst departmanın “id” değerinin tutulduğu bir “parentId” değerimiz var.

export class Department {
    constructor(public id: number, public parentId?: number) {}
}


Ardından her bir çalışana ait bilgileri tutacak “Employee” sınıfını aşağıdaki şekilde projemize ekleyelim. Her çalışanın bağlı olduğu departmanın “id” değerinin tutulduğu bir “parentId” değeri ve bir de maaş bilgisinin saklandığı “salary” değerimiz var. Bunların yanında private maaş bilgisini dönen bir “getSalary” metodumuz da mevcut (Belki buraya daha sonradan bir yetki mekanizması eklenebilir).

export class Employee {
    constructor(public parentId: number, private salary: number) {}

    getSalary(): number {
        return this.salary;
    }
}


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

import {Employee} from "./employee";
import {Department} from "./department";

const departmentList: Department[] = [];
const employeeList: Employee[] = [];

// Üst ve alt departman tanımları
departmentList.push(new Department(1));
departmentList.push(new Department(2, 1));
departmentList.push(new Department(3, 2));

// Departmanlara bağlı çalışanlar
employeeList.push(new Employee(1, 1000));
employeeList.push(new Employee(2, 2000));
employeeList.push(new Employee(3, 3000));


// Bir departmanın ve alt departmanlarının tüm çalışanlarının maaşını toplayan fonksiyon
function getDepartmentSalary(departmentId: number): number {
    let totalSalary = 0;

    // İlgili departmandaki çalışanların maaşını toplar
    for (let employee of employeeList) {
        if (employee.parentId === departmentId) {
            totalSalary += employee.getSalary();
        }
    }

    // İlgili departmanın alt departmanlarını bulur ve recursive çağrımla onların maaşını da toplar
    for (let department of departmentList) {
        if (department.parentId === departmentId) {
            totalSalary += getDepartmentSalary(department.id);
        }
    }

    return totalSalary;
}


// Tüm departmanların bileşik maaşlarını yazdıralım
console.log("Departman 1: ", getDepartmentSalary(1)); // 6000
console.log("Departman 2: ", getDepartmentSalary(2)); // 5000
console.log("Departman 3: ", getDepartmentSalary(3)); // 3000


Burada öncelikle “id” – “parentId” eşleştirmesi üzerinden departmanları ve çalışanları birbirlerine bağlıyoruz. Ardından verilen departmanın ve alt departmanlarının tüm çalışanlarının maaşını toplayan “getDepartmentSalary” isimli bir metodumuzu yazıyoruz. Üç farklı seviye için bu metodu çağırarak hesaplama yaptırdık. Don’t Repeat Yourself prensibine de uymuş olduk. Amaç da hasıl olduğuna göre, proje bitmiştir.

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 üç farklı departman seviyesi için maaşların toplamları hesaplandı.


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

Bir düşünelim. Asıl işi yapan kodu istemci tarafına bırakarak, Single Responsibility, KISS, Separation of Concerns, Law of Demeter ve Encapsulation prensiplerini bir seferde çiğnemiş olduk. Open/Closed hakkında çok konuşamıyoruz çünkü kapatacak pek de bir şey kalmadı.

Aslında yukarıdaki “id” – “parentId” eşleştirmesi, yazılıma yeni başlayanlar ve orta seviyeli meslektaşlarımızda sık başvurulan bir anti-pattern olarak karşımıza çıkıyor. Bunun temel sebebi, bu seviyelerde DB First düşüncesinin daha baskın olması ve bu düşüncenin kodda bulunan veri yapılarının da veritabanı tablolarına benzer şekilde tasarlama eğilimine sebep olmasından geçiyor. Öte yandan NoSQL veritabanlarına ve aggregate yapısına alışık olan yazılım geliştiricilerde bu yaklaşımın daha ender görüldüğüne şahit oluyoruz.

Unutmayalım ki uzun ve kısa süreli veri saklama ortamları arasında yapısal farklılıklar olmasaydı, ORM gibi bir kavrama da ihtiyacımız kalmazdı.


Nasıl Bir Çözüm Sunulabilirdi?

Bu tarz hiyerarşik nesneler ile çalışmamız gerektiği durumlarda Composite tasarım kalıbı güzel bir öneri olabilir. Burada “Employee” sınıfı “Leaf” türünde, “Department” sınıfı da “Composite” türünde birer bileşendir. Bu iki bileşenin ortak bir paydada buluşabilmesi için aşağıdaki gibi bir “Component” sınıfını projemize ekleyerek başlayalım.

export abstract class Component {
    abstract getSalary(): number;
}


Bu abstract sınıfı baz alacak tüm sınıfların “getSalary” metodunu sağlaması gerekecek. İlk olarak “Employee” sınıfı ile başlayalım ve bu sınıfı aşağıdaki şekilde değiştirelim.

import {Component} from "./component";

export class Employee extends Component {
    constructor(private salary: number) {
        super();
    }

    getSalary(): number {
        return this.salary;
    }
}


Bu sınıfta yapılan çok ciddi bir değişiklik yok. Sadece kurucu metottan geçilen “parentId” değerini kaldırdık. Çalışanı departmana bağlama işi farklı bir yöntem ile gerçekleştireceğiz.

Sırada “Department” sınıfı var. Bu sınıfı da aşağıdaki şekilde değiştirelim.

import {Component} from "./component";

export class Department extends Component {
    private children: Component[] = [];

    add(component: Component): void {
        this.children.push(component);
    }

    getSalary(): number {
        return this.children.reduce((sum, child) => sum + child.getSalary(), 0);
    }
}


Burada bir miktar değişiklik göze çarpıyor. Öncelikle sınıf seviyesinde “children” isminde ve “Component” tipinde bir dizimiz var. Bu diziye “add” metodu ile yeni bileşenler ekleyebiliyoruz.

Ancak burada dikkat edilmesi gereken husus, hem “Employee“, hem de “Department” sınıfları “Component” sınıfını baz almıştı. Dolayısıyla “add” metodu vasıtasıyla bir departman altına hem bir çalışan, hem de başka bir departman eklenebilir.

getSalary” metodu ise, kendi altındaki tüm bileşenlerin “getSalary” metodunu çağırarak toplamı geri döner. Bu durumda eğer bileşen bir çalışansa, doğrudan bu çalışanın maaşı hesaba katılırken, eğer bileşen bir departman ise, bu departmanın altındaki bileşenlerin “getSalary” metodunun çağrılması sağlanır. Dolayısıyla burada recursive metot çağrım karmaşasına gerek kalmaksızın recursive süreç işletilmiş olur.

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

import {Employee} from "./employee";
import {Department} from "./department";

// Departman tanımları
const department1 = new Department();
const department2 = new Department();
const department3 = new Department();

// Çalışan tanımları
const employee1 = new Employee(1000);
const employee2 = new Employee(2000);
const employee3 = new Employee(3000);

// Çalışanları departmanlara bağlıyoruz
department1.add(employee1);
department2.add(employee2);
department3.add(employee3);

// Departmanları da departmanlara bağlıyoruz
department1.add(department2);
department2.add(department3);


// Tüm departmanların bileşik maaşlarını yazdıralım
console.log("Departman 1: ", department1.getSalary()); // 6000
console.log("Departman 2: ", department2.getSalary()); // 5000
console.log("Departman 3: ", department3.getSalary()); // 3000


Görüldüğü üzere çok daha sade ve iş mantığından soyutlanmış bir istemci kodu ile karşı karşıyayız. Composite Pattern sayesinde her bir nesnenin, sadece kendi altındaki nesneler ile ilgilenmesini sağlayarak recursive işlem karmaşasından kurtulmuş olduk.

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


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




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

Herkese iyi çalışmalar…

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

Bir Yorum Yaz