Anasayfa » 7. Builder Pattern

7. Builder Pattern

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

Bu makalemizde, nesnelerin karmaşık oluşturma süreçlerini adımlara bölerek, nesne oluşturma işlemini daha yönetilebilir ve anlaşılır hale getiren Builder tasarım kalıbını inceleyeceğiz. Bu serinin genelinde olduğu gibi öncelikle Builder 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 Builder 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.


Builder Tasarım Kalıbı Nedir?

Builder tasarım kalıbı, özellikle çok sayıda değer içeren sınıfların farklı varyasyonlarda oluşturulması sürecini adım adım ve opsiyonel şekilde bölerek, daha temiz ve kolay anlaşılır hale getiren bir tasarım kalıbıdır.

Kimi zaman nesnesi üretilecek sınıfların çok sayıda özelliğinin sadece belirli bir kısmını kullanmamız gerekebilir. Böyle zamanlarda sınıfın kurucu metoduna kullanılacak değerleri geçip diğer değerleri boş veya null şekilde geçmek, okunması ve anlaşılması zor bir görüntü oluşturur.

Alternatif olarak sınıfın farklı kullanım şekilleri için farklı Factory’ler hazırlanabilir ancak bu yöntem, özellikle çok sayıda farklı kullanım şekli ile başa çıkmak için çok sayıda Factory metoduna ihtiyaç duyulmasına sebep olabilir. Bu tarz durumlar için Builder tasarım kalıbı biçilmiş kaftandır. Nesne oluşturmaya ihtiyaç duyan katmanlar, sadece ihtiyaç duydukları özellikler ile ilgili metotları kullanarak temiz ve anlaşılabilir bir yapı ortaya çıkarırlar.


Talebimizi Alalım

Yazılım geliştirdiğimiz altyapıda e-postaları gönderen bir yapı mevcut. Bu yapı, gönderim işleminde kullanılmak üzere özel bir nesneye ihtiyaç duyuyor. Gönderim talebinde bulunan katman, bu nesnedeki ihtiyaç duyduğu özellikleri doldurarak gönderim işini altyapıya bırakıyor. Bu nesneyi üretmek için hazırlanması gereken sınıfın sahip olması gereken özellikler aşağıda şekilde listeleniyor.

  • Gönderim yapan adres
  • Mail gönderilecek adresler
  • CC’ye eklenecek adresler
  • BCC’ye eklenecek adresler
  • E-Posta konusu
  • E-Posta içeriği
  • E-Posta ile birlikte gönderilecek ekler
  • Cevap adresi
  • E-Posta gönderim tarihi (İleri vadeli)
  • Gönderim formatı (html, plain-text)


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

Oldukça basit bir taleple karşı karşıyayız. Bu özellikleri içinde barındıran bir sınıf hazırlayıp, sınıf kurucu metodundan bu değerleri almamız yeterli olacaktır. Öncelikle 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"
  ]
}


Ardından e-posta bilgilerini içerecek sınıfı aşağıdaki şekilde projemize ekleyelim.

export class Email {
    constructor(
        public from: string,
        public to: string[],
        public cc: string[],
        public bcc: string[],
        public subject: string,
        public body: string,
        public attachments?: string[],
        public replyTo?: string,
        public sendingDate?: Date | null,
        public format?: string,
    ) {
    }
}


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

import {Email} from "./email";

const email1 = new Email(
    "sender@test.com",
    ["receiver1@test.com"],
    [],
    [],
    "Test Subject",
    "Test Body",
    undefined,
    undefined,
    new Date(2023, 5, 8)
);

const email2 = new Email(
    "sender@test.com",
    ["receiver2@test.com"],
    [],
    ["receiver4@test.com", "receiver5@test.com"],
    "Test Subject",
    "Test Body",
    undefined,
    undefined,
    null,
    "html"
);

const email3 = new Email(
    "sender@test.com",
    ["receiver1@test.com"],
    [],
    [],
    "Test Subject",
    "Test Body",
    undefined,
    "no-reply@test.com",
    null,
    "plain-text"
);


Sınıfımızı hazırlayıp üç farklı örnek kullanım şekliyle nesneler oluşturduk. İsteyen istediği değeri geçerek, farklı şekillerde nesneyi özelleştirebilir.

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 belki Separation of Concerns‘den bahsetmek gerekli çünkü nesne üretme işi Factory sınıflarının görevi olmalıyken, burada doğrudan nesne oluşturuyoruz. Ancak yukarıdaki koddaki asıl sıkıntı, kolay okunabilirlik ve geliştirme sonrası bakım maliyetleri ile alakalı. “app.ts” dosyamızdaki koda baktığımızda, hangi değerin hangi parametreye geçildiğini anlamak pek mümkün değil. Özellikle de boş geçilebilen değerler okunabilirliği oldukça düşürüyor.

Uygulamalarımızdaki sınıflarımız çoğu zaman örneğimizde yer alan “Email” sınıfından daha kompleks bir yapıya sahip olacaktır. Farklı kullanım şekilleriyle aynı nesnenin farklı varyasyonlarını üretmek, daha da karmaşık hale gelecektir. Elbette her bir kullanım şekli için ayrı birer Factory metodu hazırlanabilir ancak değerlerin farklılık kombinasyonları arttıkça bu yöntem de pratik olmaktan çıkacaktır.


Nasıl Bir Çözüm Sunulabilirdi?

Builder tasarım kalıbı, bu tarz özelleştirilme ihtiyacı duyulan nesneler için bire bir uyum sağlar. “Email” sınıfımızı değiştirmeye gerek yok, sonuçta üretmek istediğimiz sınıfımızda sıkıntı yok. Asıl mesele nesneyi ürettiğimiz kısımda olduğuna göre, ihtiyaca yönelik üretim işlemini gerçekleştirecek “EmailBuilder” sınıfımızı aşağıdaki şekilde projemize ekleyelim.

import {Email} from "./email";

export class EmailBuilder {
    private from: string = "";
    private to: string[] = [];
    private cc: string[] = [];
    private bcc: string[] = [];
    private subject: string = "";
    private body: string = "";
    private attachments?: string[];
    private replyTo?: string;
    private sendingDate?: Date | null;
    private format?: string;

    setFrom(from: string): EmailBuilder {
        this.from = from;
        return this;
    }

    setTo(to: string[]): EmailBuilder {
        this.to = to;
        return this;
    }

    setCc(cc: string[]): EmailBuilder {
        this.cc = cc;
        return this;
    }

    setBcc(bcc: string[]): EmailBuilder {
        this.bcc = bcc;
        return this;
    }

    setSubject(subject: string): EmailBuilder {
        this.subject = subject;
        return this;
    }

    setBody(body: string): EmailBuilder {
        this.body = body;
        return this;
    }

    setAttachments(attachments: string[]): EmailBuilder {
        this.attachments = attachments;
        return this;
    }

    setReplyTo(replyTo: string): EmailBuilder {
        this.replyTo = replyTo;
        return this;
    }

    setSendingDate(sendingDate: Date): EmailBuilder {
        this.sendingDate = sendingDate;
        return this;
    }

    setFormat(format: string): EmailBuilder {
        this.format = format;
        return this;
    }

    build(): Email {
        return new Email(this.from, this.to, this.cc, this.bcc, this.subject, this.body, this.attachments, this.replyTo, this.sendingDate, this. Format);
    }
}


Nesne oluşturmaktan sorumlu Builder sınıfımıza her bir özelliğimiz için ayrı birer metot ekledik. Metot içerisinde ilgili değişken değerini ayarladıktan sonra nesnenin referansını saklayan “this” ifadesini geri döndük. Bu yönteme Method Chaining ismi verilir ve birazdan göreceğimiz gibi bize tek bir nesne üzerinden arka arkaya birden fazla metot çağrımı yapmamıza imkan sağlayan bir yapı sunar.

Son olarak “app.ts” dosyamızda bu sınıfı aşağıdaki şekilde kullanalım.

import {EmailBuilder} from "./email-builder";

const email1 = new EmailBuilder()
    .setFrom("sender@test.com")
    .setTo(["receiver1@test.com"])
    .setSubject("Test Subject")
    .setBody("Test Body")
    .setSendingDate(new Date(2023, 5, 8))
    .build();

const email2 = new EmailBuilder()
    .setFrom("sender@test.com")
    .setTo(["receiver2@test.com"])
    .setBcc(["receiver4@test.com", "receiver5@test.com"])
    .setSubject("Test Subject")
    .setBody("Test Body")
    .setFormat("html")
    .build();

const email3 = new EmailBuilder()
    .setFrom("sender@test.com")
    .setTo(["receiver3@test.com"])
    .setSubject("Test Subject")
    .setBody("Test Body")
    .setReplyTo("no-reply@test.com")
    .setFormat("plain-text")
    .build();


Görüldüğü üzere bu kullanım, bir önceki önerimize nazaran çok daha temiz ve okunaklı bir görünüm sunuyor. Kullanmak istemediğimiz parametreleri boş geçme derdinden kurtulduğumuz gibi aynı zamanda hangi değeri hangi özellik için geçtiğimiz de rahat bir şekilde anlaşılabiliyor.

Bu tasarım kalıbına, özellikle Query Builder veya ORM gibi farklı varyasyonların kullanıldığı yapılarda sıklıkla rastlarız. Benzer şekilde oyunlardaki farklı özelliklere sahip karakterlerin oluşturulmasından, robotikte devre elemanlarının yönetimine kadar pek çok farklı kulvarda kullanım örneklerine denk gelemiz mümkündür.

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





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

Herkese iyi çalışmalar…

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

Bir Yorum Yaz