Mert Özen Her Satırda Daha İleri
Yazılım Tasarımı Design Patterns — #10

Decorator Pattern: Caching, Logging ve Retry'ı Koda Dokunmadan Eklemek

Mert Özen 7 Kas 2026 13 dk 6 görüntülenme
Decorator Pattern: Caching, Logging ve Retry'ı Koda Dokunmadan Eklemek

Decorator, aynı interface'i implement edip içindeki object'e iş devrederek ona runtime'da davranış ekler; decorator'ların sırası ne ölçtüğünü ve neyi koruduğunu belirleyen bir tasarım kararıdır. Bookstore kataloğuna cache, log ve retry eklemek, Stream ailesi ve leaveOpen ile sahiplik, DI'da elle registration ve Scrutor Decorate, DelegatingHandler ve default interface method'ların decorator'ı sessizce atlatması.

Sürüm notu: Bu yazıdaki kodlar C# 14 ve .NET 10 ile derlenip denenmiştir.

Önce Tahmin Et

Bookstore'un ürün sayfası her açılışta kitabı veritabanından okuyor. Ekip iki şey istiyor: aynı kitap için veritabanına tekrar tekrar gitmemek (cache) ve katalog çağrılarını loglamak. SqlBookCatalog'a dokunmadan, aynı interface'i implement edip içindeki bir IBookCatalog'a iş devreden iki class yazılıyor. Sonra bunlar iki farklı sırayla üst üste takılıyor:

SqlBookCatalog db = new();

IBookCatalog loggingOutside = new LoggingBookCatalog(new CachingBookCatalog(db));
IBookCatalog loggingInside = new CachingBookCatalog(new LoggingBookCatalog(db));

foreach ((string name, IBookCatalog catalog) in new[] { ("log -> cache -> db", loggingOutside), ("cache -> log -> db", loggingInside) })
{
    Console.WriteLine(name);
    for (int i = 0; i < 3; i++)
        await catalog.GetByIsbnAsync("978-0141439518");
}

Console.WriteLine($"Database queries: {db.Queries}");

public sealed record Book(string Isbn, string Title, decimal Price);

// Component: client'ın bildiği tek sözleşme
public interface IBookCatalog
{
    Task<Book?> GetByIsbnAsync(string isbn, CancellationToken cancellationToken = default);
}

// Concrete component: asıl iş (burada veritabanını taklit ediyor)
public sealed class SqlBookCatalog : IBookCatalog
{
    public int Queries { get; private set; }

    public async Task<Book?> GetByIsbnAsync(string isbn, CancellationToken cancellationToken = default)
    {
        Queries++;
        await Task.Delay(20, cancellationToken);
        return isbn == "978-0141439518" ? new Book(isbn, "Emma", 9.50m) : null;
    }
}

// Decorator: aynı interface, içeride bir IBookCatalog, araya davranış
public sealed class CachingBookCatalog(IBookCatalog inner) : IBookCatalog
{
    private readonly Dictionary<string, Book?> _cache = [];

    public async Task<Book?> GetByIsbnAsync(string isbn, CancellationToken cancellationToken = default)
    {
        if (_cache.TryGetValue(isbn, out Book? cached))
            return cached;

        Book? book = await inner.GetByIsbnAsync(isbn, cancellationToken);
        _cache[isbn] = book;
        return book;
    }
}

public sealed class LoggingBookCatalog(IBookCatalog inner) : IBookCatalog
{
    public async Task<Book?> GetByIsbnAsync(string isbn, CancellationToken cancellationToken = default)
    {
        Console.WriteLine($"  [log] GetByIsbn {isbn}");
        return await inner.GetByIsbnAsync(isbn, cancellationToken);
    }
}

Her zincirde aynı kitap üç kez isteniyor. İki zincir toplam kaç log satırı yazar ve veritabanına toplam kaç sorgu gider?

Sıra Neden Önemli

Çıktı:

log -> cache -> db
  [log] GetByIsbn 978-0141439518
  [log] GetByIsbn 978-0141439518
  [log] GetByIsbn 978-0141439518
cache -> log -> db
  [log] GetByIsbn 978-0141439518
Database queries: 2

İki zincir de aynı iki class'tan oluşuyor, ama farklı şeyler ölçüyor. Log en dıştayken her çağrıyı görüyor: üç istek, üç log satırı, bunların ikisi cache'ten döndü. Log cache'in içindeyken yalnız cache'i geçen çağrıları görüyor: tek satır, o da veritabanına giden tek sorgu. Her zincir kendi cache'iyle bir kez veritabanına gitti, toplam iki sorgu.

Hangisi doğru? İkisi de, soruya göre. "Ürün sayfası saniyede kaç kez katalog istiyor" sorusunun cevabı dıştaki logda, "veritabanı ne kadar yükleniyor" sorusunun cevabı içtekinde. Önemli olan sıranın bir tasarım kararı olduğunu bilmek. Aynı mantık diğer davranışlar için de geçerli: yetkilendirme kontrolü cache'in dışında olmalı ki yetkisiz bir kullanıcı cache'teki veriyi alamasın; retry cache'in içinde olmalı ki cache'ten dönen bir sonuç için gereksiz deneme yapılmasın. Zinciri okumak için kuralı ezberle: en dıştaki decorator çağrıyı ilk görür, sonucu en son.

Decorator Pattern

GoF (Gang of Four: Erich Gamma, Richard Helm, Ralph Johnson ve John Vlissides'in 1994 tarihli Design Patterns kitabı) Decorator'ın amacını, bir object'e runtime'da ek sorumluluklar eklemek ve işlevselliği genişletmek için subclass'a esnek bir alternatif sunmak diye tanımlar. Adapter gibi onun da diğer adı Wrapper. Kitaptaki örnek bir metin görünümü: TextView'a kenarlık ve kaydırma çubuğu, onu değiştirmeden, üst üste sarılan decorator'larla ekleniyor.

Roller şöyle: component, client'ın bildiği interface (IBookCatalog); concrete component, asıl işi yapan class (SqlBookCatalog); decorator'lar ise aynı interface'i implement eden ve içlerinde bir component referansı tutan class'lar (CachingBookCatalog, LoggingBookCatalog). Decorator'ı Adapter'dan ayıran şey interface'in aynı kalması: adapter bir interface'i başka birine çevirir, decorator aynı interface'i korur ve davranış ekler. Bu yüzden decorator'lar istediğin kadar üst üste takılabilir ve client sarılmış bir object'le sarılmamış olanı ayırt edemez.

Inheritance ile aynı işi yapmayı denesen LoggingSqlBookCatalog, CachingSqlBookCatalog, CachingLoggingSqlBookCatalog... diye giden bir class listesi çıkar; sıra da class adına gömülür. SOLID (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) ve Design Principles serisindeki "Composition over Inheritance: Fragile Base Class, Delegation ve Davranışı Takmak" yazısında bildirim kanallarına retry, sessiz saat ve log davranışlarını bu şekilde takmıştık; o yazı aynı yapının inheritance'la karşılaştırmasını ayrıntılı gösteriyor. Bu yazıda Decorator'ın .NET'te nerede yaşadığına ve gerçek bir uygulamada nasıl kaydedildiğine odaklanalım.

Stream Ailesi: .NET'in En Eski Decorator'ı

System.IO.Stream .NET'in ilk sürümünden beri Decorator'ın ders kitabı örneği. FileStream ve MemoryStream concrete component'ler; BufferedStream, GZipStream, CryptoStream ise constructor'ında başka bir Stream alıp onu saran ve kendisi de Stream olan decorator'lar. Sipariş export'unu sıkıştırarak belleğe yazalım:

using System.IO.Compression;
using System.Text;

string csv = string.Join('\n', Enumerable.Range(1, 1000).Select(i => $"BK-{i:D5};978-0141439518;Emma;9.50"));

// StreamWriter -> GZipStream -> MemoryStream: her katman bir Stream'i sarıyor
MemoryStream export = new();
using (GZipStream gzip = new(export, CompressionLevel.Optimal))
using (StreamWriter writer = new(gzip, Encoding.UTF8))
    writer.Write(csv);

try
{
    Console.WriteLine(export.Length);
}
catch (ObjectDisposedException ex)
{
    Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
}

// leaveOpen: içteki stream'in sahibi biziz, decorator onu kapatmasın
MemoryStream kept = new();
using (GZipStream gzip = new(kept, CompressionLevel.Optimal, leaveOpen: true))
using (StreamWriter writer = new(gzip, Encoding.UTF8))
    writer.Write(csv);

Console.WriteLine($"{Encoding.UTF8.GetByteCount(csv)} bytes -> {kept.Length} bytes");

kept.Position = 0;
using StreamReader reader = new(new GZipStream(kept, CompressionMode.Decompress));
Console.WriteLine(reader.ReadLine());
ObjectDisposedException: Cannot access a closed Stream.
33999 bytes -> 2363 bytes
BK-00001;978-0141439518;Emma;9.50

Sıkıştırma 34 KB'lık CSV (Comma-Separated Values) metnini 2,3 KB'a indirdi, okuma tarafında da GZipStream bu kez decompress modunda StreamReader'ın altına girdi. StreamWriter sıkıştırmanın varlığından, GZipStream de verinin bellekte mi dosyada mı durduğundan habersiz. Bir şifreleme katmanı gerekseydi araya bir CryptoStream girerdi ve diğer katmanların hiçbiri değişmezdi.

İlk denemedeki exception ise Decorator'ın en sık unutulan sorusunun cevabı: içteki object'in sahibi kim? GZipStream dispose edildiğinde, varsayılan olarak sardığı stream'i de dispose eder. using bloğundan çıkınca export kapanmıştı ve uzunluğunu bile soramadık. Stream'i decorator'dan sonra kullanmaya devam edeceksen leaveOpen: true ile sahipliğin sende kaldığını söylersin. Kendi decorator'ların IDisposable bir component sarıyorsa aynı kararı açıkça vermen gerekiyor: ya decorator içteki object'in sahibidir ve onu dispose eder, ya da sahiplik onu yaratana, çoğu zaman DI (dependency injection) container'a kalır.

DI'da Decorator: Elle Registration ve Scrutor

Gerçek bir ASP.NET Core uygulamasında katalog constructor'dan enjekte ediliyor ve client'lar IBookCatalog istiyor. Zinciri container'a kurdurmanın iki yolu var. Microsoft.Extensions.DependencyInjection'da hazır bir Decorate method'u yok; ya factory ile zinciri elle kurarsın ya da Kristian Hellang'ın açık kaynak Scrutor kütüphanesini kullanırsın. Bu kez zincire geçici veritabanı hatalarını yeniden deneyen üçüncü bir decorator da ekleyelim:

public sealed record Book(string Isbn, string Title, decimal Price);

public interface IBookCatalog
{
    Task<Book?> GetByIsbnAsync(string isbn, CancellationToken cancellationToken = default);
}

public sealed class SqlBookCatalog : IBookCatalog
{
    private int _calls;

    public Task<Book?> GetByIsbnAsync(string isbn, CancellationToken cancellationToken = default)
    {
        // İlk sorgu zaman aşımına uğruyor, sonrakiler başarılı
        if (++_calls == 1)
            throw new TimeoutException("Database timeout");

        Console.WriteLine($"  [db] SELECT ... WHERE Isbn = '{isbn}'");
        return Task.FromResult<Book?>(new Book(isbn, "Emma", 9.50m));
    }
}

public sealed class RetryingBookCatalog(IBookCatalog inner, int maxAttempts) : IBookCatalog
{
    public async Task<Book?> GetByIsbnAsync(string isbn, CancellationToken cancellationToken = default)
    {
        for (int attempt = 1; ; attempt++)
        {
            try
            {
                return await inner.GetByIsbnAsync(isbn, cancellationToken);
            }
            catch (TimeoutException ex) when (attempt < maxAttempts)
            {
                Console.WriteLine($"  [retry] attempt {attempt} failed: {ex.Message}");
            }
        }
    }
}

public sealed class CachingBookCatalog(IBookCatalog inner) : IBookCatalog
{
    private readonly Dictionary<string, Book?> _cache = [];

    public async Task<Book?> GetByIsbnAsync(string isbn, CancellationToken cancellationToken = default)
    {
        if (_cache.TryGetValue(isbn, out Book? cached))
            return cached;

        return _cache[isbn] = await inner.GetByIsbnAsync(isbn, cancellationToken);
    }
}

public sealed class LoggingBookCatalog(IBookCatalog inner) : IBookCatalog
{
    public async Task<Book?> GetByIsbnAsync(string isbn, CancellationToken cancellationToken = default)
    {
        Console.WriteLine($"  [log] GetByIsbn {isbn}");
        return await inner.GetByIsbnAsync(isbn, cancellationToken);
    }
}
using Microsoft.Extensions.DependencyInjection;

// 1) Elle: zinciri factory'de içten dışa kurmak
ServiceCollection manual = new();
manual.AddSingleton<SqlBookCatalog>();
manual.AddSingleton<IBookCatalog>(sp =>
    new LoggingBookCatalog(
        new CachingBookCatalog(
            new RetryingBookCatalog(sp.GetRequiredService<SqlBookCatalog>(), maxAttempts: 3))));

// 2) Scrutor: her Decorate bir öncekini sarar, sonuncusu en dışta
ServiceCollection scrutor = new();
scrutor.AddSingleton<IBookCatalog, SqlBookCatalog>();
scrutor.Decorate<IBookCatalog>((inner, _) => new RetryingBookCatalog(inner, maxAttempts: 3));
scrutor.Decorate<IBookCatalog, CachingBookCatalog>();
scrutor.Decorate<IBookCatalog, LoggingBookCatalog>();

foreach ((string name, ServiceCollection services) in new[] { ("manual", manual), ("scrutor", scrutor) })
{
    using ServiceProvider provider = services.BuildServiceProvider();
    IBookCatalog catalog = provider.GetRequiredService<IBookCatalog>();

    Console.WriteLine($"{name}: {catalog.GetType().Name}");
    for (int i = 0; i < 2; i++)
        Console.WriteLine($"  -> {(await catalog.GetByIsbnAsync("978-0141439518"))?.Title}");
}
manual: LoggingBookCatalog
  [log] GetByIsbn 978-0141439518
  [retry] attempt 1 failed: Database timeout
  [db] SELECT ... WHERE Isbn = '978-0141439518'
  -> Emma
  [log] GetByIsbn 978-0141439518
  -> Emma
scrutor: LoggingBookCatalog
  [log] GetByIsbn 978-0141439518
  [retry] attempt 1 failed: Database timeout
  [db] SELECT ... WHERE Isbn = '978-0141439518'
  -> Emma
  [log] GetByIsbn 978-0141439518
  -> Emma

İki yol aynı zinciri kurdu: dışta log, ortada cache, içte retry, en içte veritabanı. İlk çağrıda veritabanı zaman aşımına uğradı, retry ikinci denemede başardı, cache sonucu sakladı; ikinci çağrı yalnız log'a ve cache'e uğradı. Elle registration'da sıra, iç içe new'lerde görünüyor ve okuması kolay. Scrutor'da ise her Decorate bir öncekini sarar, yani kayıt sırası içten dışa doğru: en son eklenen en dışta. Scrutor'ın kazancı, decorator'ların bağımlılıklarını container'dan çözmesi; LoggingBookCatalog bir ILogger<T> isteseydi onu ayrıca vermen gerekmezdi. Elle yolda ise factory içinde new'lediğin her şeyin bağımlılığını kendin sağlarsın.

Lifetime'a dikkat: zincir singleton olarak kaydedildi ve CachingBookCatalog'un içinde düz bir Dictionary var. Eşzamanlı isteklerde bu thread-safe değil; gerçek uygulamada ConcurrentDictionary, IMemoryCache ya da .NET 9 ile gelen HybridCache kullanırsın. Daha sinsi olanı, singleton bir cache decorator'ının scoped bir component'i, örneğin bir DbContext kullanan katalogu sarması: captive dependency. C# İleri serisinin "C#'ta Dependency Injection Temelleri: Lifetime'lar, Container ve Captive Dependency" yazısı bu tuzağı ayrıntılı anlatıyor. Retry için de bir uyarı: buradaki döngü pattern'i göstermek için; üretimde exponential backoff, jitter ve circuit breaker isteyen dayanıklılık davranışları ilerideki External Service Integration ve Resilience serisinin konusu.

DelegatingHandler: HttpClient Pipeline'ında Decorator

HttpClient'ın handler zinciri de aynı yapıda. DelegatingHandler bir HttpMessageHandler'dır ve InnerHandler property'sinde başka bir HttpMessageHandler tutar; base.SendAsync çağrısı isteği içerdekine iletir:

using System.Net;

// DelegatingHandler da aynı tipten (HttpMessageHandler) bir inner'ı sarar
HttpMessageHandler pipeline = new CorrelationIdHandler
{
    InnerHandler = new LoggingHandler { InnerHandler = new FakeCatalogApi() }
};

using HttpClient client = new(pipeline);
HttpResponseMessage response = await client.GetAsync("https://catalog.bookstore.test/books/978-0141439518");
Console.WriteLine($"status: {(int)response.StatusCode}");

public sealed class CorrelationIdHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Headers.Add("X-Correlation-Id", "c-1001");
        return base.SendAsync(request, cancellationToken);
    }
}

public sealed class LoggingHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        Console.WriteLine($"-> {request.Method} {request.RequestUri!.AbsolutePath} ({string.Join(",", request.Headers.GetValues("X-Correlation-Id"))})");
        HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
        Console.WriteLine($"<- {(int)response.StatusCode}");
        return response;
    }
}

public sealed class FakeCatalogApi : HttpMessageHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>
        Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
}
-> GET /books/978-0141439518 (c-1001)
<- 200
status: 200

Correlation id header'ı ekleyen ve isteği loglayan iki davranış, ne HttpClient'a ne de en içteki handler'a dokunmadan eklendi. ASP.NET Core'da bu zinciri IHttpClientFactory ve AddHttpMessageHandler ile kaydedersin; Microsoft.Extensions.Http.Resilience paketinin standart resilience handler'ı da zincire eklenen bir handler'dır. Her handler isteği bir sonrakine iletmeyi seçip seçmemekte de serbest olduğu için bu zincir Chain of Responsibility'ye de benzer; o bakış açısı bu serinin Chain of Responsibility yazısında.

Interface Büyüdüğünde: Default Interface Method Tuzağı

Decorator'ın sözleşmesi, interface'in her member'ını içteki object'e iletmek. Interface'e yeni bir member eklenince her decorator'ın da güncellenmesi gerekir; abstract bir member eklendiyse derleyici bunu sana hatırlatır. C# 8 ile gelen default interface method'lar ise bu güvenlik ağını sessizce kaldırabilir. Kataloğa toplu okuma ekleyen ekip, mevcut implementasyonları kırmamak için method'u default gövdeli yazıyor ve SqlBookCatalog'da tek sorguluk bir override yazıyor:

string[] isbns = ["978-0141439518", "978-0547928227", "978-0061120084"];

SqlBookCatalog db = new();
IBookCatalog direct = db;
IBookCatalog decorated = new LoggingBookCatalog(db);

await direct.GetManyAsync(isbns);
Console.WriteLine($"direct:    {db.Queries} queries");

db.Queries = 0;
await decorated.GetManyAsync(isbns);
Console.WriteLine($"decorated: {db.Queries} queries");

public sealed record Book(string Isbn, string Title, decimal Price);

public interface IBookCatalog
{
    Task<Book?> GetByIsbnAsync(string isbn, CancellationToken cancellationToken = default);

    // Sonradan eklenen default interface method: mevcut implementasyonlar derlenmeye devam eder
    async Task<IReadOnlyList<Book>> GetManyAsync(IEnumerable<string> isbns, CancellationToken cancellationToken = default)
    {
        List<Book> books = [];
        foreach (string isbn in isbns)
            if (await GetByIsbnAsync(isbn, cancellationToken) is { } book)
                books.Add(book);
        return books;
    }
}

public sealed class SqlBookCatalog : IBookCatalog
{
    public int Queries { get; set; }

    public Task<Book?> GetByIsbnAsync(string isbn, CancellationToken cancellationToken = default)
    {
        Queries++;
        return Task.FromResult<Book?>(new Book(isbn, "Title " + isbn[^4..], 9.50m));
    }

    // Toplu okuma: tek sorgu (WHERE Isbn IN (...))
    public Task<IReadOnlyList<Book>> GetManyAsync(IEnumerable<string> isbns, CancellationToken cancellationToken = default)
    {
        Queries++;
        return Task.FromResult<IReadOnlyList<Book>>([.. isbns.Select(i => new Book(i, "Title " + i[^4..], 9.50m))]);
    }
}

// Interface değişmeden önce yazılmış decorator: GetManyAsync'i bilmiyor
public sealed class LoggingBookCatalog(IBookCatalog inner) : IBookCatalog
{
    public Task<Book?> GetByIsbnAsync(string isbn, CancellationToken cancellationToken = default)
    {
        Console.WriteLine($"  [log] GetByIsbn {isbn}");
        return inner.GetByIsbnAsync(isbn, cancellationToken);
    }
}
direct:    1 queries
  [log] GetByIsbn 978-0141439518
  [log] GetByIsbn 978-0547928227
  [log] GetByIsbn 978-0061120084
decorated: 3 queries

Kod uyarısız derlendi. Doğrudan çağrıda tek sorgu, decorator üzerinden üç sorgu. LoggingBookCatalog GetManyAsync'i implement etmediği için çağrı, interface'in default gövdesine düştü; o gövde de decorator'ın GetByIsbnAsync'ini kitap başına bir kez çağırdı. SqlBookCatalog'un toplu sorgusu decorator'ın arkasında görünmez oldu. Sipariş geçmişi sayfasında yüz kitaplık bir listede bu, bir sorgu yerine yüz sorgu demek. Çözüm, decorator'larda yeni member'ı açıkça inner'a iletmek; bunun hatırlatılması için de default interface method'u decorator'ı olan interface'lerde dikkatli kullanmak ya da decorator'ları bir base class'tan türetip iletmeyi tek yerde yapmak.

Sık Düşülen Tuzaklar

Derin zincirde kaybolmak. Beş decorator'lı bir zincirde bir çağrının nereye gittiğini bulmak için debugger'da beş kez step into yaparsın; stack trace'ler uzar. Her davranış için bir decorator yazmak yerine, gerçekten bağımsız açılıp kapanması gereken davranışları ayır.

Object kimliğine güvenmek. GoF da bunu uyarır: decorator ile sardığı component aynı object değildir. catalog is SqlBookCatalog kontrolü ya da reference equality, decorator eklendiği gün yanlış sonuç verir.

Sıra bağımlılığını belgelememek. Cache'in yetkilendirmenin dışına kaydırılması gibi bir sıra hatası derleme hatası vermez, güvenlik açığı olarak ortaya çıkar. Zinciri tek bir composition root'ta kur ve sırayı test eden bir test yaz.

İletmeyi unutmak. CancellationToken'ı içteki çağrıya geçmemek, async method'da await etmeden sonucu loglamak ya da yukarıdaki gibi yeni bir member'ı iletmemek. Decorator'ın varsayılan davranışı "hiçbir şeyi değiştirmeden ilet" olmalı.

Decorator'a iş kuralı koymak. "Cache'ten dönen kitabın fiyatına kampanya indirimi uygula" gibi bir kural decorator'a girdiğinde, kural zincirin kurulumuna bağlı hâle gelir; decorator'sız bir test ya da başka bir zincir kuralı atlar. Decorator'lar cross-cutting concern'ler için: logging, caching, retry, metrik, yetkilendirme.

Proxy ile karıştırmak. Yapısal olarak aynı görünen ama niyeti erişimi kontrol etmek olan Proxy pattern, bu serinin ilerideki bir yazısının konusu; kabaca, decorator davranış ekler, proxy erişimi yönetir.

Tek kullanımlık davranış için decorator yazmak. Bir davranış yalnız tek bir component için, tek bir yerde ve hep birlikte gerekiyorsa, onu component'in içine iki satır olarak yazmak bir decorator class'ından, onun registration'ından ve sıra kararından daha ucuz. Decorator'ın değeri, aynı davranışın birden fazla component'e takılabildiği ya da ortama göre açılıp kapandığı yerde ortaya çıkar.

Küçük Bir Deneme

★☆☆ Açılıştaki örneğe bir TimingBookCatalog ekle: her çağrının süresini Stopwatch ile ölçüp yazsın. Önce zincirin en dışına, sonra cache ile veritabanı arasına koy ve iki ölçümün neyi anlattığını karşılaştır.

★★☆ CachingBookCatalog'u IMemoryCache kullanacak ve kayıtları beş dakika sonra düşürecek şekilde yeniden yaz; null sonuçları (bulunamayan ISBN (International Standard Book Number)) cache'lemeli misin? Kararını bir testle sabitle.

★★★ Default interface method tuzağını önlemek için BookCatalogDecorator adında abstract bir base class yaz: interface'in tüm member'larını virtual olarak inner'a iletsin. Sonra interface'e yeni bir member ekle ve kaç decorator'ı güncellemen gerektiğini say. Bu çözümün, SOLID ve Design Principles serisinde konuştuğumuz fragile base class riskiyle ilişkisi ne?

Decorator, aynı interface'i koruyarak bir object'in etrafına katmanlar ekledi. Bir sonraki yazıda tersini yapan bir pattern'e geçiyoruz: birden fazla alt sistemi tek ve sade bir interface'in arkasına alan Facade ile sipariş tamamlama akışını stok, ödeme, fatura ve bildirim çağrılarından tek bir kapıya indirmek.