İçeriğe atla

Sunum Servisi için Hexagonal Mimari: BFF Karşılaştırması

Bir UI parçasının arkasındaki ince sunum servisi yapışkan koda dönüşür. Port-ve-adaptör, çekirdeği somut hiçbir şeye bağımlı bırakmayarak bunu sürdürülebilir tutar.

Ayhan Sipahi Ayhan Sipahi

Sunum servisi, tek bir UI parçasının arkasında duran ince arka uçtur: parayı ve tarihleri biçimler, metni ve i18n’i çözer, oturum durumunu şekillendirir ve UI adına tek bir alt domain servisini proxy’ler. Ekipler bunu yapışkan kod için bir çöplük olarak görür ve bu kod zamanla çamur topuna dönüşür. Para biçimleme HTTP işleyicilerinin içine karışır, alt servis istemcisinin tuhaflıkları iş kurallarına sızar. Bu servisi sürdürülebilir tutan disiplin port-ve-adaptör (hexagonal mimari) yaklaşımıdır ve tek bir kurala iner: domain çekirdeğini somut hiçbir şeye bağımlı olmamaya zorla.

Yapışkan Koddan Çamur Topuna#

“İnce proxy” olarak başlayan bir servis, hiçbir sınır ayakta kalmayana kadar sessizce sorumluluk biriktirir:

  • Biçimleme, i18n, oturum şekillendirme, kimlik doğrulama token işleme ve alt istemci çözümleri aralarında hiçbir ayrım olmadan yığılır.
  • Para ve tarih biçimleme çağrıları HTTP denetleyicilerine gömülür, dolayısıyla biçimleme mantığını test etmenin tek yolu bir HTTP sunucusu ayağa kaldırmaktır.
  • Alt servisin aktarım formatı (alan adları, hata şekilleri, sayfalama tuhaflıkları) iş kararlarına sızar. O istemciyi değiştirmek veya sürümlemek her şeye dokunmak demektir.
  • Bir para veya i18n kütüphanesi onlarca dosyadan çağrılır, dolayısıyla onu değiştirmek başlı başına bir projeye dönüşür.
  • Gözlemlenebilirlik kenarlarda bir kez bağlanmak yerine her işleyiciye tutarsızca eklenir.

Bunların her biri yanlış yöne işaret eden bir bağımlılıktır: çekirdek mantık somut bir kütüphaneye, somut bir protokole veya somut bir aktarım formatına bağımlıdır.

Bağımlılık Kuralı#

Tüm desen, bağımlılık yönü üzerindeki tek bir kısıttır. Cockburn’ün hexagonal mimari için 2005’teki özgün amacı, “bir uygulamanın kullanıcılar, programlar, otomatik testler veya toplu betikler tarafından eşit şekilde yönetilebilmesini ve nihai çalışma zamanı cihazlarından ve veritabanlarından izole olarak geliştirilip test edilebilmesini” sağlamaktı. Martin aynı kısıtı bağımlılık kuralı olarak ifade eder: “kaynak kodu bağımlılıkları yalnızca içeri işaret edebilir. İç bir halkadaki hiçbir şey, dış bir halkadaki herhangi bir şey hakkında en ufak bilgiye sahip olamaz” ve özellikle “dış bir halkada kullanılan veri formatları iç bir halkada kullanılmamalıdır.”

Pratikte bu, domain çekirdeğinin hiçbir HTTP kütüphanesi, para kütüphanesi veya i18n kataloğu içe aktarmaması anlamına gelir. Çekirdek ihtiyaç duyduğu arayüzleri (portları) tanımlar; somut uygulamalar (adaptörler) kenarlarda yaşar ve çekirdeğe bağımlıdır, asla tersi değil. Domain çekirdeği böylece ağ, saat veya yerel ayar olmadan çalıştırılabilir.

Hexagonal, onion ve clean mimari aynı ailenin üyeleridir. Onion (Palermo, 2008) ve clean (Martin, 2012), aynı bağımlılık tersine çevirme fikrini farklı çizimlerle inşa eder; böylece üç terim de tek bir ilkenin farklı adı olur.

Dosya Düzeni#

Bir sunum servisi hexagona sorunsuz oturur. Genelleştirilmiş haliyle düzen şöyle görünür:

  • domain/, sunum kurallarını tutan izole çekirdektir. HTTP, para kütüphaneleri veya i18n hakkında hiçbir bilgisi yoktur ve ihtiyaç duyduğu port arayüzlerinin sahibidir.
  • gateways/, alt domain servisine giden dışa dönük istemcileri tutar. Bu, yönlendirilen (driven) taraftır.
  • adapters/, değiştirilebilir kenarları tutar: HTTP, para biçimleme, tarih, çeviri ve kimlik doğrulama token kimlik bilgileri, her biri bir portu uygular.
  • endpoints/ (denetleyiciler), gelen HTTP kenarıdır. Bu, yönlendiren (driving) taraftır.
  • Küçük bir gözlemlenebilirlik katmanı (metrikler, izleme, yapılandırılmış günlükleme) kenarlarda bağlanır.

AWS Prescriptive Guidance aynı yapıyı tarafsız biçimde çerçeveler: “uygulama, dış bileşenlerle port adı verilen arayüzler üzerinden iletişim kurar ve teknik alışverişleri çevirmek için adaptörler kullanır” ve domain-driven design uyumunu, “her uygulama bileşeni DDD’de bir alt domaini temsil eder” diyerek belirtir.

HTTP Denetleyici (yönlendiren adaptör)

Gelen Port

Domain Çekirdeği (sunum kuralları)

Gateway Portu

Para Portu

i18n Portu

Alt Servis Gateway

Para Biçimleyici

Çeviri Adaptörü

Gözlemlenebilirlik konusunda OpenTelemetry tarafsız çerçeveyi sağlar. Üç sinyali vardır: izler (traces), metrikler ve günlükler. Enstrümantasyon ise bunları yayma eylemidir. Bir sunum servisi için adaptörleri enstrümante edin: HTTP kenarı ve alt servis gateway’i, isteklerin bir sınırı geçtiği ve gecikmeyle hataların gözlemlenebildiği yerlerdir; domain çekirdeği ise sinyalsiz kalır, böylece testleri saf, mantığı taşınabilir kalır.

Adaptörler: Değiştirilebilir Kenarlar#

Çekirdek, hiçbir şeye bağımlı olmayan bir port tanımlar:

// domain/ports.ts: çekirdeğe ait, somut hiçbir şeye bağımlı değil
export interface MoneyFormatter {
  format(amountMinor: number, currency: string, locale: string): string;
}

Bir adaptör onu kenarda uygular:

// adapters/money/intl-money-formatter.ts: değiştirilebilir bir uygulama
import type { MoneyFormatter } from "../../domain/ports";

export class IntlMoneyFormatter implements MoneyFormatter {
  format(amountMinor: number, currency: string, locale: string): string {
    return new Intl.NumberFormat(locale, { style: "currency", currency })
      .format(amountMinor / 100);
  }
}

Para kütüphanesini, alt istemciyi veya i18n arka ucunu değiştirmek artık tek bir adaptör dosyasına dokunur. Domain çekirdeğinin testleri hiç değişmez, çünkü çekirdek baştan beri yalnızca MoneyFormatter arayüzünü görür. Bağımlılık enjeksiyonu ile bağımlılık kuralı arasındaki ayrıma dikkat edin. Enjeksiyon, başlangıçta çekirdeğe bir adaptör veren bağlama mekanizmasıdır; bağımlılık kuralı ise çekirdeğin adaptörü hiç içe aktarmadan derlenmesini sağlayan yönsel kısıttır ve bu kural olmadan enjeksiyon sizi yine de bağımlı bırakır.

İzole Çekirdeği Test Etmek#

Bu, Cockburn’ün özgün yazısında en çok anılan faydadır: izolasyon içinde geliştir ve test et. Çekirdek yalnızca portlarına bağımlı olduğu için, onu bellek içi sahtelerle test edersiniz; HTTP sunucusu yok, ağ yok, sistem saati yok ve gerçek çeviri kataloğu yok.

// domain/__tests__/present-checkout.test.ts
import { presentCheckout } from "../present-checkout";
import type { MoneyFormatter, CheckoutGateway } from "../ports";

const fakeFormatter: MoneyFormatter = {
  format: (m, c, l) => `${c} ${(m / 100).toFixed(2)} [${l}]`,
};

const fakeGateway: CheckoutGateway = {
  async getCart() {
    return { totalMinor: 12999, currency: "USD" };
  },
};

test("sepet toplamını yerel ayara göre şekillendirir", async () => {
  const view = await presentCheckout(
    { locale: "en-US" },
    { formatter: fakeFormatter, gateway: fakeGateway },
  );
  expect(view.total).toBe("USD 129.99 [en-US]");
});

Test milisaniyeler içinde çalışır ve şekillendirilmiş sunum çıktısını sıfır I/O ile doğrular. Alt servis aktarım formatını değiştirdiğinde, bu değişiklik gateway adaptöründe soğurulur. Bir tasarımcı, bir yerel ayarda paranın nasıl okunacağını değiştirdiğinde, çekirdeğin mantığına dokunmadan yeni dizgeyi sahte bir biçimleyiciye karşı doğrularsınız.

Sunum Servisi ile BFF Farkı#

En yaygın hata, bir sunum servisini Backend-for-Frontend ile karıştırmaktır. İkisi farklı soruları yanıtlar ve bu ayrım önemlidir.

Bir BFF, onu kimin tükettiğiyle tanımlanır. Desen SoundCloud’da geliştirildi; terimi web teknoloji lideri Nick Fisher ortaya attı. Sam Newman kanonik tanımı verir: “genel amaçlı bir API arka ucu yerine, her kullanıcı deneyimi için bir arka ucunuz olur” ve BFF “belirli bir kullanıcı deneyimine sıkı sıkıya bağlıdır” ve “kullanıcı arayüzüyle aynı ekip tarafından bakımı yapılır.” Bir BFF tipik olarak birçok alt servise yayılır ve UI başına toplama yapar. Bu yayılmanın sipariş, envanter ve ödeme servisleri üzerinden işlenmiş bir örneği için AppSync Subscription’larını AppSync Dışından Tetiklemek yazısına bakın.

Bir sunum servisi ise ne yaptığıyla tanımlanır: esas olarak tek bir domain bağımlılığı üzerinde sunum mantığı, içeride hexagonal katmanlamayla düzenlenir. Bir BFF olabilir, çünkü topoloji ile iç yapı birbirinden bağımsız (orthogonal) eksenlerdir. BFF bir topoloji ve sahiplik sorusunu yanıtlar: her ön uç için bir arka uç, UI ekibinin sahip olduğu, birçok servise yayılan. Hexagonal ise servis içindeki bağımlılıkların hangi yöne işaret ettiğini, yani iç yapıyı belirler.

Sunum Servisi (yapı seçimi)

UI Parçası

HTTP Kenarı

Domain Çekirdeği

Tek Domain Servisi

BFF (topoloji seçimi)

Mobil UI

BFF

Servis A

Servis B

Servis C

Calçado, bu karışıklığı kolaylaştıran tarihsel kaymayı belgeler: SoundCloud’un ilk BFF’leri, sunum kavramları (örneğin profil sayfasının sunucu tarafı bir kavram olarak ele alınması) içine göç etmeden önce “hâlâ büyük ölçüde genel API’ye benziyordu.” Yani bir BFF sunum mantığı biriktirebilir; iç yapı sorusunun topoloji sorusundan ayrı kalmasının nedeni tam da budur. Port-ve-adaptör düzeni bir şeyi BFF yapmaz ve bir BFF’in içeride hexagonal olması gerekmez. Servisin tek bir domain bağımlılığı varsa ona sunum servisi deyin ki geçerli olmayan yayılma dayanıklılığı varsayımlarını miras almayın.

Simetri Üzerine Duruş#

Adı konmaya değer gerçek bir gerilim var. Fowler, hexagonu “bir çekirdeği saran arayüzlerden oluşan simetrik bileşenler yaratmak için sunum katmanı ile veri kaynağı katmanı arasındaki benzerlikleri kullandığı” için över, ancak aynı özelliği bir dezavantaj olarak işaretler: “katmanlar olarak daha iyi temsil edilecek olan bir servis sağlayıcı ile servis tüketicisi arasındaki doğal asimetriyi” gizler.

Bir sunum servisi için asimetri gerçektir. Bir gelen HTTP kenarı ve birkaç yönlendirilen adaptör vardır. Pratik duruş şu: port disiplinini koruyun (çekirdek arayüzlere sahiptir, bağımlılıklar içeri işaret eder), yönlendirilen tarafın ise sıradan katmanlar gibi okunmasına izin verin; böylece gelen ve giden tarafları ayna görüntüsü saymadan portlardan test edilebilirlik kazanırsınız.

Hexagonun Maliyeti#

Yapının gerçek bir başlangıç maliyeti var. Şu durumlarda atlayın:

  • Biçimleme, i18n veya oturum şekillendirme içermeyen, gerçekten önemsiz bir geçirgenin (pass-through) hexagona ihtiyacı yoktur. Kurulan yapı, yerine geçtiği yapışkan koddan daha pahalıya mal olur.
  • Her şeye erkenden port açmak. Tam olarak tek uygulaması olan ve test dikişi bulunmayan şeyler için port tanımlamak, karşılığı olmayan dolaylama ekler. Uygulamayı gerçekten değiştirecek, testte yerine bir sahte koyacak ya da çekirdekten ayıracak somut bir nedeniniz olduğunda port tanıtın.
  • Kısa ömürlü veya tek kişilik işler. Tek seferlik bir deney veya bir prototip bu yapıyı nadiren geri öder.

AWS Prescriptive Guidance pragmatist çekinceyi doğrudan belirtir: adaptör kodu “yalnızca uygulama bileşeni birkaç girdi kaynağı ve çıktı hedefi gerektiriyorsa… veya girdiler ve çıktı veri deposu zaman içinde değişmek zorundaysa gerekçelendirilir. Aksi takdirde adaptör, bakım yükü getiren, bakımı yapılacak başka bir katman hâline gelir.” Ayrıca “port ve adaptör kullanmak başka bir katman ekler ve bu gecikmeye yol açabilir” der.

Bu, maksimalist ile pragmatist arasındaki ayrımdır. Node ve TypeScript eğitimleri port-ve-adaptörü varsayılan iyi yapı olarak sunma eğilimindedir; pragmatist okuma ise şudur: bu yatırım, ancak sınırların maliyetini karşılayacak kadar uzun yaşayan ve yeterince yapışkan kod biriktiren servislerde kazanca dönüşür.

Hayır

Evet

Hayır

Evet

Hayır

Evet

Tek domain bağımlılığı üzerinde sunum mantığı?

Düz proxy (hexagonu atla)

Uzun ömürlü mü?

Gerçek değiştirme, sahteleme veya izolasyon ihtiyacı?

Hafif yapı (portları ertele)

Port-ve-adaptör (önerilen varsayılan)

Lint Kuralının Yakaladığı Sızıntılar#

Önce iki sızıntı görünür: denetleyicilerde yaşayan biçimleme ve i18n çağrıları, bir de çekirdeğe ulaşan alt servis aktarım şekilleri. İlkini çekirdeğin sahip olduğu bir portun arkasına taşıyın; ikincisinin yeri, gelen şekilleri sınırda domain tiplerine eşleyen gateway adaptörüdür. domain/ içinde herhangi bir HTTP, para veya i18n içe aktarmasını yasaklayan bir lint kuralı ikisini de yakalar. Gözden geçirmeye değer iki sessiz sorun daha var: tek uygulamayla ve testsiz tanımlanmış portlar, bir de her işleyiciye serpiştirilen gözlemlenebilirlik.

Varsayılan Nerede Geçerli#

Varsayılan, gerçekten biçimleme, i18n ve oturum işi yapan uzun ömürlü bir sunum servisi için geçerlidir: protokolü, formatı, sağlayıcıyı ve gözlemlenebilirliği kenarlarda tutun. Önemsiz bir geçirgen veya tek seferlik bir prototip söz konusuysa varsayılanı uygulamayın.

Kaynaklar#

İlgili yazılar