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

Strategy Pattern Derinlemesine: Keyed Services, IEnumerable<IStrategy> ve Func ile Runtime Seçim

Mert Özen 7 Kas 2026 12 dk 3 görüntülenme
Strategy Pattern Derinlemesine: Keyed Services, IEnumerable<IStrategy> ve Func ile Runtime Seçim

Strategy'yi yazmak kolay, zor olan doğru strategy'yi runtime'da seçmek: keyed services tek bir key'le, IEnumerable<IStrategy> ve CanHandle object'in birden fazla özelliğine bakarak, Dictionary<string, Func<>> ise bağımlılığı olmayan formüllerde en az törenle seçer. Kayıt sırasının sessizce fiyat değiştirmesi, keyed kayıtların GetServices'te görünmemesi ve BCL'deki IComparer ile StringComparer örnekleri.

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

Önce Tahmin Et

Bookstore'un ödeme ekranında üç kargo seçeneği var: standart posta, ekspres kurye ve mağazadan teslim. Her biri için bir strategy class'ı yazılmış, üçü de DI (Dependency Injection) container'a aynı interface ile kaydedilmiş. Fiyat teklifini veren service ise constructor'dan tek bir IShippingRateStrategy istiyor:

using Bookstore.Shipping;
using Microsoft.Extensions.DependencyInjection;

ServiceCollection services = new();
services.AddSingleton<IShippingRateStrategy, StandardPostRate>();
services.AddSingleton<IShippingRateStrategy, ExpressCourierRate>();
services.AddSingleton<IShippingRateStrategy, StorePickupRate>();
services.AddSingleton<ShippingQuoteService>();

using ServiceProvider provider = services.BuildServiceProvider();
ShippingQuoteService quotes = provider.GetRequiredService<ShippingQuoteService>();

Shipment shipment = new("standard-post", "TR-34", 1.2m);
Console.WriteLine(quotes.Quote(shipment));
Console.WriteLine(provider.GetServices<IShippingRateStrategy>().Count());

public sealed class ShippingQuoteService(IShippingRateStrategy strategy)
{
    public string Quote(Shipment shipment) =>
        $"{strategy.Carrier}: {strategy.Calculate(shipment):0.00}";
}

Müşteri standart postayı seçti, paket 1,2 kilo. Ekrana ne yazar? İkinci satır kaç basar? Cevabı okumadan önce bir tahminde bulun; strategy'lerin kendisi şöyle:

namespace Bookstore.Shipping;

public sealed record Shipment(string Carrier, string Region, decimal WeightKg);

public interface IShippingRateStrategy
{
    string Carrier { get; }
    decimal Calculate(Shipment shipment);
}

public sealed class StandardPostRate : IShippingRateStrategy
{
    public string Carrier => "standard-post";
    public decimal Calculate(Shipment shipment) => 2.90m + 0.80m * shipment.WeightKg;
}

public sealed class ExpressCourierRate : IShippingRateStrategy
{
    public string Carrier => "express-courier";
    public decimal Calculate(Shipment shipment) =>
        6.50m + 1.50m * shipment.WeightKg + (shipment.Region.StartsWith("TR-") ? 0m : 4.00m);
}

public sealed class StorePickupRate : IShippingRateStrategy
{
    public string Carrier => "store-pickup";
    public decimal Calculate(Shipment shipment) => 0m;
}

Tanım ve Naif switch

Çıktı:

store-pickup: 0.00
3

Müşteri standart posta seçti, sistem mağazadan teslim fiyatı verdi: sıfır. Container üç kaydı da tutuyor (ikinci satır 3), ama tek bir IShippingRateStrategy istendiğinde son kaydı döndürüyor. Hata vermiyor, uyarmıyor; kayıt sırasını değiştiren masum bir pull request kargo ücretlerini sessizce değiştiriyor. Bu yazının asıl konusu tam olarak bu: strategy'leri yazmak kolay, zor olan doğru strategy'yi runtime'da seçmek.

GoF (Gang of Four: Gamma, Helm, Johnson ve Vlissides'in 1994 tarihli Design Patterns kitabı) Strategy'nin amacını şöyle koyar: bir algoritma ailesi tanımla, her birini encapsulate et ve birbirinin yerine kullanılabilir kıl; algoritma, onu kullanan client'tan bağımsız değişebilsin. Kitaptaki rollerin adları context (strategy'yi kullanan), strategy (ortak interface) ve concrete strategy (her bir algoritma). Bizde context ShippingQuoteService, strategy IShippingRateStrategy, concrete strategy'ler de üç rate class'ı.

C# OOP (Object-Oriented Programming) serisindeki "C#'ta Birkaç Design Pattern: Factory, Strategy ve Observer" yazısında ülkeye göre kargo hesabını bir factory ve strategy ile kurmuştuk; orada seçim tek bir switch expression'daydı. Bu yazı oradan devam ediyor: strategy'nin kendisini değil, seçimin nerede ve nasıl yapıldığını konuşacağız. Başlangıç noktası, pattern'den önceki hâli:

using Bookstore.Shipping;

Console.WriteLine($"{NaiveShippingCalculator.Calculate(new("express-courier", "DE-BE", 1.2m)):0.00}");

public static class NaiveShippingCalculator
{
    public static decimal Calculate(Shipment shipment)
    {
        switch (shipment.Carrier)
        {
            case "standard-post":
                return 2.90m + 0.80m * shipment.WeightKg;
            case "express-courier":
                decimal cost = 6.50m + 1.50m * shipment.WeightKg;
                if (!shipment.Region.StartsWith("TR-"))
                {
                    cost += 4.00m;
                }
                return cost;
            case "store-pickup":
                return 0m;
            default:
                throw new ArgumentException($"Unknown carrier: {shipment.Carrier}");
        }
    }
}
12.30

Kod çalışıyor ve üç firma için gayet okunaklı. Sorun büyüyünce başlıyor. Dördüncü firma geldiğinde bu method'a bir case daha eklenir; firmalardan biri hafta sonu teslimatı için ek ücret istediğinde bir if daha. Her değişiklikte aynı method'u açıyorsun, bütün firmaların test'leri aynı dosyaya bağlı ve ekspres kuryenin yurt dışı kuralını değiştiren kişi standart postanın satırlarına da dokunuyor. SOLID (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) ve Design Principles serisinin "Open/Closed Principle: Değiştirmeden Genişletmek ve Spekülatif Genellik Tuzağı" yazısında konuştuğumuz değişim noktası burada açıkça görünüyor: değişen şey firmanın fiyat formülü, değişmeyen şey teklif verme akışı.

Strategy bu iki şeyi ayırır. Her formül kendi class'ına taşınır, akış yalnız interface'i tanır. Ama formülleri ayırdığın anda yeni bir soru doğar: "standard-post" string'ini StandardPostRate object'ine kim çevirecek? Naif kodda switch bu işi de yapıyordu. Aşağıdaki üç yöntem, bu sorunun .NET'teki üç cevabı.

Seçim 1: Keyed Services

.NET 8 ile gelen keyed services, aynı interface'in birden fazla implementation'ını bir key ile kaydetmeyi sağlar. Key string olmak zorunda değil; Equals'ı doğru implement eden her object olabilir, enum da olur.

using Bookstore.Shipping;
using Microsoft.Extensions.DependencyInjection;

ServiceCollection services = new();
services.AddKeyedSingleton<IShippingRateStrategy, StandardPostRate>("standard-post");
services.AddKeyedSingleton<IShippingRateStrategy, ExpressCourierRate>("express-courier");
services.AddKeyedSingleton<IShippingRateStrategy, StorePickupRate>("store-pickup");
services.AddSingleton<ExpressCheckout>();
services.AddSingleton<ShippingRateSelector>();

using ServiceProvider provider = services.BuildServiceProvider();

// Key derleme zamanında belli: attribute yeterli
ExpressCheckout express = provider.GetRequiredService<ExpressCheckout>();
Console.WriteLine(express.Quote(new("express-courier", "DE-BE", 1.2m)));

// Key runtime'da, kullanıcının seçiminden geliyor
ShippingRateSelector selector = provider.GetRequiredService<ShippingRateSelector>();
Console.WriteLine($"{selector.For("standard-post").Calculate(new("standard-post", "TR-34", 1.2m)):0.00}");
Console.WriteLine(provider.GetServices<IShippingRateStrategy>().Count());

try
{
    selector.For("drone");
}
catch (InvalidOperationException ex)
{
    Console.WriteLine(ex.Message);
}

public sealed class ExpressCheckout(
    [FromKeyedServices("express-courier")] IShippingRateStrategy rate)
{
    public string Quote(Shipment shipment) => $"{rate.Carrier}: {rate.Calculate(shipment):0.00}";
}

// Container'a dokunan tek class; uygulamanın geri kalanı yalnız bunu görür
public sealed class ShippingRateSelector(IServiceProvider services)
{
    public IShippingRateStrategy For(string carrier) =>
        services.GetRequiredKeyedService<IShippingRateStrategy>(carrier);
}
express-courier: 12.30
3.86
0
No keyed service for type 'Bookstore.Shipping.IShippingRateStrategy' using key type 'System.String' has been registered.

İki farklı kullanım var ve aralarındaki fark önemli. ExpressCheckout her zaman ekspres kuryeyle çalışıyor; key derleme zamanında belli olduğu için [FromKeyedServices("express-courier")] attribute'u yeterli. Bu en temiz hâli: bağımlılık constructor'da açıkça görünüyor ve class container'dan habersiz.

Müşterinin seçtiği firma ise runtime'da belli oluyor. Attribute'a runtime değeri veremezsin, o yüzden birinin GetRequiredKeyedService'i çağırması gerekiyor. Bunu OrderService'in içinde yaparsan class'a IServiceProvider enjekte etmiş olursun; bu da Service Locator'ın ta kendisi, serinin anti-pattern yazısında ayrıntısıyla konuşacağız. Burada yaptığımız gibi, container'a dokunan tek ve küçük bir ShippingRateSelector yazmak, locator'ı composition root'un hemen yanına hapseder; uygulamanın geri kalanı yalnız For(carrier)'ı görür.

Çıktının son iki satırı iki tuzağı gösteriyor. Birincisi, keyed kayıtlar key'siz GetServices<T>() sorgusunda görünmez: sonuç 0. Keyed kaydettiğin strategy'leri bir de IEnumerable<T> ile almayı planlıyorsan bunu bilmen gerekiyor. Tümünü istiyorsan GetKeyedServices<T>(KeyedService.AnyKey) var; Microsoft Learn'deki dependency injection sayfasına göre .NET 10'dan itibaren tekil GetKeyedService'e AnyKey vermek InvalidOperationException fırlatıyor. İkincisi, kayıtlı olmayan key için gelen exception mesajı key'in değerini değil, yalnız tipini söylüyor: "drone" kelimesi mesajda yok. Loglarda hangi firmanın eksik olduğunu bilmek istiyorsan selector'da kendin kontrol edip anlamlı bir mesaj üretmelisin.

Seçim 2: IEnumerable<IStrategy> ve CanHandle

Bazen seçim tek bir key'e bağlı değildir. Bölgesel fiyatlandırmada kural şöyle olsun: 30 kilodan ağır paketler navlun tarifesine girer, Gökçeada ve Bozcaada'ya giden paketler ada tarifesine, geri kalan her şey düz tarifeye. Burada her strategy kendisinin uygun olup olmadığına karar verir:

using Bookstore.Shipping;
using Microsoft.Extensions.DependencyInjection;

Shipment heavy = new("standard-post", "TR-34", 32m);

Console.WriteLine(Quote(registerFallbackFirst: false, heavy));
Console.WriteLine(Quote(registerFallbackFirst: true, heavy));

static string Quote(bool registerFallbackFirst, Shipment shipment)
{
    ServiceCollection services = new();
    if (registerFallbackFirst)
    {
        services.AddSingleton<IRegionalRate, FlatRegionalRate>();
    }
    services.AddSingleton<IRegionalRate, FreightRate>();
    services.AddSingleton<IRegionalRate, IslandRate>();
    if (!registerFallbackFirst)
    {
        services.AddSingleton<IRegionalRate, FlatRegionalRate>();
    }
    services.AddSingleton<RegionalQuoteService>();

    using ServiceProvider provider = services.BuildServiceProvider();
    return provider.GetRequiredService<RegionalQuoteService>().Quote(shipment);
}

public interface IRegionalRate
{
    bool CanHandle(Shipment shipment);
    decimal Calculate(Shipment shipment);
}

public sealed class FreightRate : IRegionalRate
{
    public bool CanHandle(Shipment shipment) => shipment.WeightKg > 30m;
    public decimal Calculate(Shipment shipment) => 45m + 0.50m * shipment.WeightKg;
}

public sealed class IslandRate : IRegionalRate
{
    private static readonly HashSet<string> Islands = ["TR-17-GOKCEADA", "TR-17-BOZCAADA"];
    public bool CanHandle(Shipment shipment) => Islands.Contains(shipment.Region);
    public decimal Calculate(Shipment shipment) => 9.90m + 1.10m * shipment.WeightKg;
}

public sealed class FlatRegionalRate : IRegionalRate
{
    public bool CanHandle(Shipment shipment) => true;
    public decimal Calculate(Shipment shipment) => 2.90m + 0.80m * shipment.WeightKg;
}

public sealed class RegionalQuoteService(IEnumerable<IRegionalRate> rates)
{
    public string Quote(Shipment shipment)
    {
        IRegionalRate rate = rates.First(r => r.CanHandle(shipment));
        return $"{rate.GetType().Name}: {rate.Calculate(shipment):0.00}";
    }
}
FreightRate: 61.00
FlatRegionalRate: 28.50

Aynı 32 kiloluk paket, aynı kodla iki farklı fiyat aldı. Fark yalnızca kayıt sırası. Microsoft'un DI container'ı IEnumerable<T>'yi kayıt sırasıyla doldurur ve First ilk uyan strategy'yi seçer. FlatRegionalRate'in CanHandle'ı her zaman true döndüğü için en başa kaydedildiğinde bütün özel kuralları yutuyor. Bu yaklaşım bir Chain of Responsibility'ye çok benzer ve aynı hastalığı taşır: sıraya gizli bir bağımlılık.

Bunu koda dökmenin iki yolu var. Fallback'i IRegionalRate listesinden çıkarıp RegionalQuoteService'in içinde açıkça kullanmak (rates.FirstOrDefault(...) ?? fallback) ya da her strategy'ye bir Priority property'si verip listeyi ona göre sıralamak. İlki daha az sihirli. Hangisini seçersen seç, "hiçbiri uymazsa ne olacak" ve "ikisi birden uyarsa hangisi kazanacak" sorularının cevabı kodda yazılı olmalı; kayıt sırası bu cevabı taşımak için kırılgan bir yer.

Bu yöntemin güçlü yanı, yeni bir kural eklemenin mevcut koda hiç dokunmaması: yeni class, yeni kayıt, bitti. Zayıf yanı, bir paketin hangi tarifeye girdiğini anlamak için bütün CanHandle'ları okuman gerekmesi.

Seçim 3: Dictionary ve Func

Strategy tek bir method'dan ibaretse, bağımlılığı yoksa ve state tutmuyorsa interface ve class biraz tören olabilir. C#'ta fonksiyonlar değer olarak taşınabildiği için strategy bir Func'a, seçim de bir dictionary lookup'ına iner:

using System.Collections.Frozen;
using Bookstore.Shipping;

ShippingRateTable table = new();
Console.WriteLine($"{table.Calculate(new("store-pickup", "TR-06", 0.4m)):0.00}");
Console.WriteLine($"{table.Calculate(new("express-courier", "TR-06", 0.4m)):0.00}");
Console.WriteLine(table.TryCalculate(new("drone", "TR-06", 0.4m), out decimal cost) ? $"{cost:0.00}" : "not supported");

public sealed class ShippingRateTable
{
    private static readonly FrozenDictionary<string, Func<Shipment, decimal>> Rates =
        new Dictionary<string, Func<Shipment, decimal>>
        {
            ["standard-post"] = s => 2.90m + 0.80m * s.WeightKg,
            ["express-courier"] = s => 6.50m + 1.50m * s.WeightKg + (s.Region.StartsWith("TR-") ? 0m : 4.00m),
            ["store-pickup"] = _ => 0m,
        }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);

    public decimal Calculate(Shipment shipment) => Rates[shipment.Carrier](shipment);

    public bool TryCalculate(Shipment shipment, out decimal cost)
    {
        if (Rates.TryGetValue(shipment.Carrier, out Func<Shipment, decimal>? rate))
        {
            cost = rate(shipment);
            return true;
        }
        cost = 0m;
        return false;
    }
}
0.00
7.10
not supported

Peter Norvig'in 1996 tarihli "Design Patterns in Dynamic Languages" sunumundaki gözlem tam olarak bu: first-class function'ı olan dillerde Strategy ayrı bir yapı olmaktan çıkıp sıradan bir fonksiyon değerine dönüşür. C#'ta bu, delegate'ler ve lambda'larla yıllardır mümkün. FrozenDictionary .NET 8 ile geldi; bir kez kurulup sonra yalnız okunan tablolar için lookup'ı hızlandırır. StringComparer.OrdinalIgnoreCase ise key karşılaştırmasının kendisini bir strategy olarak dictionary'ye veriyor; bir sonraki bölümün konusu.

Bu tablo okunabilir ve tek bakışta bütün firmaları gösteriyor. Ama sınırı da belli: formüllerden biri bir veritabanı sorgusuna, bir dış API (Application Programming Interface) çağrısına ya da configuration'a ihtiyaç duyduğu an lambda'lar closure ile bağımlılık yakalamaya başlar ve test edilmeleri zorlaşır. O noktada interface'e geri dönmek bir geri adım değil, doğru araç.

YöntemSeçim neye göreYeni strategy eklemekDikkat
Keyed servicesTek bir keyYeni class ve yeni AddKeyed... satırıRuntime key için container'a erişim gerekir
IEnumerable<IStrategy> ve CanHandleObject'in birden fazla özelliğiYeni class ve yeni kayıtKayıt sırası sonucu değiştirir
Dictionary<string, Func<>>Tek bir keyTabloya bir satırBağımlılığı olan formülde dağılır

BCL'deki Strategy'ler: IComparer ve StringComparer

BCL (Base Class Library), yani .NET'in temel kütüphanesi, Strategy'yi en çok karşılaştırma ve eşitlikte kullanır. Array.Sort sıralama algoritmasını bilir ama iki elemanın hangisinin önce geleceğini bilmez; bunu bir IComparer<T>'den öğrenir. Dictionary ve HashSet de key eşitliğini bir IEqualityComparer<T>'den öğrenir. Türkçe kitap adlarında farkı görmek kolay:

using System.Globalization;

string[] titles = ["Zeytin", "Çalıkuşu", "Ince Memed", "İnce Memed", "Saatleri Ayarlama Enstitüsü"];

string[] ordinal = [.. titles];
Array.Sort(ordinal, StringComparer.Ordinal);
Console.WriteLine(string.Join(" | ", ordinal));

string[] turkish = [.. titles];
Array.Sort(turkish, StringComparer.Create(CultureInfo.GetCultureInfo("tr-TR"), ignoreCase: false));
Console.WriteLine(string.Join(" | ", turkish));

Dictionary<string, int> stock = new(StringComparer.OrdinalIgnoreCase) { ["ISBN-978-0441013593"] = 4 };
Console.WriteLine(stock["isbn-978-0441013593"]);
Ince Memed | Saatleri Ayarlama Enstitüsü | Zeytin | Çalıkuşu | İnce Memed
Çalıkuşu | Ince Memed | İnce Memed | Saatleri Ayarlama Enstitüsü | Zeytin
4

Aynı Array.Sort, iki farklı comparer ile iki farklı sıra üretti. Ordinal karakterlerin sayısal kod değerine bakar ve Ç ile İ'yi Z'den sonraya atar; tr-TR culture'ının comparer'ı Türk alfabesini bilir. Sıralama algoritması hiç değişmedi, yalnızca karşılaştırma strategy'si değişti. Son satırdaki stok sorgusu da büyük-küçük harfe duyarsız eşitliği aynı şekilde dışarıdan alıyor. Bu örneklerin ortak noktası, strategy'nin context'e constructor ya da parametre üzerinden açıkça verilmesi; hiçbir yerde gizli bir seçim yok. C# Koleksiyonlar serisindeki "C# Sıralı Collection'lar: SortedList, SortedDictionary, SortedSet ve IComparer" yazısı comparer yazmanın ayrıntılarını anlatıyor.

Strategy mi State mi?

Bir önceki yazıdaki State pattern ile Strategy'nin class diyagramı neredeyse aynıdır: bir context, bir interface, birkaç implementation. Fark niyette ve seçimi kimin yaptığında. Strategy'de seçimi dışarıdaki client yapar ve strategy'ler birbirini tanımaz; standart posta, ekspres kuryenin varlığından habersizdir. State'te ise state object'leri geçişleri bilir: Paid state'i, bir sonraki adımın Shipped olduğunu bilir ve context'in state'ini kendisi değiştirir.

Pratik bir test: "bu davranışı kim değiştiriyor?" sorusunun cevabı kullanıcı ya da configuration ise Strategy, object'in kendi yaşam döngüsündeki bir olay ise State. Kargo firması müşterinin seçimi, sipariş durumu ise siparişin hikâyesi.

Sık Düşülen Tuzaklar

Tek implementation için strategy yazmak. IShippingRateStrategy'nin tek implementation'ı varsa ve ikincisi için somut bir plan yoksa, interface sadece bir dolaylılık katmanı ekler. "İleride başka firma gelir" spekülatif genelliktir; ikinci firma geldiğinde interface'i çıkarmak birkaç dakikalık bir refactoring.

Seçimi her yere dağıtmak. Strategy'leri ayırıp sonra OrderService'te, CartController'da ve fatura job'unda ayrı ayrı if (carrier == "express-courier") yazmak, naif switch'i üç yere kopyalamak demek. Seçimin tek bir sahibi olmalı: selector, tablo ya da CanHandle zinciri.

Tek T ile çoklu kaydı karıştırmak. Önce Tahmin Et'teki hata gerçek projelerde sık görülür: bir library kendi varsayılan implementation'ını kaydeder, sen kendininkini eklersin ve hangisinin kazandığı kayıt sırasına kalır. Birden fazla implementation'ı olan bir interface'i tek T olarak istememek, istiyorsan keyed kayıt kullanmak bu belirsizliği kaldırır.

Strategy'ye context'in iç state'ini sızdırmak. Calculate(Order order) imzası strategy'ye bütün siparişi verir; birkaç ay sonra bir strategy siparişin müşteri notunu okuyup fiyatı ona göre değiştirmeye başlar. Strategy'ye yalnız ihtiyacı olanı veren dar bir tip (Shipment gibi) hem test'i kolaylaştırır hem de bu sızıntıyı engeller.

Bilinmeyen key'de sessizce varsayılana düşmek. Tablo ya da selector bilinmeyen firma için standart postaya düşerse yanlış fiyat kesilir ve kimse fark etmez. Bilinmeyen durum ya exception ile ya da TryCalculate gibi açık bir "desteklenmiyor" sonucuyla görünür olmalı.

Ne Zaman Kullanma

Algoritma değişmiyorsa ya da değişim yalnızca bir sayı ise. Kargo ücreti sadece "kilo başına X lira" ise X'i configuration'a koymak yeterli; strategy hiyerarşisi bir parametrenin yerini tutmaz.

Seçenekler sabit ve az ise. Üç durumu olan, yılda bir değişen bir kural için iyi isimlendirilmiş bir switch expression, üç class ve bir selector'dan daha kolay okunur. Strategy, değişim sıklaştığında ya da farklı ekipler farklı implementation'ların sahibi olduğunda kendini öder.

Seçim bir kez yapılıp hiç değişmiyorsa. Uygulama başlarken bir kez seçilen ve süreç boyunca değişmeyen bir davranış için runtime seçim mekanizması kurma; composition root'ta tek bir kayıt yeterli.

Küçük Bir Deneme

★☆☆ ShippingRateTable'a "same-day" diye yeni bir firma ekle: sabit 14,90 ve yalnızca TR-34 bölgesinde geçerli. Başka bölge için TryCalculate'in false dönmesini sağla. Bu kural lambda'ya mı, yoksa ayrı bir class'a mı daha iyi oturdu?

★★☆ RegionalQuoteService'i, fallback'i listeden ayrı bir constructor parametresi olarak alacak şekilde değiştir ve iki kayıt sırasında da aynı fiyatı verdiğini göster. Sonra iki strategy'nin aynı pakete uyduğu bir durum (32 kiloluk, Gökçeada'ya giden paket) için hangisinin kazanacağını açıkça belirleyen bir kural yaz.

★★★ ShippingRateSelector'ı IServiceProvider yerine, uygulama başlarken GetKeyedServices<IShippingRateStrategy>(KeyedService.AnyKey) ile bütün strategy'leri toplayıp Carrier property'sine göre bir FrozenDictionary'ye koyan bir class'a dönüştür. Aynı Carrier değerine sahip iki strategy kaydedilirse uygulama açılırken anlamlı bir hata versin.

Strategy'de davranışı bir object'e verip context'e taktık; inheritance'a hiç ihtiyaç duymadık. Bir sonraki yazı aynı soruyu tersinden soruyor: algoritmanın iskeleti sabit, yalnızca bazı adımları değişiyorsa, o adımları bir base class'ın abstract method'larına bırakmak. Template Method pattern'i, hook'ları ve aynı sonucu delegate'lerle almanın yolunu konuşacağız.