Facade Pattern: Karmaşık Alt Sistemi Tek Kapıdan Sunmak
Facade, birden fazla subsystem'i doğru sırayla ve doğru compensation adımlarıyla kullanma bilgisini tek bir class'a toplar; subsystem'i kapatmadan client'a kolay bir giriş noktası verir. Bookstore'da stok, ödeme, fatura ve bildirimi koordine eden bir CheckoutFacade, File.ReadAllTextAsync ve GetFromJsonAsync'in arkasındaki gerçek kod, Facade'ın Service Layer, Adapter ve Mediator'dan farkı ve god facade tuzağı.
Sürüm notu: Bu yazıdaki kodlar C# 14 ve .NET 10 ile derlenip denenmiştir.
Önce Tahmin Et
Bookstore'da sipariş iki yerden geliyor: web sitesinin çağırdığı bir Web API (Application Programming Interface) endpoint'i ve çağrı merkezinin kullandığı telefonla sipariş ekranı. İkisi de aynı dört subsystem'i kullanıyor: stoğu ayıran Inventory, kartı onaylayıp tahsil eden PaymentGateway, faturayı kesen InvoiceService ve müşteriye e-posta atan CustomerNotifier. Telefon ekranı bir yıl önce web endpoint'inden kopyalanarak yazılmış. Demo ödeme sağlayıcısı 100 EUR üstündeki tutarları kart limiti yüzünden reddediyor.
namespace Bookstore.Checkout;
// Web API endpoint'i: dört subsystem'i ve aralarındaki sırayı biliyor
public static class OrderEndpoint
{
public static async Task<string> PlaceOrderAsync(Cart cart, Inventory inventory, PaymentGateway payments,
InvoiceService invoices, CustomerNotifier notifier, CancellationToken ct)
{
Reservation reservation = inventory.Reserve(cart.Lines);
PaymentResult payment = await payments.AuthorizeAsync(cart.CustomerId, cart.Total, ct);
if (!payment.Approved)
{
inventory.Release(reservation);
return $"declined ({payment.DeclineCode})";
}
await payments.CaptureAsync(payment.AuthorizationId!, ct);
inventory.Commit(reservation);
string invoiceNo = await invoices.CreateAsync(Guid.CreateVersion7(), cart, ct);
await notifier.SendOrderConfirmationAsync(cart.Email, invoiceNo, ct);
return $"placed ({invoiceNo})";
}
}
// Çağrı merkezi ekranı: aynı akışın kopyası, bir satırı eksik
public static class PhoneOrderScreen
{
public static async Task<string> PlaceOrderAsync(Cart cart, Inventory inventory, PaymentGateway payments,
InvoiceService invoices, CustomerNotifier notifier, CancellationToken ct)
{
Reservation reservation = inventory.Reserve(cart.Lines);
PaymentResult payment = await payments.AuthorizeAsync(cart.CustomerId, cart.Total, ct);
if (!payment.Approved)
return $"declined ({payment.DeclineCode})";
await payments.CaptureAsync(payment.AuthorizationId!, ct);
inventory.Commit(reservation);
string invoiceNo = await invoices.CreateAsync(Guid.CreateVersion7(), cart, ct);
await notifier.SendOrderConfirmationAsync(cart.Email, invoiceNo, ct);
return $"placed ({invoiceNo})";
}
}
Stokta 5 adet Dune var. Önce web'den, sonra telefondan, her biri 3 adetlik ve 149.70 EUR tutarlı iki sipariş geliyor; ikisi de reddedilecek. İkisinden sonra stokta kaç Dune görünür?
const string Dune = "978-0441013593";
Inventory inventory = new();
PaymentGateway payments = new();
InvoiceService invoices = new();
CustomerNotifier notifier = new();
Cart small = new(Guid.CreateVersion7(), "ada@example.com", [new(Dune, 2, 12.99m)]);
Cart large = new(Guid.CreateVersion7(), "alan@example.com", [new(Dune, 3, 49.90m)]);
if (args is ["naive"])
{
Console.WriteLine("web: " + await OrderEndpoint.PlaceOrderAsync(large, inventory, payments, invoices, notifier, default));
Console.WriteLine("phone: " + await PhoneOrderScreen.PlaceOrderAsync(large, inventory, payments, invoices, notifier, default));
Console.WriteLine($"Dune available: {inventory.Available(Dune)}, open reservations: {inventory.OpenReservations}");
return;
}
Aynı Akışın İki Kopyası
Çıktı:
inventory: reserved 3 item(s)
payments: authorize 149.70 EUR -> insufficient_funds
inventory: reservation released
web: declined (insufficient_funds)
inventory: reserved 3 item(s)
payments: authorize 149.70 EUR -> insufficient_funds
phone: declined (insufficient_funds)
Dune available: 2, open reservations: 1
Hiçbir kitap satılmadı ama stokta 5 yerine 2 Dune görünüyor ve askıda bir reservation var. Web endpoint'i ödeme reddedilince inventory.Release çağırıyor; telefon ekranının kopyasında o satır yok. Kod derleniyor, testleri olan tek yol web tarafı, ve hata ancak ay sonu sayımında "depoda 3 kitap var, sistem 0 diyor" diye ortaya çıkıyor.
Asıl problem eksik satır değil. İki client da dört subsystem'i, onları hangi sırayla çağırmak gerektiğini ve bir adım başarısız olunca hangi adımın geri alınacağını biliyor. Bu bilgi bir iş kuralı, ama kimsenin sahiplenmediği bir iş kuralı: her client'ta yeniden yazılıyor. Yarın bir mobil uygulama ya da toplu sipariş import'u geldiğinde üçüncü ve dördüncü kopya yazılacak. Coupling açısından bakarsan her client dört class'a ve onların çağrı protokolüne bağımlı. SOLID (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) ve Design Principles serisindeki "Coupling ve Cohesion" yazısında anlattığımız Law of Demeter'in (bir object yalnızca yakın arkadaşlarıyla konuşmalı) tam tersi.
Bu yazıda bu bilgiyi tek bir yere toplayan pattern'i, Facade'ı konuşacağız: GoF'un tanımı, Bookstore'da bir CheckoutFacade, .NET'in kendi kütüphanesinde her gün kullandığın iki facade, Facade'ın Service Layer, Adapter ve Mediator ile farkı ve facade'ın kendisinin bir god object'e dönüştüğü an.
GoF'un Tanımı: Facade Ne Yapar, Ne Yapmaz
Facade, GoF (Gang of Four: Erich Gamma, Richard Helm, Ralph Johnson ve John Vlissides'in 1994 tarihli Design Patterns: Elements of Reusable Object-Oriented Software kitabı) kataloğundaki structural pattern'lerden biri. Kitaptaki amacını kendi cümlelerimle anlatırsam: bir subsystem'deki interface'ler kümesine tek ve birleşik bir interface vermek; subsystem'i kullanmayı kolaylaştıran, daha üst seviyeli bir giriş noktası tanımlamak.
Kitabın örneği bir derleyici: Scanner, Parser, ProgramNodeBuilder ve CodeGenerator gibi class'lardan oluşan bir subsystem ve bunların önünde tek bir Compiler class'ı. Derleyicinin çoğu kullanıcısı "şu kaynağı derle" demek ister; parser'ı tek başına kullanmak isteyen az sayıdaki ileri seviye kullanıcı için de alt class'lar erişilebilir kalır. Kitap bu ikinci noktanın üzerinde özellikle durur: facade, client'ların subsystem class'larını doğrudan kullanmasını engellemez. Kolay yolu sunar, zor yolu kapatmaz.
Rolleri iki tane: Facade, hangi subsystem class'ının hangi isteğe cevap vereceğini bilir ve client'ın isteğini onlara dağıtır; subsystem class'ları gerçek işi yapar ve facade'dan haberdar değildir. Bu son ayrıntı önemli: bağımlılık tek yönlü, facade subsystem'i tanıyor, subsystem facade'ı tanımıyor. GoF'un "consequences" bölümünde saydığı kazançlar da buradan geliyor: client'ın bildiği class sayısı düşer, client ile subsystem arasındaki coupling zayıflar, subsystem içerideki class'larını client'ı kırmadan değiştirebilir.
CheckoutFacade: Akışı Tek Yerde Toplamak
Sipariş akışını ve compensation (telafi) adımlarını bir class'a alalım. Sonucu exception yerine bir CheckoutResult hiyerarşisiyle döndürüyoruz; stok yetmemesi ve kartın reddedilmesi beklenen iş sonuçları, sıra dışı durumlar değil.
namespace Bookstore.Checkout;
public abstract record CheckoutResult
{
public sealed record Placed(Guid OrderId, string InvoiceNo) : CheckoutResult;
public sealed record Declined(string Reason) : CheckoutResult;
public sealed record OutOfStock(string Isbn) : CheckoutResult;
}
// Facade: subsystem'lerin sırasını ve compensation adımlarını tek yerde tutar
public sealed class CheckoutFacade(
Inventory inventory,
PaymentGateway payments,
InvoiceService invoices,
CustomerNotifier notifier)
{
public async Task<CheckoutResult> PlaceOrderAsync(Cart cart, CancellationToken ct = default)
{
Reservation reservation;
try
{
reservation = inventory.Reserve(cart.Lines);
}
catch (OutOfStockException ex)
{
return new CheckoutResult.OutOfStock(ex.Isbn);
}
PaymentResult payment = await payments.AuthorizeAsync(cart.CustomerId, cart.Total, ct);
if (!payment.Approved)
{
inventory.Release(reservation);
return new CheckoutResult.Declined(payment.DeclineCode!);
}
try
{
await payments.CaptureAsync(payment.AuthorizationId!, ct);
}
catch
{
// Compensation: authorization'ı iptal et, stoğu geri bırak
await payments.VoidAsync(payment.AuthorizationId!, CancellationToken.None);
inventory.Release(reservation);
throw;
}
inventory.Commit(reservation);
Guid orderId = Guid.CreateVersion7();
string invoiceNo = await invoices.CreateAsync(orderId, cart, ct);
await notifier.SendOrderConfirmationAsync(cart.Email, invoiceNo, ct);
return new CheckoutResult.Placed(orderId, invoiceNo);
}
}
Aynı müşterilerle üç sipariş: biri geçen, biri kart limitine takılan, biri stok yetmeyen.
CheckoutFacade checkout = new(inventory, payments, invoices, notifier);
foreach (Cart cart in (Cart[])[small, large, new(Guid.CreateVersion7(), "grace@example.com", [new("978-0547928227", 2, 15.00m)])])
{
CheckoutResult result = await checkout.PlaceOrderAsync(cart);
Console.WriteLine(result switch
{
CheckoutResult.Placed placed => $"-> placed {placed.InvoiceNo}",
CheckoutResult.Declined declined => $"-> declined: {declined.Reason}",
CheckoutResult.OutOfStock outOfStock => $"-> out of stock: {outOfStock.Isbn}",
_ => throw new UnreachableException()
});
}
Console.WriteLine($"Dune available: {inventory.Available(Dune)}, open reservations: {inventory.OpenReservations}");
inventory: reserved 2 item(s)
payments: authorize 25.98 EUR -> approved
payments: captured
invoices: INV-2026-00001 for 25.98 EUR
notifier: confirmation sent to ada@example.com
-> placed INV-2026-00001
inventory: reserved 3 item(s)
payments: authorize 149.70 EUR -> insufficient_funds
inventory: reservation released
-> declined: insufficient_funds
-> out of stock: 978-0547928227
Dune available: 3, open reservations: 0
5 Dune'dan 2'si satıldı, 3 kaldı, askıda reservation yok. Web endpoint'i ve telefon ekranı artık tek satır: await checkout.PlaceOrderAsync(cart, ct). Hangi subsystem'in hangi sırayla çağrıldığını, capture başarısız olursa authorization'ın (provizyon) iptal edileceğini ve stoğun geri bırakılacağını yalnızca facade biliyor. Kuralı değiştirmek gerektiğinde (ör. "fatura e-postası gönderilemezse sipariş yine de geçerli olsun, e-posta queue'ya girsin") değişecek tek yer burası.
Dikkat edersen facade yeni bir iş yapmıyor; stok, ödeme, fatura ve bildirim kendi class'larında. Facade yalnızca koordinasyonu üstleniyor. İyi bir facade'ın method'ları client'ın dilinden konuşur ("siparişi tamamla"), subsystem'in dilinden değil ("rezerve et, authorize et, capture et, commit et").
Dependency Injection (DI) ile kayıt da bu niyeti gösteriyor:
using Microsoft.Extensions.DependencyInjection;
namespace Bookstore.Checkout;
public static class CheckoutRegistration
{
// Subsystem'ler de kayıtlı kalır: facade tek kapı değil, kısa yol
public static IServiceCollection AddCheckout(this IServiceCollection services) =>
services
.AddSingleton<Inventory>()
.AddSingleton<PaymentGateway>()
.AddSingleton<InvoiceService>()
.AddSingleton<CustomerNotifier>()
.AddScoped<CheckoutFacade>();
}
if (args is ["di"])
{
using ServiceProvider provider = new ServiceCollection().AddCheckout().BuildServiceProvider();
using IServiceScope scope = provider.CreateScope();
CheckoutResult placed = await scope.ServiceProvider.GetRequiredService<CheckoutFacade>().PlaceOrderAsync(small);
int left = scope.ServiceProvider.GetRequiredService<Inventory>().Available(Dune);
Console.WriteLine($"{placed.GetType().Name}, stock report reads Inventory directly: {left} left");
return;
}
inventory: reserved 2 item(s)
payments: authorize 25.98 EUR -> approved
payments: captured
invoices: INV-2026-00001 for 25.98 EUR
notifier: confirmation sent to ada@example.com
Placed, stock report reads Inventory directly: 3 left
Stok raporu ekranının sipariş akışıyla bir ilgisi yok, Inventory'yi doğrudan kullanıyor. GoF'un "kolay yolu sun, zor yolu kapatma" dediği şey bu: facade'ı tek kapı yapmak için subsystem class'larını internal yapıp her şeyi facade'dan geçirmeye zorlamak, birazdan konuşacağımız god facade'ın en kısa yolu. Lifetime'lar (singleton, scoped) ve container'ın nasıl çalıştığı için C# İleri serisindeki "Dependency Injection Temelleri" yazısına bakabilirsin.
.NET'te Facade: File.ReadAllTextAsync ve GetFromJsonAsync
.NET'in BCL'i (Base Class Library) facade'larla dolu; çoğunu facade olduğunu düşünmeden kullanıyorsun. İki tanesinin arkasında ne olduğuna bakalım. File.ReadAllTextAsync'in kaynak kodu (github.com/dotnet/runtime, File.cs) asenkron ve sıralı okumaya ayarlanmış bir FileStream açıyor, üzerine BOM'dan (Byte Order Mark) encoding tespit eden bir StreamReader koyuyor, ArrayPool<char>.Shared'dan bir buffer kiralayıp parça parça okuyor, parçaları bir StringBuilder'da topluyor ve sonunda buffer'ı havuza geri veriyor. HttpClient.GetFromJsonAsync (System.Net.Http.Json) ise isteği HttpCompletionOption.ResponseHeadersRead ile gönderiyor, EnsureSuccessStatusCode çağırıyor, body'yi stream olarak System.Text.Json'a veriyor. İkisini elle yazılmış karşılıklarıyla yan yana çalıştıralım:
using System.Buffers;
using System.Globalization;
using System.Net;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
string path = Path.Combine(Path.GetTempPath(), "catalog.csv");
await File.WriteAllTextAsync(path, "978-0441013593;Dune;12.99\n978-0547928227;The Hobbit;15.00\n");
// Tek satır: facade
string viaFacade = await File.ReadAllTextAsync(path);
// Aynı işin elle yazılmış hâli: facade'ın arkasındaki subsystem
string byHand;
using (FileStream stream = new(path, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 4096,
FileOptions.Asynchronous | FileOptions.SequentialScan))
using (StreamReader reader = new(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true))
{
char[] buffer = ArrayPool<char>.Shared.Rent(reader.CurrentEncoding.GetMaxCharCount(4096));
try
{
StringBuilder text = new();
int read;
while ((read = await reader.ReadAsync(buffer.AsMemory())) != 0)
text.Append(buffer, 0, read);
byHand = text.ToString();
}
finally
{
ArrayPool<char>.Shared.Return(buffer);
}
}
Console.WriteLine($"same text: {viaFacade == byHand}, {viaFacade.Length} chars");
using HttpClient client = new(new CatalogStub()) { BaseAddress = new Uri("https://catalog.example/") };
// Tek satır: facade
Book? dune = await client.GetFromJsonAsync<Book>("books/978-0441013593");
// Elle: istek, status kontrolü, stream, deserialize
using (HttpResponseMessage response = await client.GetAsync("books/978-0441013593", HttpCompletionOption.ResponseHeadersRead))
{
response.EnsureSuccessStatusCode();
await using Stream body = await response.Content.ReadAsStreamAsync();
Book? manual = await JsonSerializer.DeserializeAsync<Book>(body, JsonSerializerOptions.Web);
Console.WriteLine($"facade: {dune}");
Console.WriteLine($"manual: {manual}");
}
try
{
await client.GetFromJsonAsync<Book>("books/000");
}
catch (HttpRequestException ex)
{
Console.WriteLine($"{ex.GetType().Name}: {(int?)ex.StatusCode} {ex.Message}");
}
public sealed record Book(string Isbn, string Title, decimal Price);
// Ağa çıkmadan cevap veren handler
sealed class CatalogStub : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken ct) =>
Task.FromResult(request.RequestUri!.AbsolutePath.EndsWith("978-0441013593")
? new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("""{"isbn":"978-0441013593","title":"Dune","price":12.99}""", Encoding.UTF8, "application/json")
}
: new HttpResponseMessage(HttpStatusCode.NotFound));
}
same text: True, 58 chars
facade: Book { Isbn = 978-0441013593, Title = Dune, Price = 12.99 }
manual: Book { Isbn = 978-0441013593, Title = Dune, Price = 12.99 }
HttpRequestException: 404 Response status code does not indicate success: 404 (Not Found).
Sonuçlar aynı, ama elle yazdığın versiyon aslında facade'ın yaptığı her şeyi yapmıyor. GetFromJsonAsync'in kaynağına bakınca iki şey daha görüyorsun: client.Timeout'u body okuma süresine de uygulamak için linked bir CancellationTokenSource kuruyor ve ResponseHeadersRead kullandığı için body'yi MaxResponseContentBufferSize ile sınırlayan bir stream'e sarıyor. Elle yazdığın kodda timeout yalnızca header'ların gelmesine kadar geçerli ve sınırsız büyüklükte bir cevabı okumaya hazırsın. Facade'ın değeri yalnızca satır sayısı değil; subsystem'i doğru sırayla ve doğru ayarlarla kullanma bilgisinin tek bir yerde, test edilmiş hâlde durması.
Bir de şuna dikkat et: facade'lar subsystem'i kapatmıyor. Dosyayı satır satır, büyük dosyada bellek dostu okumak istersen FileStream ve StreamReader hâlâ orada; HTTP (Hypertext Transfer Protocol) cevabının header'larına bakman gerekirse GetAsync hâlâ orada. GoF'un derleyici örneğindeki yapının birebir aynısı.
Facade, Service Layer, Adapter ve Mediator
Bu dört kavram aynı kodda yan yana durabildiği için sık karıştırılıyor. Farkı niyetten ve bağımlılığın yönünden okumak gerekiyor:
| Kavram | Niyet | Bağımlılık yönü | Bookstore'daki karşılığı |
|---|---|---|---|
| Facade | Karmaşık bir subsystem'i kolay kullanılır bir giriş noktasıyla sunmak | Facade subsystem'i tanır, subsystem facade'ı tanımaz | CheckoutFacade |
| Service Layer | Uygulamanın sınırını, yani dışarıya açılan operasyonları ve transaction sınırlarını tanımlamak | Controller ve endpoint'ler service'e, service domain'e ve altyapıya | OrderService.PlaceOrderAsync |
| Adapter | Var olan bir interface'i client'ın beklediği interface'e çevirmek | Adapter tek bir adaptee'yi sarar | Ödeme sağlayıcısının SDK'sını (Software Development Kit) IPaymentGateway'e uydurmak |
| Mediator | Birbiriyle konuşan object'lerin konuşmasını merkeze almak | İki yönlü: colleague'lar mediator'ı tanır, mediator colleague'ları | Sepet ekranında birbirini güncelleyen form alanları |
Service Layer, Martin Fowler'ın PoEAA (Patterns of Enterprise Application Architecture, 2002) kitabındaki pattern; bir facade gibi davranabilir, hatta Fowler da onu ince bir facade olarak kurmayı seçeneklerden biri olarak anlatır. Fark ölçekte: Service Layer uygulamanın bütün dış sınırını tarif eder, facade ise herhangi bir seviyede, tek bir subsystem'in önünde durabilir. File.ReadAllTextAsync bir facade ama kimse ona service layer demez. ASP.NET Core Web API serisindeki "Katmanlı Yapı: Service Katmanı ve Repository Tartışması" yazısı bu katmanın Web API'deki yerini anlatıyor.
Adapter ile fark, bu serinin Adapter Pattern yazısında gördüğümüz gibi, sayıda ve niyette. Adapter genellikle tek bir class'ı sarar ve interface'ini değiştirir; facade birden fazla class'ı koordine eder ve yeni, daha basit bir interface tanımlar. Mediator ile fark ise yönde: GoF, Mediator'ın da var olan class'ların işlevselliğini soyutladığı için Facade'a benzediğini söyler; ama mediator colleague'lar arasındaki konuşmayı merkeze alır ve colleague'lar mediator'ı tanıyıp ona mesaj gönderir. Subsystem class'ları ise facade'ın varlığından habersizdir. Mediator'ı ve MediatR kütüphanesinin bu ikisinin neresinde durduğunu serinin ilerideki Mediator yazısında konuşacağız.
Sık Düşülen Tuzaklar
God facade. CheckoutFacade bugün tek method'lu. Altı ay sonra CancelOrder, RefundOrder, ChangeShippingAddress, GetOrderHistory, ExportInvoicesToExcel eklenmiş ve constructor'ı on bir dependency alıyor. Facade, "sipariş ile ilgili her şey buraya" kovasına dönüştü. Belirti basit: method'lar ortak dependency'leri kullanmıyor, her biri constructor'daki parametrelerin farklı bir alt kümesine dokunuyor. Çözüm de basit: facade'ı use case'lere böl (CheckoutFacade, RefundFacade) ve her biri kendi küçük subsystem kümesini koordine etsin.
Subsystem tiplerini dışarı sızdırmak. Facade'ın method'u PaymentResult ya da sağlayıcının SDK'sından gelen bir tipi döndürüyorsa client yine subsystem'e bağımlıdır, yalnızca bir adım uzaktan. Facade'ın imzaları client'ın tipleriyle konuşmalı; örnekteki CheckoutResult bu yüzden ayrı bir tip.
Tek class'ın önüne facade koymak. Tek bir service'in method'larını birebir yeniden yayınlayan bir class facade değil, fazladan bir katman. Koordine edecek birden fazla parça yoksa, ya da tek parçanın interface'i zaten client'a uygunsa, facade'a gerek yok.
Facade'ı tek kapı yapmak. Subsystem class'larını gizleyip her ihtiyacı facade'a method olarak eklemek god facade'ı garantiler. Stok raporu Inventory'yi doğrudan kullanabilmeli.
Compensation'ın atomik sanılması. Facade sırayı ve compensation adımlarını topluyor, ama bu bir veritabanı transaction'ı değil. Capture başarılı olup fatura kesilirken process çökerse para çekilmiş, fatura kesilmemiş olur. Bu tür dağıtık tutarlılık problemleri (Outbox, Saga) bu serinin değil, ilerideki Yazılım Mimarisi serilerinin konusu; aynı siparişin retry'da iki kez oluşmaması için Çözüm Desenleri serisindeki "Idempotency Key" yazısına bakabilirsin.
Ne Zaman Kullanma
Client tek bir subsystem class'ıyla konuşuyorsa facade gereksiz. Akış tek bir yerde kullanılıyorsa ve ikinci bir client ufukta yoksa, akışı bir method'a çıkarmak yeterli; o method'un adı ileride CheckoutFacade olur, olmayabilir de. Bir de facade'ı "katman olsun diye" eklemekten kaçın: her controller'ın önünde bir facade, her facade'ın önünde bir service, her service'in önünde bir manager olan kod tabanında bir satır davranışı değiştirmek için dört dosya açarsın. Facade'ın kendini amorti ettiği yer, aynı koordinasyon bilgisinin en az iki client'ta tekrar ettiği ya da subsystem'in kullanımının gerçekten zor olduğu yer.
Küçük Bir Deneme
★☆☆ CustomerNotifier'ı belirli bir e-posta adresinde exception fırlatacak şekilde değiştir. Şu anki facade'da ne oluyor: para çekildi mi, stok commit edildi mi, client ne görüyor? Sipariş geçerli kalsın, bildirim hatası yalnızca loglansın diye facade'ı düzelt.
★★☆ CheckoutFacade için bir ICheckout interface'i çıkar ve web endpoint'ini bu interface'e bağımlı yaz. Endpoint'in testinde gerçek subsystem'lere hiç dokunmadan "declined" sonucunu nasıl üretirsin?
★★★ Facade'a CancelOrderAsync ve RefundOrderAsync eklediğini düşün. Constructor'a hangi yeni dependency'ler gelir, hangi method hangi dependency'leri kullanır? Bir tablo çıkar ve facade'ı use case'lere bölmenin ne zaman mantıklı olduğuna karar ver.
Bir sonraki yazıda structural pattern'lerin en az tanınanına geçiyoruz: Flyweight. Milyonlarca satırlık bir raporda aynı yayınevi ve kategori bilgisini milyonlarca kez saklamak yerine paylaşmayı, intrinsic ve extrinsic state ayrımını, string.Intern'ü, ArrayPool<T>'yi ve bunları BenchmarkDotNet ile ölçmeyi konuşacağız.