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

Iterator ve Interpreter: C#'ın İçine Gömülmüş İki Pattern — yield return ve Expression Tree

Mert Özen 7 Kas 2026 14 dk 9 görüntülenme
Iterator ve Interpreter: C#'ın İçine Gömülmüş İki Pattern — yield return ve Expression Tree

Iterator, bir collection'ın içini göstermeden element'lerini sırayla dolaşmayı sağlar ve C#'ta IEnumerable, IEnumerator ve yield return olarak dilin içindedir; Interpreter ise küçük bir dilin gramerini bir AST olarak temsil edip yorumlar. Sayfalı bir katalog API'sini yield ve IAsyncEnumerable ile okumayı, kampanya kuralları için küçük bir kural dili yazıp onu Expression tree'ye derlemeyi, Regex'in içindeki interpreter'ı ve dinamik expression'larda güvenlik sınırını gerçek çıktılarla görüyoruz.

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

Önce Tahmin Et

Bookstore'un katalog servisi kitapları sayfa sayfa döndüren bir API (Application Programming Interface) arkasında duruyor. Çağıran kodun sayfalarla uğraşmaması için biri küçük bir yardımcı yazmış: dışarıya tek bir IEnumerable<Book> veriyor, sayfaları kendi içinde çekiyor. Katalogda 8 kitap var, sayfa boyutu 3, fiyatı 40'ın üstündeki ilk kitap dördüncü sırada.

public static IEnumerable<Book> ReadAll(CatalogApi api)
{
    for (int page = 1; ; page++)
    {
        IReadOnlyList<Book> items = api.GetPage(page);
        foreach (Book book in items)
            yield return book;
        if (items.Count < api.PageSize)
            yield break;
    }
}

CatalogApi api = new(all, pageSize: 3);
IEnumerable<Book> books = CatalogReader.ReadAll(api);
Console.WriteLine("Sequence created");
Book? first = books.FirstOrDefault(b => b.Price > 40m);
Console.WriteLine($"First over 40: {first?.Title}");
Console.WriteLine($"Total: {books.Count()}");
Console.WriteLine($"API calls: {api.Calls}");

GetPage her çağrıldığında konsola bir GET satırı yazıyor. "Sequence created" satırından önce kaç istek görürsün? Program bittiğinde API kaç kez çağrılmış olur: 3 mü, 4 mü, 5 mi?

Bir Sequence'ın Arkasındaki Iterator

Gerçek çıktı:

Sequence created
  GET /books?page=1&size=3
  GET /books?page=2&size=3
First over 40: Introduction to Algorithms
  GET /books?page=1&size=3
  GET /books?page=2&size=3
  GET /books?page=3&size=3
Total: 8
API calls: 5

ReadAll çağrıldığında hiçbir istek gitmedi. FirstOrDefault aradığını ikinci sayfada bulunca durdu, üçüncü sayfa hiç istenmedi. Count() ise sequence'ı baştan dolaştı ve iki sayfayı yeniden çekti. Çağıran kod ne sayfa numarasını, ne sayfa boyutunu, ne de "son sayfa mı" kontrolünü gördü; yalnız kitapları gördü. Bu yazının ilk pattern'i tam olarak bu: Iterator.

GoF (Gang of Four: Gamma, Helm, Johnson ve Vlissides'in 1994 tarihli Design Patterns kitabı) Iterator'ı behavioral pattern'ler arasında sayar. Niyeti, bir aggregate object'in element'lerine, içerideki temsilini açığa çıkarmadan sırayla erişmenin bir yolunu vermek. Kitaptaki roller dört tane: Aggregate (iterator üreten interface), ConcreteAggregate (asıl collection), Iterator (dolaşma interface'i) ve ConcreteIterator (dolaşmanın durumunu tutan object). Kitabın C++ örneğinde Iterator'ın First, Next, IsDone ve CurrentItem diye dört operasyonu var.

.NET'te bu dört rolün karşılığı framework'ün en temel interface'leri: IEnumerable<T> Aggregate'tir ve tek method'u GetEnumerator() kitaptaki CreateIterator'dır. IEnumerator<T> Iterator'dır: MoveNext() hem Next'in hem IsDone'un işini görür, Current da CurrentItem'dır. foreach ise bu interface'leri senin yerine çağıran syntax. Açılıştaki sayfa okuma kodu, BCL (Base Class Library) içindeki File.ReadLines ile aynı fikir: dosyayı satır satır okuyan ama dışarıya yalnız bir IEnumerable<string> veren bir iterator.

Elle Yazılmış Iterator ve Derleyicinin Yazdığı

Pattern'i GoF'un tarif ettiği şekilde, elle kurmak şöyle görünür:

// GoF rolleri: Bookshelf = ConcreteAggregate, BookshelfIterator = ConcreteIterator
public sealed class Bookshelf(Book[] books) : IEnumerable<Book>
{
    public IEnumerator<Book> GetEnumerator() => new BookshelfIterator(books);
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    private sealed class BookshelfIterator(Book[] books) : IEnumerator<Book>
    {
        private int _index = -1;

        public Book Current => books[_index];
        object IEnumerator.Current => Current;

        public bool MoveNext() => ++_index < books.Length;
        public void Reset() => _index = -1;
        public void Dispose() { }
    }
}

public sealed class YieldBookshelf(Book[] books) : IEnumerable<Book>
{
    public IEnumerator<Book> GetEnumerator()
    {
        foreach (Book book in books)
            yield return book;
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

İki class'ın enumerator'ını alıp tiplerine bakalım; yanına List<T>'ninkini de ekleyelim:

Bookshelf shelf = new(all.Take(3).ToArray());
IEnumerator<Book> it = shelf.GetEnumerator();
while (it.MoveNext())
    Console.WriteLine(it.Current.Title);
Console.WriteLine(it.GetType().Name);

YieldBookshelf shelf2 = new(all.Take(3).ToArray());
Console.WriteLine(shelf2.GetEnumerator().GetType().Name);
Console.WriteLine(new List<Book>().GetEnumerator().GetType());
Dune
Emma
The Hobbit
BookshelfIterator
<GetEnumerator>d__2
System.Collections.Generic.List`1+Enumerator[Bookstore.Catalog.Book]

<GetEnumerator>d__2 bizim yazmadığımız bir class. yield return gördüğünde derleyici, method'un gövdesini IEnumerator<T> implement eden gizli bir class'a çeviriyor; nerede kaldığını bir state field'ında tutuyor ve her MoveNext()'te oradan devam ediyor. Yani C#'ta ConcreteIterator'ı neredeyse hiç elle yazmazsın; derleyici yazar. List<T>'nin enumerator'ı ise bir struct: foreach interface'e değil, GetEnumerator() method'unun varlığına bakan pattern tabanlı bir syntax olduğu için, BCL sık kullanılan collection'larda allocation yapmayan struct enumerator döndürüyor.

GoF iki ayrımı daha tartışır. Birincisi external ve internal iterator: dolaşmayı client mı yönetiyor (foreach, MoveNext) yoksa collection mı (List<T>.ForEach(Action<T>))? Kitap external iterator'ı daha esnek bulur, çünkü iki collection'ı yan yana dolaşmak gibi işler ancak onunla yapılır; LINQ'in (Language Integrated Query) Zip'i de böyle çalışır. İkincisi robust iterator: dolaşma sırasında collection değişirse ne olacak?

List<Book> cart = all.Take(4).ToList();
try
{
    foreach (Book b in cart)
        if (b.Price < 15m)
            cart.Remove(b);
}
catch (InvalidOperationException ex)
{
    Console.WriteLine(ex.Message);
}
cart.RemoveAll(b => b.Price < 15m);
Console.WriteLine(string.Join(", ", cart.Select(b => b.Title)));
Collection was modified; enumeration operation may not execute.
Dune, The Hobbit, Introduction to Algorithms

List<T> her değişiklikte bir version sayacını artırır, enumerator de her MoveNext()'te bu sayacı kontrol eder. .NET'in tercihi "robust" olmak değil, hızlı ve gürültülü bozulmak: değişikliği fark ettiği anda exception fırlatır. Bizim Bookshelf'imizde ise böyle bir kontrol yok; array değişirse iterator sessizce yeni değeri okur. Kendi iterator'ını yazıyorsan bu kararı da sen veriyorsun.

IAsyncEnumerable ile Sayfalı API Okumak

Gerçek bir katalog API'si network üzerinden cevap verir, yani sayfa çekmek async bir işlemdir. C# 8'den beri iterator'ın async hâli de dilin içinde:

public static async IAsyncEnumerable<Book> ReadAllAsync(
    AsyncCatalogApi api, [EnumeratorCancellation] CancellationToken ct = default)
{
    for (int page = 1; ; page++)
    {
        IReadOnlyList<Book> items = await api.GetPageAsync(page, ct);
        foreach (Book book in items)
            yield return book;
        if (items.Count < api.PageSize)
            yield break;
    }
}

AsyncCatalogApi api = new(all, pageSize: 3);
await foreach (Book b in CatalogReader.ReadAllAsync(api).Where(b => b.Category == "IT").Take(2))
    Console.WriteLine($"{b.Title} ({b.Price})");
Console.WriteLine($"API calls: {api.Calls}");

using CancellationTokenSource cts = new();
try
{
    await foreach (Book b in CatalogReader.ReadAllAsync(api).WithCancellation(cts.Token))
    {
        Console.WriteLine(b.Title);
        if (b.Title == "Emma")
            cts.Cancel();
    }
}
catch (OperationCanceledException)
{
    Console.WriteLine("Cancelled");
}
  GET /books?page=1&size=3
  GET /books?page=2&size=3
Introduction to Algorithms (95)
Clean Code (42)
API calls: 2
  GET /books?page=1&size=3
Dune
Emma
The Hobbit
Cancelled

.NET 10'da IAsyncEnumerable<T> için LINQ operatörleri framework'ün içinde geliyor; Where ve Take için ek paket kurmadık. İki ilk kitap ikinci sayfada bulununca iterator durdu ve üçüncü sayfa istenmedi. İkinci döngüde dikkat edilecek bir ayrıntı var: iptali "Emma"da istedik ama "The Hobbit" yine yazıldı. Token yalnız await noktalarında, yani bir sonraki GetPageAsync çağrısında kontrol ediliyor; elimizdeki sayfanın kalan element'leri yine dönüyor. İptalin anında olması gerekiyorsa yield return'den önce ct.ThrowIfCancellationRequested() çağırmak gerekir. [EnumeratorCancellation] attribute'u ve await foreach'in perde arkası C# Async serisindeki "C# IAsyncEnumerable, await foreach ve Async Dispose" yazısında ayrıntılı anlatılıyor.

Kendi Iterator'ını Yazmanın Tek Gerekçesi

Bookstore'da sık görülen hata iterator'ı az değil, fazla yazmak. Elinde zaten bir List<OrderItem> varken onu saran bir OrderItemIterator yazmak, pattern'in hiçbir faydasını getirmez. Order class'ının problemi çoğu zaman tersidir: içerideki listeyi olduğu gibi dışarı açar.

// Kötü: temsil dışarı sızıyor, herkes listeyi değiştirebilir
public sealed class Order
{
    public List<OrderItem> Items { get; } = [];
}

// İyi: dışarıya yalnız okunabilir bir görünüm, değişiklik Order'ın method'larından
public sealed class Order
{
    private readonly List<OrderItem> _items = [];
    public IReadOnlyList<OrderItem> Items => _items;

    public void AddItem(OrderItem item) => _items.Add(item);
}

Iterator pattern'in gerçek gerekçesi, dolaşmanın kendisinin bir bilgi içermesi: sayfalama, bir ağacı depth-first gezmek, bir dosyayı satır satır okumak, sonsuz ya da pahalı bir sequence'ı ihtiyaç kadar üretmek. Bu durumlarda yield return ile yazılmış birkaç satırlık bir method, çağıranı o detaydan tamamen kurtarır. Collection zaten bellekteyse doğru araç iterator yazmak değil, doğru interface'i seçmek: IReadOnlyList<T>, IReadOnlyCollection<T> ya da IEnumerable<T>. Bu seçimin ayrıntıları Koleksiyonlar serisindeki "C# Collection Interface'leri: IEnumerable, ICollection, IList ve yield return" yazısında.

Interpreter: Küçük Bir Kural Dili

İkinci pattern, C#'ın içine başka bir yoldan gömülü. GoF'a göre Interpreter'ın niyeti şu: bir dil verildiğinde onun gramerini bir temsil olarak tanımlamak ve bu temsili kullanarak o dildeki cümleleri yorumlayan bir interpreter yazmak. Roller: AbstractExpression (ortak Interpret operasyonu), TerminalExpression (gramerin yaprakları), NonterminalExpression (kuralları birleştiren düğümler) ve Context (yorumlama sırasında gereken bilgi). Kısacası bir AST (Abstract Syntax Tree) ve her düğümün kendini değerlendirmesi.

Bookstore'un pazarlama ekibi kampanya kurallarını kod değiştirmeden tanımlamak istiyor: "total > 100 AND (category = 'IT' OR items >= 3)" gibi. Gramer küçük ve sabit, tam Interpreter'ın sevdiği türden. Önce AST:

// Context: kuralın değerlendirildiği sipariş bilgisi
public sealed record OrderFacts(decimal Total, string Category, int Items);

// AbstractExpression
public abstract record Rule
{
    public abstract bool Evaluate(OrderFacts order);
}

// TerminalExpression
public sealed record Comparison(string Field, string Operator, object Value) : Rule
{
    public override bool Evaluate(OrderFacts order) => (Field, Value) switch
    {
        ("total", decimal v) => Compare(order.Total.CompareTo(v)),
        ("items", decimal v) => Compare(((decimal)order.Items).CompareTo(v)),
        ("category", string v) => Compare(string.CompareOrdinal(order.Category, v)),
        _ => throw new InvalidOperationException($"Cannot compare {Field} with {Value}"),
    };

    private bool Compare(int c) => Operator switch
    {
        "=" => c == 0,
        "!=" => c != 0,
        ">" => c > 0,
        ">=" => c >= 0,
        "<" => c < 0,
        "<=" => c <= 0,
        _ => throw new InvalidOperationException($"Unknown operator {Operator}"),
    };
}

// NonterminalExpression'lar
public sealed record And(Rule Left, Rule Right) : Rule
{
    public override bool Evaluate(OrderFacts order) => Left.Evaluate(order) && Right.Evaluate(order);
}

public sealed record Or(Rule Left, Rule Right) : Rule
{
    public override bool Evaluate(OrderFacts order) => Left.Evaluate(order) || Right.Evaluate(order);
}

GoF, metni ağaca çeviren parser'ı pattern'in parçası saymaz; Interpreter ağacın elinde olduğunu varsayar. Biz yine de küçük bir recursive descent parser yazdık, çünkü güvenlik kararlarının çoğu orada veriliyor:

// Gramer:  rule := and ("OR" and)* ;  and := primary ("AND" primary)* ;
//          primary := "(" rule ")" | field op literal
private Rule ParsePrimary()
{
    if (Peek == "(")
    {
        Next();
        Rule inner = ParseOr();
        if (Next() != ")")
            throw new RuleParseException("Expected ')'");
        return inner;
    }

    string field = Next().ToLowerInvariant();
    if (!Fields.Contains(field))
        throw new RuleParseException($"Unknown field '{field}'");
    string op = Next();
    if (!Operators.Contains(op))
        throw new RuleParseException($"Unknown operator '{op}'");
    string literal = Next();
    object value = literal.StartsWith('\'')
        ? literal.Trim('\'')
        : decimal.TryParse(literal, System.Globalization.CultureInfo.InvariantCulture, out decimal d)
            ? d
            : throw new RuleParseException($"Bad literal '{literal}'");
    return new Comparison(field, op, value);
}

ParseOr ve ParseAnd aynı kalıpta birer while döngüsü; AND'in OR'dan önce bağlanması gramerin bu iki seviyesinden geliyor. Kuralı ayrıştırıp dört siparişe uygulayalım:

Rule rule = RuleParser.Parse("total > 100 AND (category = 'IT' OR items >= 3)");
Console.WriteLine(rule);

OrderFacts[] orders =
[
    new(Total: 150m, Category: "IT", Items: 1),
    new(Total: 120m, Category: "Fiction", Items: 4),
    new(Total: 120m, Category: "Fiction", Items: 2),
    new(Total: 80m, Category: "IT", Items: 5),
];
foreach (OrderFacts o in orders)
    Console.WriteLine($"{o} -> {rule.Evaluate(o)}");
And { Left = Comparison { Field = total, Operator = >, Value = 100 }, Right = Or { Left = Comparison { Field = category, Operator = =, Value = IT }, Right = Comparison { Field = items, Operator = >=, Value = 3 } } }
OrderFacts { Total = 150, Category = IT, Items = 1 } -> True
OrderFacts { Total = 120, Category = Fiction, Items = 4 } -> True
OrderFacts { Total = 120, Category = Fiction, Items = 2 } -> False
OrderFacts { Total = 80, Category = IT, Items = 5 } -> False

Record'ların derleyicinin ürettiği ToString()'i AST'yi olduğu gibi gösteriyor: kökte bir And, sağ kolunda bir Or. Parantez metinde kayboldu ama ağacın şeklinde yaşıyor.

Expression Tree: Yorumlamak Yerine Derlemek

Interpreter'ın bilinen zayıflığı hız: her değerlendirmede ağaç baştan gezilir, her düğümde virtual çağrı ve switch yapılır. Aynı AST'yi bir kez Expression<Func<OrderFacts, bool>>'a çevirip derlersen kural, elle yazılmış bir lambda kadar hızlı çalışan bir delegate'e dönüşür:

// Aynı AST'yi yorumlamak yerine bir expression tree'ye çevirip derliyoruz
private static Expression Build(Rule rule, ParameterExpression o) => rule switch
{
    And a => Expression.AndAlso(Build(a.Left, o), Build(a.Right, o)),
    Or x => Expression.OrElse(Build(x.Left, o), Build(x.Right, o)),
    Comparison c => BuildComparison(c, o),
    _ => throw new NotSupportedException(rule.GetType().Name),
};

Expression<Func<OrderFacts, bool>> lambda = RuleCompiler.ToExpression(rule);
Console.WriteLine(lambda);
Func<OrderFacts, bool> compiled = lambda.Compile();
Console.WriteLine(string.Join(", ", orders.Select(compiled)));
o => ((o.Total > 100) AndAlso ((o.Category == "IT") OrElse (Convert(o.Items, Decimal) >= 3)))
True, True, False, False

Sonuçlar interpreter'la birebir aynı. Burada ilginç bir şey var: System.Linq.Expressions'ın kendisi de bir Interpreter. Expression abstract base class, ConstantExpression ve ParameterExpression terminal, BinaryExpression nonterminal düğümler. Üstelik aynı ağaç iki şekilde yorumlanabiliyor: Compile() onu IL'e (Intermediate Language) çeviriyor, Entity Framework Core (EF Core) ise IQueryable<T>.Where'a verildiğinde aynı ağacı SQL'e (Structured Query Language) çeviriyor. Kampanya kurallarını veritabanında filtre olarak çalıştırmak istediğinde AST'den expression tree'ye geçiş bu yüzden değerli. Expression tree'nin kendisi C# İleri serisindeki "C#'ta Expression Tree: Lambda'yı Veri Olarak Tutmak ve EF Core'a Köprü" yazısında.

Regex de Bir Interpreter

Her gün kullandığın bir interpreter daha var. Regex, verdiğin pattern'i önce bir ağaca ayrıştırır; sonra o ağacı ya yorumlar ya da koda çevirir. Hangisini yaptığını reflection ile içerideki factory field'ına bakarak görebiliriz (internal bir detay, yalnız merak için):

FieldInfo factory = typeof(Regex).GetField("factory", BindingFlags.NonPublic | BindingFlags.Instance)!;
Regex interpreted = new(@"^\d{3}-\d{10}$");
Regex compiled = new(@"^\d{3}-\d{10}$", RegexOptions.Compiled);
Regex generated = Patterns.Isbn();
Console.WriteLine(factory.GetValue(interpreted)?.GetType().Name);
Console.WriteLine(factory.GetValue(compiled)?.GetType().Name);
Console.WriteLine(factory.GetValue(generated)?.GetType().Name);

static partial class Patterns
{
    [GeneratedRegex(@"^\d{3}-\d{10}$")]
    public static partial Regex Isbn();
}
RegexInterpreterFactory
CompiledRegexRunnerFactory
RunnerFactory

Varsayılan Regex ağacı yorumlayan RegexInterpreter ile çalışıyor; bu tam olarak GoF'un Interpreter'ı. RegexOptions.Compiled aynı ağacı runtime'da IL'e çeviriyor, [GeneratedRegex] source generator'ı ise derleme sırasında okunabilir C# kodu üretiyor. Bizim kural dilimizde yaptığımız "önce yorumla, gerekirse derle" yolculuğu, .NET ekibinin Regex için yaptığıyla aynı.

Sık Düşülen Tuzaklar

Büyük gramer için Interpreter yazmak. GoF da pattern'in basit gramerler için uygun olduğunu, çok kurallı gramerlerde class hiyerarşisinin yönetilemez hâle geldiğini söyler. Kural dilin fonksiyon çağrıları, değişkenler ve öncelik seviyeleriyle büyüyorsa elle parser yazmayı bırak; ANTLR (ANother Tool for Language Recognition) gibi bir parser generator'a ya da bir parser combinator library'sine geç. Daha da iyisi, gerçekten yeni bir dile ihtiyaç var mı diye sor: çoğu zaman bir JSON (JavaScript Object Notation) kural tanımı ya da bir form ekranı yeterli.

Kullanıcı girdisini doğrudan dinamik expression'a çevirmek. Bizim parser'ımız yalnız üç field adını ve altı operatörü tanıyor; listede olmayan her şey reddediliyor:

total > 100 AND password = 'x' -> Unknown field 'password'
total >> 100 -> Bad literal '>'
total > 100 OR -> Unexpected end of rule

Metinden reflection ile property ve method adı çözen genel amaçlı çözümler bu sınırı koymazsa ciddi bir açık doğar. System.Linq.Dynamic.Core library'sinin 1.3.0 öncesi sürümlerinde, Where ya da OrderBy'a verilen güvenilmeyen bir string'in object tipleri üzerinden method çağırıp kod çalıştırabildiği bir açık bulundu; CVE (Common Vulnerabilities and Exposures) kaydı CVE-2023-32571, CVSS (Common Vulnerability Scoring System) puanı 9.8. Kural: kullanıcının yazdığı metin hiçbir zaman doğrudan bir tip ya da member adına dönüşmemeli; izin listesi (allowlist) sende olmalı. Injection'ın genel çerçevesi OWASP (Open Worldwide Application Security Project) Top 10 serisindeki "OWASP A05: Injection (1) — SQL Injection ve Command Injection" yazısında.

Iterator'ı iki kez dolaşmak. Açılıştaki örnekte Count() bütün sayfaları yeniden çekti. Lazy bir sequence'ı birden fazla kez kullanacaksan bir kez ToList() ile materialize et; her dolaşmanın bir maliyeti olduğunu, iterator'ı yazanın değil kullananın unuttuğunu bil. Bu konu LINQ serisindeki "LINQ Deferred Execution: Sorgu Ne Zaman Çalışır? IEnumerable ve IQueryable" yazısında ölçülerek anlatılıyor.

Parametre doğrulamasını iterator'ın içine koymak. yield return içeren bir method'un gövdesi ilk MoveNext()'e kadar hiç çalışmaz; ArgumentNullException da o zamana kadar gelmez. Doğrulamayı lazy kısımdan ayırma tekniği LINQ serisindeki "LINQ'te Kendi Operatörünü Yazmak: Extension Method ve yield return" yazısında.

Ne Zaman Kullanma

Iterator için: elindeki şey zaten bellekte bir collection ise kendi iterator class'ını yazma, doğru readonly interface'i döndür. Kopyası çıkarılmış küçük bir listeyi dolaşmak için yield kullanmak da gereksiz; return _items; yeter. Interpreter için: kural sayısı az ve nadiren değişiyorsa, kuralları düz C# kodu ya da birkaç Func<OrderFacts, bool> olarak yazmak, bir dil tasarlayıp parser, hata mesajı, sürümleme ve güvenlik yükünü taşımaktan çok daha ucuz. Interpreter ancak kuralları geliştirici olmayan biri yazacaksa ve bu kurallar deploy beklemeden değişecekse kendini öder. Ondan önce, kural birleştirmenin daha hafif bir yolu olan Specification pattern'ini düşün; o serinin "GoF Dışında Bilmen Gereken Kod Seviyesi Pattern'ler: Null Object, Specification, Result ve Pipeline" yazısında.

Küçük Bir Deneme

★☆☆ Açılıştaki örnekte books'u ToList() ile materialize et ve API çağrısı sayısının kaça düştüğünü gör. Aynı değişiklik FirstOrDefault'un ilk sayfada durma avantajını neden kaybettiriyor?

★★☆ Kategori ağacını (Fiction → Fantasy → Epic Fantasy gibi) depth-first dolaşan bir IEnumerable<Category> Descendants() iterator'ı yaz. Recursive yield kullanan ilk sürümün her seviyede yeni bir iterator object'i ürettiğini fark et ve açık bir Stack<T> ile ikinci bir sürüm yaz.

★★★ Kural diline NOT operatörünü ve category IN ('IT', 'Fiction') syntax'ını ekle. Yeni düğümler için hem Evaluate'i hem RuleCompiler'ı genişlet; iki yolun her sipariş için aynı sonucu verdiğini kontrol eden küçük bir döngü yaz.

Bu yazıdaki iki pattern de bir yapının içini dışarıdan gizliyordu. Bir sonraki yazı tersine, birbirini fazla iyi tanıyan object'lerle ilgileniyor: ödeme ekranındaki kupon, kargo ve toplam tutar component'leri birbirini doğrudan çağırdığında ne olur, GoF'un Mediator'ı bunu nasıl çözer ve MediatR gerçekten bu pattern mi?