C++ İle Mediator (Arabulucu) Tasarım Kalıbı

mozgan
4 min readJul 22, 2022

Bu makalede C++ ile mediator (arabulucu) tasarım kalıbını inceleyeceğiz. C++ ile yazılan kodların daha iyi anlaşılabilmesi için öncelikle C++ İle Soyut Thread Sınıfı Oluşturmak isimli makalemi incelemenizi tavsiye ederim.

Source: https://refactoring.guru/design-patterns/mediator

Amaç

Mediator (arabulucu) tasarım kalıbının asıl amacı nesneler arasındaki kaotik bağlantıları en aza indirmeyi hedeflemesidir. Yani nesneler, birbirleri ile doğrudan değil de mediator adı verilen bir kanal üzerinden iletişimi gerçekleştirirler.

Sorun

Yukarıda da bahsedildiği gibi birbirleri ile iletişimde olan nesnelerin doğrudan haberleşmeleri, birbirlerine olan bağımlılığını arttırır. İlk etapta bu iletişim zor gibi görünmemekle beraber tasarlanan sistem geliştikçe nesneler arası iletişim bir sorun haline gelmektedir.

Daha iyi anlaşılabilemesi için programcı gözünden bir örnek vereyim:

Farz edelim ki elimizde, kendi işlerinden sorumlu beş adet thread (adları A, B, C, D ve E) içeren bir sistem mevcut ve her bir thread bir diğeri ile aşağıdaki gibi iletişim halinde olsunlar:

A -> {B, C, E}
B -> {D, E}
C -> {B, E}
D -> {A, C}
E -> {}

Yani A nesnesi B, C ve E nesnelerine, B nesnesi D ve E nesnelerine v.b. mesaj göndermektedirler. E nesnesi ise hiç bir nesneye mesaj göndermemektedir.

Şimdi bu haberleşme için aşağıdaki gibi bir yapı kurabiliriz:

  1. Her bir thread içerisinde public olarak bir fonksiyon tanımlayabiliriz ve diğerleri bu fonksiyon yardımı ile mesajlarını thread içerisindeki bir kuyruğa kaydedebilirler. Böylece her bir thread kendi döngüsünde bu kuyruğu okuyup alınan mesajları işleme koyar. Fakat program akışı devam ederken hangi thread nesnesinin hangisine mesaj gönderme işlemini elle kodlamak (hard-code) gittikçe zor olacaktır.
  2. Ortak bir veri havuzu oluşturulabilir ve her bir thread için bir kuyruk ayrabiliriz. Böylece her bir thread, kendisi için ayrılan bu kuyruğu okuma imkanına kavuşur. Ancak bu da başka sorunlar getirmektedir. Mesela, thread içerisinde private olarak tanımlanması gereken kuyruğun başka bir nesneye aktarılması vb.

(Bu kurgular çoğaltılabilir… Aslında mediator tasarım kalıbı, bu iki maddeye farklı bir açıdan bakıp sentezleyerek oluşturulur :-))

Diyelim ki bir şekilde yukarıdaki maddelerden birini kullanarak nesneler arası iletişimi gerçekleştirdik. Tabii her geçen gün sistemimizi geliştirmemiz gerekecek ve kullanılan thread sayısı da artacaktır. Mesela yukarıdaki sisteme üç thread (adları F, G ve H) daha eklediğimizde ve aradaki iletişimi geliştirdiğimizde şu şekilde bir yapı çıkabilir:

A -> {B, C, E, F, G}
B -> {D, E, G}
C -> {B, E, F, G}
D -> {A, C, F}
E -> {F, G}
F -> {B}
G -> {D}

Böylece sistemimiz içerisindeki iletişim çıkılmaz bir hal almaya yani kaosa doğru gitmeye başlayacaktır.

Çözüm

Yukarıdaki senaryoyu kısmen çözmek, en azından nesneler arasındaki bu iletişim sorununu ve bağımlıkları en aza indirmek için şu şekilde bir yol izlenir:

  1. İçerisinde Thread nesnesini içeren soyut bir sınıf hazırlıyoruz (iWorker).
  2. Bu soyut sınıfı içerisinde mesaj gönderme (send()) ve alma (receive()) işlemleri için fonksiyonlar tanımlanır.
  3. Tabii ki iWorker sınıfımız iletişim kanalı olan Mediator nesnesinin referansını da içerisinde barındırmalıdır.
iWorker Soyut Sınıfı

4. Şimdi de her bir thread nesnesinin kullanacağı Mediator sınıfı oluşturulur. Bu sınıf bir adet std::unordered_map barındırmaktadır. Bunun amacı da hangi thread nesnesinin hangisine mesaj göndereceğinin Mediator tarafından bilinmesinin gereksinimidir. Tabii broadcasting yapılırsa her bir thread, Mediator üzerinde bulunan diğer tüm nesnelere mesaj gönderebilir. Aksi takdirde her bir thread kendisinden mesaj bekleyen nesnelere mesaj gönderir.

Mediator Sınıfı

Sonuç olarak, her bir thread nesnesi sistem içerisinde izole edilmiş ve nesneler arası iletişim de Mediator adı verilen kanal üzerinden tek bir fonksiyon yardımıyla gerçekleşmiş olur.

Test

Öncelikle iWorker soyut sınıfından Worker isminde bir sınıf türetmemiz gerekiyor. Bunu gerçekleştirirken iWorker ve Thread sınıfları içerisinde bulunan ve soyut olarak tanımlanan run() ve receive() fonksiyonlarını tanımlıyoruz. Aşağıda bir örneği verilmiştir:

İsteğe bağlı olarak Worker içerisinde bir adet de kuyruk oluşturup receive() ile gelen mesajları kuyruğa da kaydedebiliriz. Worker içerisinde yazacağımız bir döngü ile her defasında bu kuyruktan bir mesaj alıp işlenebilir. NOT: Bu kuyruğun bir adet okuyanı olacağı gibi birden çok yazanı olacaktır. Olası sorunlardan kaçınmak için std::mutex kullanılması gerekmektedir!

Artıları/Eksileri

+) Nesneler arası bağımlılığı azaltır. Hatta bu işlemi bir interface yardımı ile yaptığımız için kullanım alanı da artmaktadır. Böylece interface kullanarak bir çok çeşitli sınıf oluşturabilir ve bu sınıfları rahatça geliştirebiliriz.

+) Open/Closed ve Single Responsibility prensiplerine uygundur.

+) İçerisinde bir çok thread nesnesi bulunduran bir çok sistem grubu oluşturulabileceği gibi bir thread birden çok Mediator nesnesi kullanarak farklı sistem gruplarına da dahil olabilir. Bunun gibi çok sofistike ve karmaşık yapılar basit yapılar haline getirilebilir. Böylece distributed (dağınık/dağıtık) sistem yapısına da uygundur.

-) Mediator tasarım kalıbı kullanılarak oluşturulan bir sistem çok büyüdüğü vakit “God Object” oluşma ihtimali vardır.

Kendi deneyimim açısından söyleyebilirim ki, C++’da bulunan template kullanımı zorlaşıyor. Böylece sistem içerisinde dolaşacak olan her mesaj tipi için bir adet send() ve receive() yazma ihtiyacı artıyor.

Bitirirken

Tüm kaynak koduna ve testlere ulaşmak için bu linki kullanabilirsiniz.

Böylece bir makalenin daha sonuna gelmiş olmaktayız. Bir sonraki makale büyük ihtimalle Observer (gözlemci) tasarım kalıbı üzerine olacaktır.

Tekrar görüşmek dileğiyle!

Kaynakça

  1. Refactoring.Guru
  2. Dmitri Nesteruk. Design Patterns in Modern C++. Apress Berkeley, 1. Edition, 2018.

--

--