Anasayfa » 15. Bridge Pattern

15. Bridge Pattern

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

Bu makalemizde, birbirleriyle yakın ilişkili sistemleri birbirlerinden bağımsız şekilde geliştirilebilecek iki ayrı sisteme bölme ve bunlar arasındaki ilişkiyi yönetme konusuna odaklanan Bridge tasarım kalıbını inceleyeceğiz. Bu serinin genelinde olduğu gibi öncelikle Bridge 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 Bridge 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.


Bridge Tasarım Kalıbı Nedir?

Klasik tasarım kalıplarındaki bileşenlerin nasıl bir araya geleceği ve birlikte nasıl çalışacağı konusuyla ilgilenen kalıpların (Structural Design Patterns) sonuncusu olan Bridge tasarım kalıbı, birbiri ile ilişkili iki farklı sistemi birbirinden ayırmak ve bu sistemlerin birbirinden bağımsız olarak değiştirilebilmesine olanak sağlamak için kullanılır.

Bu iki sistemden biri genellikle soyutlanan (abstract), diğeri de bu soyutlamayı uygulayan (implementation) bileşenlerden oluşur. Normal şartlarda soyutlama tarafındaki yapılan değişiklikler sırasında uygulama tarafında da değişiklik yapılmasının önüne geçilmesi çok mümkün olmaz. Bridge tasarım kalıbı bu sorun ile ilgilenir.

Diğer bir deyişle bu tasarım kalıbı, üst seviye ve alt seviye nesneler arasında bir köprü oluşturarak kodun daha iyi organize olmasını sağlar. Bu sayede, bir sınıfın değişikliklerinden diğer sınıfların etkilenmemesi mümkün olur. Bir başka deyişle bu kalıp, kodu daha da modüler hale getirir ve sınıflar arasında sıkı bağlılık (tight coupling) oluşmasını önler.

Aslında anlaşılması ve uygulanması oldukça kolay olan Bridge tasarım kalıbı, anlatım sırasında kullanılan jargon sebebiyle çoğu zaman kullanılmaktan imtina edilen bir kalıptır. Bu sebeple konuyu bir örnek üzerinden işlemek daha faydalı olacaktır.


Talebimizi Alalım

Çeşitli döviz kurları için paritelerin dönüldüğü bir servis yazmamız isteniyor. Şimdilik “USDTRY,” “EURTRY” ve “GBPTRY” kurlarının dönülmesi isteniyor ancak ileride bu sayıda artış yaşanabilir. Her para birimi için ilgili merkez bankasından kur bilgileri çekilecek. Servis, ihtiyaca göre “JSON” ve “XML” formatlarında veri sağlaması gerekiyor. Ancak daha sonradan Fixed Length Text ve Comma Separated Text gibi ek formatların da sisteme eklenmesi istenebilir.


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

Üç farklı döviz kuru için üç farklı bankadan veri çekilecek. Daha sonradan yeni bankalara da entegre olmak gerekeceğine göre, her döviz kuru için ayrı birer servis yazmak mantıklı gözüküyor. Böylece yeni eklenen bankalar için eski kodu açıp değiştirmek zorunda kalmayız.

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"
  ]
}


İlk olarak “USDTRY” ile ilgili servisle başlayalım. Single Responsibility prensibine bağlı kalmak adına her işlev için birer metot yazalım. Bankadan veriyi çekecek bir metot, JSON ve XML formatlamak için birer metot ve tüm bu işlemleri yapacak public bir metoda ihtiyacımız olacak. “USDTRYService” isimli sınıfımızı aşağıdaki şekilde projemize ekleyelim.

export class USDTRYService {
    private formatToJSON(data: any[]) {

        // Veri JSON formatına çevriliyor (Parite için kapanış değerini alıyoruz)
        return data.map((x) => {
            return {
                date: x.date,
                value: x.close
            }
        });
    }

    private formatToXML(data: any[]) {

        // Veri XML formatına çevriliyor (Parite için kapanış değerini alıyoruz)
        let parityList = data.map((x) => {
            return `<parity><date>${x.date}</date><value>${x.close}</value></parity>`;
        });

        return `<parities>${parityList.join('')}</parities>`;
    };

    private getData() {

        // Temsili olarak ilgili merkez bankasından döviz kurları çekiliyor
        return [
            {
                date: "2020-06-26",
                open: 25.32,
                high: 26.25,
                low: 25.25,
                close: 26.07
            }, {
                date: "2020-06-27",
                open: 26.07,
                high: 26.21,
                low: 25.53,
                close: 26.03
            }, {
                date: "2020-06-28",
                open: 26.03,
                high: 26.23,
                low: 25.94,
                close: 26.04
            }
        ];
    }

    public getHistoricalData(format: "json" | "xml") {

        const data = this.getData();

        if (format === "json") {
            return this.formatToJSON(data);
        } else if (format === "xml") {
            return this.formatToXML(data);
        }
    }
}


Benzer şekilde “EURTRY” için “EURTRYService” isimli sınıfımızı aşağıdaki şekilde projemize ekleyelim.

export class EURTRYService {
    private formatToJSON(data: any[]) {

        // Veri JSON formatına çevriliyor (Parite için kapanış değerini alıyoruz)
        return data.map((x) => {
            return {
                date: x.date,
                value: x.close
            }
        });
    }

    private formatToXML(data: any[]) {

        // Veri XML formatına çevriliyor (Parite için kapanış değerini alıyoruz)
        let parityList = data.map((x) => {
            return `<parity><date>${x.date}</date><value>${x.close}</value></parity>`;
        });

        return `<parities>${parityList.join('')}</parities>`;
    };

    private getData() {

        // Temsili olarak ilgili merkez bankasından döviz kurları çekiliyor
        return [
            {
                date: "2020-06-26",
                open: 27.52,
                high: 28.53,
                low: 27.33,
                close: 28.25
            }, {
                date: "2020-06-27",
                open: 28.25,
                high: 28.70,
                low: 27.78,
                close: 28.49
            }, {
                date: "2020-06-28",
                open: 28.49,
                high: 28.73,
                low: 28.25,
                close: 28.38
            }
        ];
    }

    public getHistoricalData(format: "json" | "xml") {

        const data = this.getData();

        if (format === "json") {
            return this.formatToJSON(data);
        } else if (format === "xml") {
            return this.formatToXML(data);
        }
    }
}


Aynı şekilde “GBPTRY” için “GBPTRYService” isimli sınıfımızı aşağıdaki şekilde projemize ekleyelim.

export class GBPTRYService {
    private formatToJSON(data: any[]) {

        // Veri JSON formatına çevriliyor (Parite için kapanış değerini alıyoruz)
        return data.map((x) => {
            return {
                date: x.date,
                value: x.close
            }
        });
    }

    private formatToXML(data: any[]) {

        // Veri XML formatına çevriliyor (Parite için kapanış değerini alıyoruz)
        let parityList = data.map((x) => {
            return `<parity><date>${x.date}</date><value>${x.close}</value></parity>`;
        });

        return `<parities>${parityList.join('')}</parities>`;
    };

    private getData() {

        // Temsili olarak ilgili merkez bankasından döviz kurları çekiliyor
        return [
            {
                date: "2020-06-26",
                open: 32.13,
                high: 33.34,
                low: 31.88,
                close: 32.94
            }, {
                date: "2020-06-27",
                open: 32.91,
                high: 33.40,
                low: 32.40,
                close: 33.14
            }, {
                date: "2020-06-28",
                open: 33.14,
                high: 33.42,
                low: 32.73,
                close: 32.87
            }
        ];
    }

    public getHistoricalData(format: "json" | "xml") {

        const data = this.getData();

        if (format === "json") {
            return this.formatToJSON(data);
        } else if (format === "xml") {
            return this.formatToXML(data);
        }
    }
}


Üç servisimiz de hazır olduğuna göre artık uygulamamızı ayağa kaldıracak ve tüm bu servisleri kullanacak “app.ts” dosyamızı aşağıdaki içerik ile oluşturabiliriz.

import {USDTRYService} from "./usdtry-service";
import {EURTRYService} from "./eurtry-service";
import {GBPTRYService} from "./gbptry-service";

const usdtryService = new USDTRYService();
const eurtryService = new EURTRYService();
const gbptryService = new GBPTRYService();

const usdtryData = usdtryService.getHistoricalData("json");

console.log(usdtryData);

console.log();

const eurtryData = eurtryService.getHistoricalData("xml");

console.log(eurtryData);

console.log();

const gbptryData = gbptryService.getHistoricalData("json");

console.log(gbptryData);


Kodumuz oldukça basit, daha fazla açıklama yapmıyorum. 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.


Peki Bu Çözümün Nesi Yanlış?

Öncelikle ilk göze çarpan sıkıntı, “formatToJSON” ve “formatToXML” metotlarının bire bir aynı şekilde üç ayrı sınıfta da yer alıyor olması. Burada DRY (Don’t Repeat Yourself) prensibini çiğnemiş olduk. Ancak asıl sıkıntı daha derinlerde yatıyor.

Para birimi bazında ayrı bir servis yazmak güzel bir başlangıç. Böylece sonradan eklenen yeni para birimleri için eski sınıfları açıp değiştirmek zorunda kalmayacağız. Ancak formatlara da yeni bir format eklenmesi söz konusu. Bu durumda eskiden yazılmış tüm sınıfları açıp, yeni formatları eklemek zorunda kalacağız.

Tam tersi sınıf oluşturmayı formatlar bazında da yapabilirdik. Ancak bu durumda da yeni bir para birimi eklenmesi gerektiğinde eskiden yazılmış tüm sınıfları açıp, yeni para birimlerini eklemek zorunda kalacaktık.

Bu mantıkla gidersek, Open/Closed prensibine bağlı kalmak mümkün gözükmüyor. Çünkü birbiriyle ilişkili ancak birbirinden bağımsız genişleme ihtimali olan iki ayrı sistemimiz var.

Nasıl Bir Çözüm Sunulabilirdi?

Tanımda da belirttiğimiz gibi, bu tarz birbiriyle ilişkili ancak birbirinden bağımsız genişleme ihtimali olan iki ayrı sistem içeren durumlar için Bridge Pattern güzel bir öneri olabilir. Ancak çözüm için baştan başlamamız gerekecek çünkü yazılacak sınıfların yapıları biraz farklı.

Projeyi iki kısma ayıracağız. İlk kısım para birimlerine ait verileri getirmekten sorumlu olacak. İkinci kısım ise kendisine geçilen verileri formatlamaktan sorumlu olacak. Bu iki kısım arasında köprü vazifesi görecek ve birlikte çalışmalarını sağlayacak servisimizi ise en son yazacağız.

Dikkat: Örneği karıştırmamak ve odak dağıtmamak adına metotlardan dönülen veriler için model yapılar eklemeyip doğrudan "any" ile dönüş yapacağız ancak gerçek yaşamda "any" ifadesini bu kadar özgürce kullanmaktan kaçınmak gerekir.

Öncelikle tüm para birimlerinin uygulayacağı “ICurrency” isimli Interface’imizi projemize ekleyelim.

export interface ICurrency {
    getData(): any;
}


Ardından üç para birimi için üç ayrı sınıfı aşağıdaki şekilde projeye ekleyelim. Bu sınıfların tek görevi, ilgili para birimine ait verileri getirmek olacak.

import {ICurrency} from "./icurrency";

export class USDTRY implements ICurrency {
    public getData() {

        // Temsili olarak ilgili merkez bankasından döviz kurları çekiliyor
        return [
            {
                date: "2020-06-26",
                open: 25.32,
                high: 26.25,
                low: 25.25,
                close: 26.07
            }, {
                date: "2020-06-27",
                open: 26.07,
                high: 26.21,
                low: 25.53,
                close: 26.03
            }, {
                date: "2020-06-28",
                open: 26.03,
                high: 26.23,
                low: 25.94,
                close: 26.04
            }
        ];
    }
}
import {ICurrency} from "./icurrency";

export class EURTRY implements ICurrency {
    public getData() {

        // Temsili olarak ilgili merkez bankasından döviz kurları çekiliyor
        return [
            {
                date: "2020-06-26",
                open: 27.52,
                high: 28.53,
                low: 27.33,
                close: 28.25
            }, {
                date: "2020-06-27",
                open: 28.25,
                high: 28.70,
                low: 27.78,
                close: 28.49
            }, {
                date: "2020-06-28",
                open: 28.49,
                high: 28.73,
                low: 28.25,
                close: 28.38
            }
        ];
    }
}
import {ICurrency} from "./icurrency";

export class GBPTRY implements ICurrency {
    public getData() {

        // Temsili olarak ilgili merkez bankasından döviz kurları çekiliyor
        return [
            {
                date: "2020-06-26",
                open: 32.13,
                high: 33.34,
                low: 31.88,
                close: 32.94
            }, {
                date: "2020-06-27",
                open: 32.91,
                high: 33.40,
                low: 32.40,
                close: 33.14
            }, {
                date: "2020-06-28",
                open: 33.14,
                high: 33.42,
                low: 32.73,
                close: 32.87
            }
        ];
    }
}


Şimdi de tüm formatlayıcıların uygulayacağı “IFormatter” isimli Interface’imizi projemize ekleyelim.

export interface IFormatter {
    formatData(data: any[]): any;
}


Ardından iki farklı format için iki ayrı formatlayıcıyı aşağıdaki şekilde projeye ekleyelim. Bu sınıfların tek görevi, kendilerine verilen veriyi ilgili formata göre formatlayarak geri dönmek olacak.

import {IFormatter} from "./iformatter";

export class JSONFormatter implements IFormatter {
    public formatData(data: any[]) {

        // Veri JSON formatına çevriliyor (Parite için kapanış değerini alıyoruz)
        return data.map((x) => {
            return {
                date: x.date,
                value: x.close
            }
        });
    }
}
import {IFormatter} from "./iformatter";

export class XMLFormatter implements IFormatter {
    public formatData(data: any[]) {

        // Veri XML formatına çevriliyor (Parite için kapanış değerini alıyoruz)
        let parityList = data.map((x) => {
            return `<parity><date>${x.date}</date><value>${x.close}</value></parity>`;
        });

        return `<parities>${parityList.join('')}</parities>`;
    }
}


Son olarak bu iki birbirinden bağımsız sistem arasında köprü olacak ve birlikte çalışmalarını sağlayacak “CurrencyService” isimli servisimizi aşağıdaki şekilde projemize ekleyelim.

import {ICurrency} from "./icurrency";
import {IFormatter} from "./iformatter";

export class CurrencyService {
    constructor(
        private currencyImplementor: ICurrency,
        private formatImplementor: IFormatter
    ) {
    }

    public getHistoricalData() {
        const data = this.currencyImplementor.getData();
        return this.formatImplementor.formatData(data);
    }
}


Sınıfın yapısı oldukça basit. Kurucu metottan Dependency Injection ile dışarıdan aldığı para birimi sınıfının “getData” metodunu çağırıyor ve buradan gelen veriyi, yine dışarıdan aldığı formatlayıcının “formatData” metoduna geçerek, aldığı sonucu geri dönüyor.

Biz bu servisi ekleyene kadar iki sistem birbirinden komple bağımsızdı. Halen de bir tarafa yapılan eklemeden diğer tarafın haberi olmayacaktır. “CurrencyService” isimli servisimiz iki sistem arasında köprü vazifesi görerek birlikte çalışmalarını sağlayacak.

Köprümüz de hazır olduğuna göre artık “app.ts” dosyamızı aşağıdaki şekilde değiştirebiliriz.

import {USDTRY} from "./usdtry";
import {EURTRY} from "./eurtry";
import {GBPTRY} from "./gbptry";
import {JSONFormatter} from "./json-formatter";
import {XMLFormatter} from "./xml-formatter";
import {CurrencyService} from "./currency-service";

const usdtryService = new CurrencyService(new USDTRY(), new JSONFormatter());
const eurtryService = new CurrencyService(new EURTRY(), new XMLFormatter());
const gbptryService = new CurrencyService(new GBPTRY(), new JSONFormatter());

const usdtryData = usdtryService.getHistoricalData();

console.log(usdtryData);

console.log();

const eurtryData = eurtryService.getHistoricalData();

console.log(eurtryData);

console.log();

const gbptryData = gbptryService.getHistoricalData();

console.log(gbptryData);


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 aynı çıktıyı elde ettik ancak artık hem yeni bir para birimi hem de yeni bir format geldiği taktirde, eskiden yazılmış sınıfları açıp değiştirmek zorunda kalmayacağız. Bridge tasarım kalıbı sayesinde çift taraflı değişim ihtiyacına rağmen Open/Closed prensibine bağlı kalabildik.


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




Böylece hem Bridge tasarım kalıbı konusunun, hem de klasik tasarım kalıplarındaki bileşenlerin nasıl bir araya geleceği ve birlikte nasıl çalışacağı konusuyla ilgilenen kalıpların (Structural Design Patterns) sonuna gelmiş olduk. Bir sonraki makalemizde, sistemdeki bileşenler arasındaki iletişim ve işbirliği konusuyla ilgilenen kalıplardan (Behavioral Design Patterns) ilki olan Chain of Responsibility tasarım kalıbını işleyeceğiz.

Herkese iyi çalışmalar…

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

Bir Yorum Yaz