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

Anti-Pattern'ler: God Object, Service Locator, Singleton Abuse, Anemic Domain Model ve Pattern Hastalığı

Mert Özen 7 Kas 2026 14 dk 5 görüntülenme
Anti-Pattern'ler: God Object, Service Locator, Singleton Abuse, Anemic Domain Model ve Pattern Hastalığı

Anti-pattern, ilk bakışta makul görünen ama aynı sıkıntıyı tekrar tekrar üreten bir çözümdür ve tanımak, onu daha iyi bir yapıya taşıyan refactoring'i de bilmek demektir. Service Locator'ın ValidateOnBuild'den kaçırdığı bağımlılık, CA1506 ile ölçülen God Object, sıraya göre kırılan singleton test'leri, kuralı atlanan anemic model, Golden Hammer, Lava Flow, Poltergeist ve patternitis.

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

Önce Tahmin Et

Bookstore'da iki service var. CheckoutService bağımlılıklarını constructor'dan alıyor. Daha eski olan OrderService ise IServiceProvider alıyor ve ihtiyaç duyduğu şeyi kendisi istiyor. İki service'in de birer bağımlılığı kaydedilmeyi unutulmuş. Container ValidateOnBuild ile kuruluyor; bu seçenek, kayıtlı her service'in bağımlılıklarının çözülebildiğini build sırasında kontrol ediyor:

using Microsoft.Extensions.DependencyInjection;

ServiceCollection services = new();
services.AddScoped<IPaymentGateway, FakePaymentGateway>();
services.AddScoped<CheckoutService>();
services.AddScoped<OrderService>();
// IInvoiceWriter ve IOrderNotifier kaydedilmeyi unutuldu

ServiceProviderOptions options = new() { ValidateOnBuild = true, ValidateScopes = true };
try
{
    using ServiceProvider provider = services.BuildServiceProvider(options);
}
catch (AggregateException ex)
{
    foreach (Exception inner in ex.InnerExceptions)
    {
        Console.WriteLine(inner.Message);
    }
}

services.AddScoped<IInvoiceWriter, ConsoleInvoiceWriter>();
using (ServiceProvider provider = services.BuildServiceProvider(options))
{
    Console.WriteLine("Container built");
    using IServiceScope scope = provider.CreateScope();
    OrderService orders = scope.ServiceProvider.GetRequiredService<OrderService>();
    orders.Place(1042);
}

public interface IPaymentGateway { void Charge(int orderId); }
public interface IInvoiceWriter { void Write(int orderId); }
public interface IOrderNotifier { void OrderPlaced(int orderId); }

public sealed class FakePaymentGateway : IPaymentGateway
{
    public void Charge(int orderId) => Console.WriteLine($"Charged order {orderId}");
}

public sealed class ConsoleInvoiceWriter : IInvoiceWriter
{
    public void Write(int orderId) => Console.WriteLine($"Invoice for order {orderId}");
}

// Bağımlılıklar constructor'da: container bunları build sırasında görür
public sealed class CheckoutService(IPaymentGateway payments, IInvoiceWriter invoices)
{
    public void Complete(int orderId)
    {
        payments.Charge(orderId);
        invoices.Write(orderId);
    }
}

// Service Locator: bağımlılıklar method'un içinde saklı
public sealed class OrderService(IServiceProvider services)
{
    public void Place(int orderId)
    {
        services.GetRequiredService<IPaymentGateway>().Charge(orderId);
        services.GetRequiredService<IOrderNotifier>().OrderPlaced(orderId);
    }
}

İlk build kaç hata raporlar, ikisi mi biri mi? Eksik kaydı tamamlayıp ikinci kez build ettiğimizde program nereye kadar ilerler? Siparişin parası çekilir mi?

Anti-Pattern Nedir?

Çıktının ilk dört satırı:

Error while validating the service descriptor 'ServiceType: CheckoutService Lifetime: Scoped ImplementationType: CheckoutService': Unable to resolve service for type 'IInvoiceWriter' while attempting to activate 'CheckoutService'.
Container built
Charged order 1042
Unhandled exception. System.InvalidOperationException: No service for type 'IOrderNotifier' has been registered.

İlk build yalnızca CheckoutService'i şikâyet etti. OrderService'in eksik IOrderNotifier'ı hiçbir yerde görünmedi, çünkü container'ın gördüğü tek bağımlılık IServiceProvider, o da her zaman çözülebilir. İkinci build temiz geçti, uygulama ayağa kalktı, ilk sipariş geldi ve Place önce ödemeyi çekip sonra bildirim service'ini bulamayınca patladı. Müşterinin kartından para çekildi, sipariş onayı gitmedi. Hata, deploy'dan sonra ve gerçek bir müşterinin üzerinde ortaya çıktı.

Bu yazının konusu, bu tür "ilk bakışta makul görünen ama tekrar tekrar aynı sıkıntıyı üreten" çözümler. Anti-pattern terimini Andrew Koenig 1995'te JOOP'ta (Journal of Object-Oriented Programming) yayımlanan "Patterns and Antipatterns" yazısında kullandı; Martin Fowler'ın bliki'sindeki AntiPattern notu fikri, başta iyi görünüp sonradan başını derde sokan bir çözüm olarak özetliyor. Terimi yaygınlaştıran ise 1998 tarihli "AntiPatterns: Refactoring Software, Architectures, and Projects in Crisis" kitabı (William J. Brown, Raphael C. Malveau, Hays W. McCormick ve Thomas J. Mowbray). Kitabın şablonunda her anti-pattern'in iki parçası var: tekrar eden kötü çözüm ve onu daha iyi bir yapıya taşıyan refactoring. Bir anti-pattern'i tanımak, yalnız "bu kötü" demek değil; nasıl dönüştürüleceğini de bilmek demek. Aşağıdaki her başlık bu yüzden belirti, neden ve dönüşüm sırasıyla ilerliyor.

Service Locator: Bağımlılığı Saklamak

Belirti: class constructor'ında IServiceProvider (ya da kendi yazdığın bir ServiceLocator.Current) alıyor ve method'larının içinde GetRequiredService çağırıyor. Mark Seemann'ın 2010 tarihli "Service Locator is an Anti-Pattern" yazısının ana argümanı Önce Tahmin Et'te gördüğümüz şey: class'ın gerçek bağımlılıkları API'sinden (Application Programming Interface) okunamıyor. OrderService'i kullanan biri, onun bir IOrderNotifier'a ihtiyaç duyduğunu ancak kaynak kodunu okuyarak ya da production'da patlayarak öğreniyor. Container'ın build zamanı doğrulaması, test'teki new OrderService(...) çağrısı ve IDE'nin (Integrated Development Environment) "find usages"ı bu bağımlılığı göremiyor.

Neden ortaya çıkar: genellikle bir bağımlılık zinciri dairesel hâle geldiğinde ya da constructor'a "bir parametre daha" eklemek zahmetli göründüğünde. Bazen de container'ı ilk kez kullanan bir ekip onu global bir sözlük gibi görür.

Dönüşüm: GetRequiredService çağrılarını constructor parametrelerine çevir. Constructor'ın parametre sayısı rahatsız edici derecede büyüyorsa sorun locator'ın gizlediği şeydir: class çok fazla iş yapıyordur, bir sonraki başlığın konusu. SOLID (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) ve Design Principles serisinin "Dependency Inversion Principle: DIP, DI ve IoC Arasındaki Fark ve Composition Root" yazısı (DIP: Dependency Inversion Principle, DI: Dependency Injection, IoC: Inversion of Control) bu dönüşümü composition root fikriyle birlikte anlatıyor.

IServiceProvider'ı enjekte etmenin her hâli anti-pattern değil. Composition root'un kendisi, bir factory'nin runtime key ile seçim yaptığı küçük bir class (serinin Strategy yazısındaki ShippingRateSelector gibi), bir BackgroundService'in her iş için scope açması ve ASP.NET Core middleware'inin InvokeAsync parametreleri, container'a bilinçli ve sınırlı bir erişim. Ölçüt şu: locator'ı kullanan kod altyapının bir parçası mı, yoksa iş kuralının ortasında mı?

God Object

Belirti: adı Manager, Helper, Processor ya da projenin kendisi olan, binlerce satırlık, herkesin her şey için açtığı bir class. AntiPatterns kitabı buna The Blob der. Bookstore'da şöyle başlar:

namespace Bookstore;

// Sipariş, fatura, ödeme, kargo, stok, bildirim, yorum ve audit: hepsi tek class'ta
public sealed class BookstoreManager
{
    public Order PlaceOrder(Customer customer, Book book) => new();
    public Invoice IssueInvoice(Order order) => new();
    public Payment Charge(Invoice invoice, Coupon? coupon) => new();
    public Shipment Ship(Order order, Warehouse warehouse) => new();
    public StockItem Reserve(Book book, Warehouse warehouse) => new();
    public EmailMessage ConfirmByEmail(Customer customer, Order order) => new();
    public SmsMessage ConfirmBySms(Customer customer, Shipment shipment) => new();
    public Review ModerateReview(Review review, Author author) => review;
    public AuditEntry Audit(OrderLine line) => new();
}

Belirtiyi hisle değil sayıyla görmek için CA1506'yı (Avoid excessive class coupling) kullanabilirsin. Kural bir tipin kaç farklı tipe referans verdiğini sayar; varsayılan eşiği tipler için 95 ve .NET 10'da varsayılan olarak kapalı. Burada .editorconfig ile açıp CodeMetricsConfig.txt dosyasında eşiği CA1506(Type): 10 yaptık:

Model.cs(4,21): warning CA1506: 'BookstoreManager' is coupled with '15' different types from '1' different namespaces. Rewrite or refactor the code to decrease its class coupling below '11'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1506)

On beş tip, dokuz method. Gerçek projelerde bu sayı kolayca 95'i geçer. Neden ortaya çıkar: yeni bir özellik geldiğinde "zaten siparişle ilgili her şey burada" diye en yakın büyük class'a eklenir; her ekleme tek başına makul görünür. Bedeli, class'a dokunan her değişikliğin birbirinden alakasız özellikleri riske atması ve test'in on beş bağımlılığın sahtesini kurmayı gerektirmesi.

Dönüşüm: method'ları kimin için değiştiklerine göre grupla. Sipariş ve stok (operasyon ekibi), fatura ve ödeme (finans), e-posta ve SMS (Short Message Service) bildirimleri (pazarlama), yorum moderasyonu (içerik ekibi) dört ayrı actor; SOLID serisinin "Single Responsibility Principle: 'Tek İş' Değil, Tek Actor" yazısı bu ayrımı anlatıyor. Bir seferde değil, her değişiklikte bir grubu yeni bir class'a taşıyarak ilerlemek daha güvenli; Clean Code serisinin "Code Smell Kataloğu: C# Kodunda Kokuyu Tanımak" yazısındaki Large Class ve Extract Class bu işin mekaniği.

Singleton Abuse

Belirti: değiştirilebilir state tutan bir static instance ve onu method'ların içinden okuyan kod. Hiçbir constructor ya da method imzası bu bağımlılığı göstermez:

Console.WriteLine("Order A then B:");
Test.CampaignAppliesDiscount();
Test.NoCampaignMeansFullPrice();

DiscountRegistry.Reset();
Console.WriteLine("Order B then A:");
Test.NoCampaignMeansFullPrice();
Test.CampaignAppliesDiscount();

public static class Test
{
    public static void CampaignAppliesDiscount()
    {
        DiscountRegistry.Instance.Set("BLACKFRIDAY", 0.30m);
        decimal price = new PriceCalculator().PriceOf(100m);
        Console.WriteLine($"  CampaignAppliesDiscount: {(price == 70m ? "passed" : $"FAILED ({price})")}");
    }

    public static void NoCampaignMeansFullPrice()
    {
        decimal price = new PriceCalculator().PriceOf(100m);
        Console.WriteLine($"  NoCampaignMeansFullPrice: {(price == 100m ? "passed" : $"FAILED ({price})")}");
    }
}

// Global ve değiştirilebilir: her yerden yazılır, hiçbir imzada görünmez
public sealed class DiscountRegistry
{
    public static DiscountRegistry Instance { get; private set; } = new();
    private readonly Dictionary<string, decimal> rates = [];

    public void Set(string campaign, decimal rate) => rates[campaign] = rate;
    public decimal TotalRate => rates.Values.Sum();

    public static void Reset() => Instance = new();
}

public sealed class PriceCalculator
{
    public decimal PriceOf(decimal listPrice) => listPrice * (1 - DiscountRegistry.Instance.TotalRate);
}
Order A then B:
  CampaignAppliesDiscount: passed
  NoCampaignMeansFullPrice: FAILED (70.00)
Order B then A:
  NoCampaignMeansFullPrice: passed
  CampaignAppliesDiscount: passed

Aynı iki test, sıraya göre geçiyor ya da kırılıyor. İlk test kampanya oranını global registry'ye yazdı ve geri almadı; ikinci test o oranı miras aldı. Test runner'lar test'leri paralel ve belirsiz sırayla çalıştırdığında bu, "bazen kırmızı" bir CI (Continuous Integration) hattı demek. Aynı paylaşımın production'daki hâli de kötü: bir isteğin yazdığı değer başka bir isteğin hesabını değiştirir ve Dictionary eşzamanlı yazmalara karşı güvenli değil.

Neden ortaya çıkar: Singleton pattern'in "tek instance" garantisi ile "her yerden erişim" kolaylığı aynı paketle gelir. İlki çoğu zaman gerçek bir ihtiyaçtır, ikincisi ise sorunun kendisi. Dönüşüm, ilkini tutup ikincisini atmak: instance'ı constructor'dan al, tek instance garantisini DI container'ın singleton lifetime'ına bırak.

Console.WriteLine("Order A then B:");
Test.CampaignAppliesDiscount();
Test.NoCampaignMeansFullPrice();

Console.WriteLine("Order B then A:");
Test.NoCampaignMeansFullPrice();
Test.CampaignAppliesDiscount();

public static class Test
{
    public static void CampaignAppliesDiscount()
    {
        DiscountRates rates = new();
        rates.Set("BLACKFRIDAY", 0.30m);
        decimal price = new PriceCalculator(rates).PriceOf(100m);
        Console.WriteLine($"  CampaignAppliesDiscount: {(price == 70m ? "passed" : $"FAILED ({price})")}");
    }

    public static void NoCampaignMeansFullPrice()
    {
        decimal price = new PriceCalculator(new DiscountRates()).PriceOf(100m);
        Console.WriteLine($"  NoCampaignMeansFullPrice: {(price == 100m ? "passed" : $"FAILED ({price})")}");
    }
}

// Uygulamada tek instance: services.AddSingleton<DiscountRates>()
public sealed class DiscountRates
{
    private readonly Dictionary<string, decimal> rates = [];

    public void Set(string campaign, decimal rate) => rates[campaign] = rate;
    public decimal TotalRate => rates.Values.Sum();
}

public sealed class PriceCalculator(DiscountRates rates)
{
    public decimal PriceOf(decimal listPrice) => listPrice * (1 - rates.TotalRate);
}
Order A then B:
  CampaignAppliesDiscount: passed
  NoCampaignMeansFullPrice: passed
Order B then A:
  NoCampaignMeansFullPrice: passed
  CampaignAppliesDiscount: passed

Artık her test kendi DiscountRates'ini kuruyor ve sıra fark etmiyor. Uygulamada tek instance hâlâ var; ama bu bir kayıt satırının kararı, class'ın kendisinin değil. Serinin "Singleton Pattern: Thread-Safe Lazy<T>, Static Field ve Neden DI Singleton Lifetime Daha İyi" yazısı bu geçişi ve thread-safety tarafını ayrıntısıyla anlatıyor.

Anemic Domain Model

Belirti: domain class'ları yalnızca public get/set property'lerden oluşuyor, bütün kurallar service class'larında. Martin Fowler 2003'teki "AnemicDomainModel" bliki notunda bunu, object-oriented tasarımın temel fikrine, yani veriyi ve onu işleyen davranışı bir araya koymaya aykırı bulur ve bir anti-pattern olarak tanımlar. Sorunun pratik yüzü şu:

AnemicOrder anemic = new() { Status = "Shipped" };
AnemicOrderService service = new();
try
{
    service.AddItem(anemic, "9780441013593", 1);
}
catch (InvalidOperationException ex)
{
    Console.WriteLine($"Service: {ex.Message}");
}
// Gece çalışan import job'u service'i bilmiyor, listeye doğrudan ekliyor
anemic.Items.Add(new OrderLine("9780141439518", 2));
Console.WriteLine($"Anemic shipped order now has {anemic.Items.Count} line(s)");

Order order = new(1042);
order.AddItem("9780441013593", 1);
order.Ship();
try
{
    order.AddItem("9780141439518", 2);
}
catch (InvalidOperationException ex)
{
    Console.WriteLine($"Order: {ex.Message}");
}
Console.WriteLine($"Rich shipped order has {order.Items.Count} line(s)");

public sealed record OrderLine(string Isbn, int Quantity);

// Anemic: yalnız veri, kurallar başka bir yerde
public sealed class AnemicOrder
{
    public string Status { get; set; } = "Draft";
    public List<OrderLine> Items { get; set; } = [];
}

public sealed class AnemicOrderService
{
    public void AddItem(AnemicOrder order, string isbn, int quantity)
    {
        if (order.Status == "Shipped")
        {
            throw new InvalidOperationException("Cannot add items to a shipped order.");
        }
        order.Items.Add(new OrderLine(isbn, quantity));
    }
}

// Zengin model: kural verinin yanında, liste dışarıya salt okunur
public sealed class Order(int id)
{
    private readonly List<OrderLine> items = [];

    public int Id => id;
    public bool IsShipped { get; private set; }
    public IReadOnlyList<OrderLine> Items => items;

    public void AddItem(string isbn, int quantity)
    {
        if (IsShipped)
        {
            throw new InvalidOperationException($"Order {Id} is shipped; items cannot be added.");
        }
        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(quantity);
        items.Add(new OrderLine(isbn, quantity));
    }

    public void Ship()
    {
        if (items.Count == 0)
        {
            throw new InvalidOperationException($"Order {Id} has no items.");
        }
        IsShipped = true;
    }
}
Service: Cannot add items to a shipped order.
Anemic shipped order now has 1 line(s)
Order: Order 1042 is shipped; items cannot be added.
Rich shipped order has 1 line(s)

"Kargolanmış siparişe kalem eklenmez" kuralı AnemicOrderService'te doğru yazılmış ve service üzerinden gelen çağrıyı durdurdu. Ama kural verinin sahibinde değil; Items public bir List olduğu için service'i bilmeyen import job'u kuralı hiç görmeden listeye ekledi. Zengin modelde aynı kural Order.AddItem'ın içinde ve Items dışarıya IReadOnlyList olarak açılıyor; kuralı atlamanın derlenebilir bir yolu yok.

Fowler'ın eleştirisi güçlü, ama her uygulamaya aynı ağırlıkla uygulanmaz. Bir ürün kataloğunun admin ekranı gibi, iş kuralı neredeyse olmayan, veriyi okuyup yazan CRUD (Create, Read, Update, Delete) ağırlıklı bir modülde property'lerden ibaret bir class ve ince bir service gayet makul; ortada korunacak bir invariant yoksa davranışı modele taşımanın da faydası yok. Anemic model, kurallar ortaya çıkmaya başladığında ve aynı kural birden fazla service'te kopyalanmaya başladığında gerçek bir sorun hâline gelir. Clean Code serisinin "Object mi Data Structure mı? Record, DTO ve Primitive Obsession" yazısı (DTO: Data Transfer Object) bu ayrımın iki ucunu anlatıyor; DTO gibi bilerek davranışsız tasarlanan tipler anemic model değil, sınırı geçen veri taşıyıcılarıdır.

Golden Hammer, Lava Flow ve Poltergeist

AntiPatterns kitabındaki diğer üç tanıdık yüz, kod yerine daha çok ekip alışkanlıklarında görünür:

Anti-patternBelirtiNedenDönüşüm
Golden HammerHer yeni problem aynı araçla çözülüyor: her şey MediatR request'i, her veri bir message queue'dan geçiyor, her tablo için generic repositoryEkip bir aracı iyi biliyor ve onu kullanmak düşünmekten daha az riskli görünüyorHer yeni problemde en az iki seçeneği ve tradeoff'unu yazıya dökmek
Lava FlowKimsenin ne işe yaradığını bilmediği, silinmeye korkulan kod: kullanılmayan class'lar, yorum satırına alınmış bloklar, "geçici" feature flag'lerKodu yazan ayrıldı, test yok, silmek riskli görünüyorTest ve kullanım analiziyle kanıt toplayıp silmek; Git geçmişi kodu zaten saklıyor
PoltergeistYalnızca başka bir class'ı çağırıp kaybolan kısa ömürlü class'lar: OrderProcessStarter.Start() içinde tek satır new OrderProcessor().Run()"Her şeyin bir class'ı olsun" refleksi, sorumlulukları yanlış yere bölmekAracı class'ı kaldırıp çağrıyı asıl sahibine taşımak (Inline Class)

Tablodaki örnekler bilinçli olarak suçlayıcı değil. MediatR, generic repository ya da feature flag kendi başına kötü değil; anti-pattern, aracın problemden bağımsız olarak her yere uygulanması. Kod seviyesinde bunun en yaygın hâli ise pattern'lerin kendisi.

Patternitis: Üç Satırlık İşe Beş Pattern

Belirti: bir design pattern kitabını yeni bitirmiş ekip, her kararı bir pattern'le ifade etmeye başlar. Ücretsiz kargo kararı için gerçek bir örnek:

Cart cart = new([12.99m, 7.50m, 31.00m]);

// Pattern hastalığı: tek bir if için dört tip
IFreeShippingPolicyFactory factory = new DefaultFreeShippingPolicyFactory();
IFreeShippingPolicy policy = factory.Create(new FreeShippingPolicyOptionsBuilder().WithThreshold(50m).Build());
Console.WriteLine(policy.IsFree(cart));

// Aynı karar, düz kod
Console.WriteLine(cart.Total >= 50m);

public sealed record Cart(decimal[] Prices)
{
    public decimal Total => Prices.Sum();
}

public interface IFreeShippingPolicy { bool IsFree(Cart cart); }
public interface IFreeShippingPolicyFactory { IFreeShippingPolicy Create(FreeShippingPolicyOptions options); }
public sealed record FreeShippingPolicyOptions(decimal Threshold);

public sealed class FreeShippingPolicyOptionsBuilder
{
    private decimal threshold;
    public FreeShippingPolicyOptionsBuilder WithThreshold(decimal value) { threshold = value; return this; }
    public FreeShippingPolicyOptions Build() => new(threshold);
}

public sealed class ThresholdFreeShippingPolicy(FreeShippingPolicyOptions options) : IFreeShippingPolicy
{
    public bool IsFree(Cart cart) => cart.Total >= options.Threshold;
}

public sealed class DefaultFreeShippingPolicyFactory : IFreeShippingPolicyFactory
{
    public IFreeShippingPolicy Create(FreeShippingPolicyOptions options) => new ThresholdFreeShippingPolicy(options);
}
True
True

İki satır aynı cevabı veriyor. Birincisi dört tip, bir builder, bir factory ve bir strategy kullanıyor; ikincisi bir karşılaştırma. Pattern'li sürümün getirdiği esneklik (farklı politikalar, farklı eşikler) bugün hiçbir gereksinimde yok. Bedeli ise gerçek: bir okuyucu "kargo ne zaman ücretsiz?" sorusunun cevabını bulmak için beş tipi gezmek zorunda. Bu serinin her yazısındaki "Ne Zaman Kullanma" bölümleri tam olarak bu hastalığa karşı yazıldı.

Kural basit: pattern'i kod istediğinde kullan. Joshua Kerievsky'nin 2004 tarihli "Refactoring to Patterns" kitabının temel fikri de bu: pattern'leri baştan tasarıma koymak yerine, kod gerçekten o yapıya ihtiyaç duyduğunda refactoring ile o yapıya doğru ilerlemek. Elinde üç if varsa ve dördüncüsü geliyorsa Strategy'yi düşün; tek bir if için değil. SOLID serisinin "Prensiplerin Eleştirisi: CUPID, Over-Engineering ve SOLID'in Gerçekten Gerektiği Yer" yazısı, Dan North'un CUPID (Composable, Unix philosophy, Predictable, Idiomatic, Domain-based) önerisiyle birlikte aynı soruyu prensipler için soruyor.

Sık Düşülen Tuzaklar

Her hoşlanmadığın kodu anti-pattern diye etiketlemek. "Bu bir God Object" demek bir teşhis; arkasından belirti (hangi değişiklikler bu class'ı birlikte açtırıyor?) ve dönüşüm planı gelmiyorsa yalnızca bir yargı. Code review'da isim koymak yerine somut bedeli göstermek daha ikna edici: "bu class'a son üç ayda dokunan 14 pull request'in 11'i birbirinden alakasız".

Anti-pattern'i tek seferde yeniden yazmak. God Object'i bir haftalık bir branch'te on class'a bölmek, merge edilemeyen ve test edilemeyen bir değişiklik üretir. Her yeni özellikte bir parçayı dışarı çıkarmak daha yavaş ama güvenli.

Dönüşümün kendisini yeni bir anti-pattern'e çevirmek. Service Locator'dan kaçarken her şeyi constructor'a koyup on iki parametreli constructor'lar üretmek, sorunu çözmez, görünür kılar. O görünürlük iyi bir şey: sıradaki adım class'ı bölmek.

Bağlamı unutmak. Anemic model bir CRUD modülünde kabul edilebilir, global bir static sabit tablo (değişmeyen bir ülke kodu listesi gibi) singleton abuse değildir. Anti-pattern'i yapan şey yapının kendisinden çok, problemle uyumsuzluğudur.

Küçük Bir Deneme

★☆☆ Önce Tahmin Et'teki OrderService'i constructor injection'a çevir ve ilk build'in artık iki hata raporladığını gör. Sonra IOrderNotifier'ı kaydet: siparişin "parası çekildi ama onay gitmedi" hâline düşme ihtimali nereye taşındı?

★★☆ BookstoreManager'ı actor'lere göre en az üç class'a böl ve CA1506'yı aynı eşikle yeniden çalıştır. Hangi class eşiğe en yakın kaldı ve bu sana o class hakkında ne söylüyor?

★★★ Kendi projende bir Lava Flow avına çık: son bir yılda hiç değişmemiş ve hiçbir test'in çağırmadığı public class'ları bul (git log ve "find usages" yeterli). Birini silmek için hangi kanıtları topladığını bir pull request açıklaması olarak yaz.

Bu yazıda kaçınılması gereken yapılara baktık. Bir sonraki yazı, GoF (Gang of Four: Gamma, Helm, Johnson ve Vlissides'in 1994 tarihli Design Patterns kitabı) kataloğunda olmayan ama günlük C# kodunda en az onlar kadar sık karşılaşacağın, kendini kanıtlamış kod seviyesi pattern'lere bakıyor: Null Object ve Special Case, Specification, Result ve Pipeline.