Factory Method ve Static Factory: Oluşturmayı Alt Class'a ya da Tek Bir Method'a Bırakmak
GoF Factory Method hangi ürünün oluşturulacağını alt class'a bırakır; static factory method ise constructor'ın yerine geçen, adı olan, cache'leyebilen ve gizli alt tip döndürebilen tek bir method'dur. Constructor'dan çağrılan factory method hatası ve CA2214, DbConnection.CreateDbCommand, ILoggerFactory ve IHttpClientFactory, DI'da Func<T>, ActivatorUtilities, keyed services ve IDisposable ürünlerin sahipliği gerçek çıktılarla.
Sürüm notu: Bu yazıdaki kodlar C# 14 ve .NET 10 ile derlenip denenmiştir.
Önce Tahmin Et
Bookstore'un muhasebe ekibi aylık satış raporunu CSV (Comma-Separated Values) olarak istiyor, ayraç da noktalı virgül olsun, çünkü Excel'in Türkçe ayarları virgülü ondalık ayracı sayıyor. Rapor işinin akışı bir base class'ta; hangi exporter'ın kullanılacağına alt class karar veriyor ve base class bu kararı constructor'ında alıyor. Kod derleniyor, çalışıyor. Çıktıda ne görürsün?
using System.Globalization;
using Bookstore.Reporting;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
Sale[] sales =
[
new("Dune", 2, 25.98m),
new("Emma", 1, 7.50m)
];
ReportJob job = new CsvReportJob(separator: ";");
Console.WriteLine(job.Run(sales));
namespace Bookstore.Reporting
{
public record Sale(string Title, int Quantity, decimal Total);
public interface IReportExporter
{
string Export(IReadOnlyList<Sale> sales);
}
public sealed class CsvExporter(string separator) : IReportExporter
{
public string Export(IReadOnlyList<Sale> sales) =>
string.Join(Environment.NewLine, sales.Select(s => $"{s.Title}{separator}{s.Quantity}{separator}{s.Total}"));
}
// Creator: akışı bilir, hangi exporter'ın üretileceğini alt class'a bırakır
public abstract class ReportJob
{
private readonly IReportExporter _exporter;
protected ReportJob()
{
_exporter = CreateExporter(); // factory method
}
protected abstract IReportExporter CreateExporter();
public string Run(IReadOnlyList<Sale> sales) => _exporter.Export(sales);
}
public sealed class CsvReportJob : ReportJob
{
private readonly string _separator;
public CsvReportJob(string separator)
{
_separator = separator;
}
protected override IReportExporter CreateExporter() => new CsvExporter(_separator);
}
}
Constructor'dan Çağrılan Factory Method
Çıktı:
Dune225.98
Emma17.50
Ayraç yok. Dune;2;25.98 beklerken Dune225.98 geldi ve dosyayı açan muhasebeci hangi rakamın adet, hangisinin tutar olduğunu bilemiyor. Sebep C#'ın constructor sırası: new CsvReportJob(";") önce base class'ın constructor'ını çalıştırır, CsvReportJob'un constructor gövdesi ancak ondan sonra çalışır. Base constructor CreateExporter()'ı çağırdığında virtual dispatch alt class'taki override'a gider, ama o anda _separator henüz atanmamıştır; değeri null'dır ve string.Join onu boş string gibi kullanır. Nullable reference types da yardım etmiyor, çünkü derleyicinin bakış açısından field constructor'da atanıyor.
Derleyici sessiz kalsa da .NET'in code analysis kurallarından biri bu durumu tanıyor. Proje klasörüne dotnet_diagnostic.CA2214.severity = warning satırı olan bir .editorconfig koyunca build şunu söylüyor:
Program.cs(37,25): warning CA2214: Do not call overridable methods in constructors (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2214) [app.csproj]
İlginç bir ayrıntı: alt class'ı primary constructor ile yazınca hata kayboluyor.
// Primary constructor: yakalanan parametre base constructor'dan önce atanır
public sealed class CsvReportJob(string separator) : ReportJob
{
protected override IReportExporter CreateExporter() => new CsvExporter(separator);
}
Dune;2;25.98
Emma;1;7.50
Primary constructor'ın yakalanan parametreleri, field initializer'lar gibi base constructor çağrısından önce atanıyor. Buna güvenme. Bir sonraki geliştirici bir değeri constructor gövdesinde hesaplamaya başladığında hata geri gelir. Asıl düzeltme, factory method'u constructor'dan çağırmamak: ürünü ihtiyaç anında, yani Run'ın içinde üretmek.
GoF Factory Method: Kararı Alt Class'a Bırakmak
Factory Method, GoF'un (Gang of Four: Gamma, Helm, Johnson ve Vlissides'in 1994 tarihli Design Patterns kitabı) creational pattern'leri arasında class scope'taki tek pattern. Amacı kısaca şu: object oluşturmak için bir interface tanımla, ama hangi concrete class'ın üretileceğine alt class'lar karar versin. Rolleri dört: ürünün interface'i (Product, burada IReportExporter), concrete ürünler (CsvExporter, MarkdownExporter), akışı bilen ve factory method'u tanımlayan creator (ReportJob) ve factory method'u override eden concrete creator'lar. Düzeltilmiş hâli:
using System.Data.Common;
using System.Globalization;
using System.Reflection;
using Bookstore.Reporting;
using Microsoft.Data.Sqlite;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
Sale[] sales =
[
new("Dune", 2, 25.98m),
new("Emma", 1, 7.50m)
];
foreach (ReportJob job in new ReportJob[] { new CsvReportJob(";"), new MarkdownReportJob() })
{
ExportedFile file = job.Run(sales);
Console.WriteLine($"--- {file.Name}");
Console.WriteLine(file.Content);
}
// BCL'deki factory method: DbConnection.CreateCommand -> protected abstract CreateDbCommand
MethodInfo hook = typeof(DbConnection).GetMethod("CreateDbCommand", BindingFlags.NonPublic | BindingFlags.Instance)!;
Console.WriteLine($"--- DbConnection.CreateDbCommand: abstract={hook.IsAbstract}, protected={hook.IsFamily}");
using DbConnection connection = new SqliteConnection("Data Source=:memory:");
using DbCommand command = connection.CreateCommand();
Console.WriteLine($"{connection.GetType().Name}.CreateCommand() -> {command.GetType().Name}");
namespace Bookstore.Reporting
{
public record Sale(string Title, int Quantity, decimal Total);
public record ExportedFile(string Name, string Content);
public interface IReportExporter
{
string Extension { get; }
string Export(IReadOnlyList<Sale> sales);
}
public sealed class CsvExporter(string separator) : IReportExporter
{
public string Extension => "csv";
public string Export(IReadOnlyList<Sale> sales) =>
string.Join(Environment.NewLine, sales.Select(s => string.Join(separator, s.Title, s.Quantity, s.Total)));
}
public sealed class MarkdownExporter : IReportExporter
{
public string Extension => "md";
public string Export(IReadOnlyList<Sale> sales) =>
string.Join(Environment.NewLine,
["| Title | Qty | Total |", "|---|---:|---:|", .. sales.Select(s => $"| {s.Title} | {s.Quantity} | {s.Total} |")]);
}
public abstract class ReportJob
{
// Akış sabit; değişen tek şey hangi exporter'ın üretildiği
public ExportedFile Run(IReadOnlyList<Sale> sales)
{
IReportExporter exporter = CreateExporter();
return new ExportedFile($"sales-2026-10.{exporter.Extension}", exporter.Export(sales));
}
protected abstract IReportExporter CreateExporter();
}
public sealed class CsvReportJob(string separator) : ReportJob
{
protected override IReportExporter CreateExporter() => new CsvExporter(separator);
}
public sealed class MarkdownReportJob : ReportJob
{
protected override IReportExporter CreateExporter() => new MarkdownExporter();
}
}
--- sales-2026-10.csv
Dune;2;25.98
Emma;1;7.50
--- sales-2026-10.md
| Title | Qty | Total |
|---|---:|---:|
| Dune | 2 | 25.98 |
| Emma | 1 | 7.50 |
--- DbConnection.CreateDbCommand: abstract=True, protected=True
SqliteConnection.CreateCommand() -> SqliteCommand
ReportJob.Run raporun nasıl üretileceğini (exporter'ı al, satırları ver, dosya adını uzantıyla kur) biliyor ama hiçbir concrete exporter'ı tanımıyor. Yeni bir format eklemek Run'a dokunmadan yeni bir alt class yazmak demek. Factory method'un çağrıldığı yer de artık constructor değil, akışın kendisi.
Programın son iki satırı bu pattern'in .NET'teki en eski örneklerinden birini gösteriyor. .NET'in veri erişim katmanı ADO.NET'te (adını eski ActiveX Data Objects teknolojisinden alır) DbConnection.CreateCommand() public ve virtual değil; içeride protected abstract CreateDbCommand()'ı çağırır. SqliteConnection, SqlConnection ve NpgsqlConnection bu method'u override edip kendi command tipini döndürür. Sen DbConnection ile konuşursun, hangi command'ın üretildiğine provider karar verir. Reflection çıktısı tam bunu söylüyor: abstract=True, protected=True.
Peki 2026'da bir rapor işi için gerçekten alt class açar mıydın? Çoğu zaman hayır. Önceki yazıda andığımız Peter Norvig'in tespiti burada da geçerli: first-class function'ı olan bir dilde "alt class bir method'u override etsin" ile "constructor bir Func<IReportExporter> alsın" aynı esnekliği verir ve ikincisi inheritance gerektirmez. Factory Method'un inheritance'lı biçimi bugün daha çok framework'lerde yaşıyor: ADO.NET'in provider modeli, ASP.NET Core'daki WebApplicationFactory<T>'nin override edilebilen CreateHost method'u ya da kendi base class'ını kullanıcılarına açan library'ler. Uygulama kodunda ise ürünü dışarıdan almak genelde daha sade.
Static Factory Method: GoF'ta Olmayan Akraba
Günlük kodda "factory" dediğimiz şeyin büyük kısmı aslında GoF'ta yok. TimeSpan.FromSeconds, Guid.CreateVersion7(), Channel.CreateBounded<T>() ya da ToFrozenDictionary() birer static factory method: alt class yok, override yok, yalnızca constructor yerine çağrılan, adı olan bir static method. Bu fikri en iyi savunan metin Joshua Bloch'un (Java collection'larının tasarımcısı) Effective Java kitabının ilk maddesi, "constructor yerine static factory method'ları düşün". Bloch'un saydığı kazançları Bookstore'da ve .NET'te görelim:
using System.Collections.Frozen;
using System.Threading.Channels;
using Bookstore.Reporting;
// 1) Ad: ne üretildiği çağrıdan okunuyor
ReportPeriod october = ReportPeriod.Month(2026, 10);
ReportPeriod q4 = ReportPeriod.Quarter(2026, 4);
Console.WriteLine($"{october} | {q4}");
Console.WriteLine(ReportPeriod.TryParse("2026-Q5", out _) ? "parsed" : "2026-Q5 rejected");
// 2) Alt tip seçimi: dönen tip imzadakinden daha somut ve gizli
Console.WriteLine(Channel.CreateBounded<Sale>(100).GetType().Name);
Console.WriteLine(new Dictionary<string, int> { ["dune"] = 3 }.ToFrozenDictionary().GetType().Name);
// 3) Cache: her çağrıda yeni object şart değil
Console.WriteLine(ReferenceEquals(Enumerable.Empty<Sale>(), Enumerable.Empty<Sale>()));
Console.WriteLine(ReferenceEquals(Task.FromResult(true), Task.FromResult(true)));
// 4) Overload'larla anlam: .NET 9'daki integer TimeSpan overload'ları
Console.WriteLine(TimeSpan.FromMilliseconds(101.832).Ticks);
Console.WriteLine(TimeSpan.FromMilliseconds(101, 832).Ticks);
namespace Bookstore.Reporting
{
public record Sale(string Title, int Quantity, decimal Total);
public sealed record ReportPeriod
{
public DateOnly From { get; }
public DateOnly To { get; }
public string Label { get; }
private ReportPeriod(DateOnly from, DateOnly to, string label) => (From, To, Label) = (from, to, label);
public static ReportPeriod Month(int year, int month)
{
DateOnly from = new(year, month, 1);
return new(from, from.AddMonths(1).AddDays(-1), $"{year}-{month:00}");
}
public static ReportPeriod Quarter(int year, int quarter)
{
ArgumentOutOfRangeException.ThrowIfLessThan(quarter, 1);
ArgumentOutOfRangeException.ThrowIfGreaterThan(quarter, 4);
DateOnly from = new(year, (quarter - 1) * 3 + 1, 1);
return new(from, from.AddMonths(3).AddDays(-1), $"{year}-Q{quarter}");
}
public static bool TryParse(string text, out ReportPeriod? period)
{
period = null;
string[] parts = text.Split('-');
if (parts.Length != 2 || !int.TryParse(parts[0], out int year))
return false;
if (parts[1].StartsWith('Q') && int.TryParse(parts[1][1..], out int q) && q is >= 1 and <= 4)
period = Quarter(year, q);
else if (int.TryParse(parts[1], out int m) && m is >= 1 and <= 12)
period = Month(year, m);
return period is not null;
}
public override string ToString() => $"{Label}: {From:yyyy-MM-dd}..{To:yyyy-MM-dd}";
}
}
2026-10: 2026-10-01..2026-10-31 | 2026-Q4: 2026-10-01..2026-12-31
2026-Q5 rejected
BoundedChannel`1
LengthBucketsFrozenDictionary`1
True
True
1018319
1018320
Birinci kazanç ad. new ReportPeriod(from, to) imzası "Ekim ayı" ile "dördüncü çeyrek" arasındaki farkı anlatamaz; iki constructor aynı parametre tiplerini alacağı için overload ile de ayrılamazlar. Month ve Quarter hem niyeti söylüyor hem de kendi kuralını uyguluyor. Constructor private olduğu için geçersiz bir dönem (bitişi başlangıcından önce, "çeyrek" dendiği hâlde dört ay) üretmenin yolu da kalmıyor. TryParse ise aynı ailenin exception fırlatmayan üyesi.
İkinci kazanç, dönen tipin imzadakinden daha somut ve gizli olabilmesi. Channel.CreateBounded imzada Channel<T> döndürüyor ama gerçekte internal BoundedChannel<T>. ToFrozenDictionary daha da ileri gidiyor: key'lerin tipine ve uzunluklarına bakıp birkaç özel implementation'dan birini seçiyor; string key'ler için bu koşuda LengthBucketsFrozenDictionary geldi. Microsoft yarın daha hızlı bir implementation yazarsa senin kodun değişmeden ondan faydalanır. Constructor bunu yapamaz; new her zaman tam olarak yazdığın tipi üretir.
Üçüncü kazanç cache. Enumerable.Empty<T>() her çağrıda aynı boş instance'ı döndürüyor; Task.FromResult(true) da bu koşuda iki çağrıda aynı tamamlanmış task'ı verdi. Bir constructor her zaman yeni bir object oluşturmak zorunda.
Son iki satır static factory'lerin bir inceliğini gösteriyor. TimeSpan.FromMilliseconds(101.832) bir double alıyor ve ikili kayan nokta gösterimi yüzünden 1.018.320 yerine 1.018.319 tick üretiyor. .NET 9 bu yüzden From* ailesine integer overload'lar ekledi: FromMilliseconds(101, 832) milisaniye ve mikrosaniyeyi tam sayı olarak alıyor ve doğru sonucu veriyor. Static factory'ler zamanla aile olarak büyüyebiliyor ve hepsi aynı adlandırma kuralını paylaşıyor; bu da onların bir başka gücü.
İsim karmaşasını bir yere bağlayalım. Static factory method tek bir method'dur ve tipin kendisine aittir. Simple factory, C# OOP (Object-Oriented Programming) serisindeki "C#'ta Birkaç Design Pattern" yazısında gördüğümüz gibi bir switch ile hangi tipin üretileceğine karar veren ayrı bir class'tır; o da GoF'ta yok. Factory Method alt class'a bırakılan karardır. Abstract Factory ise birbirine uyumlu bir ürün ailesini birlikte üreten object'tir ve bir sonraki yazının konusu.
.NET'in Factory'leri: ILoggerFactory ve IHttpClientFactory
.NET'in kendi kütüphanesinde adında Factory geçen tipler az ama kullanıldıkları yerler önemli. İkisinin neden birer factory olduğuna, yani neden doğrudan new ile oluşturulmadıklarına bakalım:
using Bookstore.Reporting;
using Microsoft.Extensions.Logging;
// Static factory method, bir factory üretiyor
using (ILoggerFactory loggerFactory = LoggerFactory.Create(builder => builder.AddSimpleConsole(o => o.SingleLine = true)))
{
ILogger<ReportService> typed = loggerFactory.CreateLogger<ReportService>();
ILogger named = loggerFactory.CreateLogger("Bookstore.Audit");
typed.LogInformation("Exported {Rows} rows", 2);
named.LogWarning("Report {Name} was downloaded twice", "sales-2026-10.csv");
Console.WriteLine($"{typed.GetType().Name} / {named.GetType().Name}");
}
namespace Bookstore.Reporting
{
public sealed class ReportService;
}
Logger`1 / Logger
info: Bookstore.Reporting.ReportService[0] Exported 2 rows
warn: Bookstore.Audit[0] Report sales-2026-10.csv was downloaded twice
LoggerFactory.Create bir static factory method ve bir factory üretiyor; CreateLogger ise o factory'nin ürettiği logger'lar. Factory'nin var olma nedeni oluşturmanın kendisinin karmaşık olması: bir logger'ın ne yapacağı, factory'ye kayıtlı provider'lara (console, debug, OpenTelemetry), filtre kurallarına ve kategori adına bağlı. ILogger<ReportService> kategori adını tipten çıkarıyor, CreateLogger("Bookstore.Audit") ise elle veriyor. Çıktıdaki sıra da öğretici: Console.WriteLine satırı log'lardan önce geldi, çünkü console logger mesajları bir queue'ya bırakıp arka plandaki bir thread'le yazıyor; using bloğu factory'yi dispose edince queue boşaltıldı.
IHttpClientFactory'nin hikâyesi daha da öğretici. new HttpClient() yazmak kolay ama her client kendi HttpMessageHandler'ını, yani kendi connection'larını açar; çok sayıda kısa ömürlü client socket'ları tüketir. Tek bir static client ise DNS (Domain Name System) değişikliklerini göremez. IHttpClientFactory bu iki uç arasında bir politika uygular: CreateClient her çağrıda yeni ve ucuz bir HttpClient döndürür ama altındaki handler'ları bir pool'da tutar ve varsayılan olarak iki dakikada bir yeniler. "Bir object'i nasıl oluşturmalı?" sorusunun cevabı bir politika gerektiriyorsa, o politikanın yeri bir factory'dir.
DI ile Factory: Func<T>, ActivatorUtilities ve Keyed Services
DI (dependency injection) container'ın kendisi zaten büyük bir factory: hangi interface için hangi class'ın, hangi bağımlılıklarla oluşturulacağını biliyor. Factory'ye ayrıca ihtiyaç duyduğun an genelde şudur: ürünün tipi ya da bir parametresi ancak runtime'da, kullanıcının isteğiyle belli oluyor. Rapor formatı ve CSV ayracı tam böyle; ikisi de HTTP (Hypertext Transfer Protocol) isteğinden geliyor.
using System.Globalization;
using Bookstore.Reporting;
using Microsoft.Extensions.DependencyInjection;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
ServiceCollection services = new();
services.AddSingleton<TimeProvider>(new FixedClock(new DateTimeOffset(2026, 11, 1, 9, 0, 0, TimeSpan.Zero)));
services.AddKeyedTransient<IReportExporter, MarkdownExporter>("md");
services.AddSingleton<IReportExporterFactory, ReportExporterFactory>();
services.AddTransient<ReportService>();
services.AddTransient<NaiveReportService>();
using ServiceProvider provider = services.BuildServiceProvider();
// 1) Microsoft DI, Func<T>'yi kendiliğinden üretmez
try
{
provider.GetRequiredService<NaiveReportService>();
}
catch (InvalidOperationException ex)
{
Console.WriteLine(ex.Message);
}
// 2) Factory: runtime argümanı (ayraç) + container bağımlılığı (saat)
ReportService reports = provider.GetRequiredService<ReportService>();
Sale[] sales = [new("Dune", 2, 25.98m), new("Emma", 1, 7.50m)];
Console.WriteLine(reports.Export(sales, "csv", separator: ";"));
Console.WriteLine(reports.Export(sales, "md"));
try
{
reports.Export(sales, "xlsx");
}
catch (NotSupportedException ex)
{
Console.WriteLine(ex.Message);
}
namespace Bookstore.Reporting
{
public record Sale(string Title, int Quantity, decimal Total);
public sealed class FixedClock(DateTimeOffset now) : TimeProvider
{
public override DateTimeOffset GetUtcNow() => now;
}
public interface IReportExporter
{
string Export(IReadOnlyList<Sale> sales);
}
// separator runtime'da gelir, clock container'dan
public sealed class CsvExporter(TimeProvider clock, string separator) : IReportExporter
{
public string Export(IReadOnlyList<Sale> sales) =>
$"# generated {clock.GetUtcNow():yyyy-MM-dd HH:mm}Z" + Environment.NewLine +
string.Join(Environment.NewLine, sales.Select(s => string.Join(separator, s.Title, s.Quantity, s.Total)));
}
public sealed class MarkdownExporter : IReportExporter
{
public string Export(IReadOnlyList<Sale> sales) =>
string.Join(Environment.NewLine, sales.Select(s => $"- {s.Title} x{s.Quantity}: {s.Total}"));
}
public interface IReportExporterFactory
{
IReportExporter Create(string format, string separator = ",");
}
// Composition root'a ait: container'ı bilen tek class
public sealed class ReportExporterFactory(IServiceProvider services) : IReportExporterFactory
{
public IReportExporter Create(string format, string separator = ",") => format switch
{
"csv" => ActivatorUtilities.CreateInstance<CsvExporter>(services, separator),
"md" => services.GetRequiredKeyedService<IReportExporter>("md"),
_ => throw new NotSupportedException($"Report format '{format}' is not supported. Use csv or md.")
};
}
public sealed class ReportService(IReportExporterFactory exporters)
{
public string Export(IReadOnlyList<Sale> sales, string format, string separator = ",") =>
exporters.Create(format, separator).Export(sales);
}
public sealed class NaiveReportService(Func<IReportExporter> createExporter)
{
public string Export(IReadOnlyList<Sale> sales) => createExporter().Export(sales);
}
}
Unable to resolve service for type 'System.Func`1[Bookstore.Reporting.IReportExporter]' while attempting to activate 'Bookstore.Reporting.NaiveReportService'.
# generated 2026-11-01 09:00Z
Dune;2;25.98
Emma;1;7.50
- Dune x2: 25.98
- Emma x1: 7.50
Report format 'xlsx' is not supported. Use csv or md.
İlk satır, Autofac gibi container'lardan gelenlerin sık düştüğü bir tuzak. Microsoft'un built-in container'ı Func<T> bağımlılıklarını kendiliğinden üretmez; Microsoft Learn'deki DI yönergeleri Func<T> desteğini, başka bir container'a geçmeyi gerektirebilecek eksik özellikler arasında açıkça sayar. Ya delegate'i kendin kaydedersin (services.AddSingleton<Func<IReportExporter>>(...)) ya da adı olan bir factory interface'i tanımlarsın.
ReportExporterFactory ikinci yolu izliyor ve iki ayrı ihtiyacı birleştiriyor. CSV exporter'ın bir bağımlılığı container'dan (TimeProvider), bir parametresi runtime'dan (ayraç) geliyor. ActivatorUtilities.CreateInstance tam bunun için var: verdiğin argümanları kullanır, gerisini container'dan çözer. Markdown exporter ise .NET 8 ile gelen keyed services ile kayıtlı; "md" key'i doğrudan implementation'ı seçiyor. Keyed services'i Strategy yazısında, birden fazla implementation arasında runtime'da seçim yapmanın yollarıyla birlikte ayrıntılı göreceğiz.
Bu factory IServiceProvider alıyor ve bu bir uyarıyı hak ediyor. Microsoft'un yönergeleri, runtime'da container'dan bağımlılık çözen bir factory'yi enjekte etmeyi service locator'ın bir çeşidi olarak tarif eder. Buradaki ayrım şu: ReportService container'ı tanımıyor, yalnızca IReportExporterFactory'yi tanıyor. Container'ı bilen tek class factory'nin implementation'ı ve o da composition root'a, yani uygulamanın bağımlılıkları kurduğu tek yere ait. Factory'nin implementation'ı iş kodunun arasına dağılmaya başlarsa çizgi aşılmış demektir.
Factory'nin Sahiplik Sorusu: IDisposable Ürünler
Factory ile container'ın kesiştiği yerde bir soru daha var: üretilen object'i kim dispose edecek? Excel dosyası üreten bir exporter'ın geçici bir dosya ya da native bir kaynak tuttuğunu düşün:
using Bookstore.Reporting;
using Microsoft.Extensions.DependencyInjection;
ServiceCollection services = new();
services.AddTransient<SpreadsheetExporter>();
ServiceProvider provider = services.BuildServiceProvider();
// Root provider'dan 1000 kez transient IDisposable çözülüyor
for (int i = 0; i < 1000; i++)
{
SpreadsheetExporter exporter = provider.GetRequiredService<SpreadsheetExporter>();
exporter.Export();
}
Console.WriteLine($"After 1000 exports, disposed: {SpreadsheetExporter.Disposed}");
provider.Dispose();
Console.WriteLine($"After provider.Dispose(), disposed: {SpreadsheetExporter.Disposed}");
// Factory ile: instance'ın sahibi çağıran kod
SpreadsheetExporter.Disposed = 0;
using ServiceProvider provider2 = services.BuildServiceProvider();
for (int i = 0; i < 1000; i++)
{
using SpreadsheetExporter exporter = ActivatorUtilities.CreateInstance<SpreadsheetExporter>(provider2);
exporter.Export();
}
Console.WriteLine($"Factory + using, disposed: {SpreadsheetExporter.Disposed}");
namespace Bookstore.Reporting
{
// Gerçekte geçici bir dosya ya da native bir kaynak tutuyor olabilir
public sealed class SpreadsheetExporter : IDisposable
{
public static int Disposed;
public void Export() { }
public void Dispose() => Disposed++;
}
}
After 1000 exports, disposed: 0
After provider.Dispose(), disposed: 1000
Factory + using, disposed: 1000
Container, kendi oluşturduğu transient IDisposable object'leri dispose etmek için bir listede tutar ve ancak bulundukları scope bittiğinde dispose eder. Root provider'ın scope'u uygulamanın kendisi; bin export'tan sonra bin exporter hâlâ bellekte ve hiçbiri dispose edilmemiş. Microsoft Learn bunu bir memory leak anti-pattern'i olarak listeler ve çözüm olarak tam da factory'yi önerir: object'i container'ın dışında oluşturan (doğrudan constructor ya da ActivatorUtilities ile) ve sahipliği çağırana veren bir Create method'u. İkinci döngüde her exporter using ile hemen dispose edildi. Kural basit: factory'nin döndürdüğü IDisposable'ı çağıran dispose eder, container'ın enjekte ettiğini ise asla.
Sık Düşülen Tuzaklar
Her new'i bir factory'ye sarmak. BookFactory.Create(isbn, title) içinde yalnızca new Book(isbn, title) varsa, okuyan bir dolaylılık kazanır ve karşılığında hiçbir şey almaz. Factory'nin bir gerekçesi olmalı: bir karar (hangi tip), bir politika (pool, cache), bir kural (geçerli dönem) ya da runtime'da gelen bir bilgi.
Constructor'dan virtual method çağırmak. Yazının başındaki hata yalnızca factory method'lara özgü değil; constructor'dan çağrılan her virtual ya da abstract method, alt class'ın henüz hazır olmadığı bir anda çalışır. CA2214'ü projende açmak ucuz bir sigorta.
Factory'yi bir boolean bayrak çöplüğüne çevirmek. Create(format, compress: true, encrypt: false, legacy: true) gibi bir imza, factory'nin aslında birkaç farklı soruya aynı anda cevap vermeye çalıştığını gösterir. Bayraklar ürünün kendisini değil sarmalayıcılarını belirliyorsa, Decorator yazısında göreceğimiz gibi bunları ayrı katmanlara ayırmak daha temiz.
Factory'yi service locator'a dönüştürmek. IServiceProvider'ı alıp Create<T>() diye her tipi üreten generic bir factory, container'ı başka bir adla her yere taşımaktır. Factory'nin ürettiği şey dar ve adlı olmalı: IReportExporter, HttpClient, ILogger.
Bilinmeyen durumda sessizce varsayılana düşmek. "xlsx" istendiğinde CSV üreten bir factory, kullanıcının bir sonraki adımda fark edeceği bir hatayı şimdiden saklar. Desteklenen formatları söyleyen bir exception, yanlış formatta indirilen bir dosyadan iyidir.
Ne Zaman Kullanma
Ürünün tipi compile time'da belliyse ve oluşturma tek satırsa factory yazma; new ya da container'ın constructor injection'ı yeter. GoF Factory Method'un inheritance'lı biçimini, yani alt class'ın override ettiği bir creation hook'u, ancak zaten bir base class'ın var ve kullanıcılarına genişletme noktası açıyorsan seç; tek bir uygulama içinde bir Func<T> ya da adı olan küçük bir factory interface'i aynı esnekliği inheritance'sız verir. Static factory method'ları ise cömertçe kullanabilirsin: maliyetleri neredeyse sıfır ve okunurluğa katkıları büyük. Tek dikkat edilecek nokta, bir tipin hem public constructor'ı hem de aynı işi yapan static factory'si varsa okuyanın hangisini kullanacağını bilememesi; ikisinden birini seç.
Küçük Bir Deneme
★☆☆ ReportPeriod'a Year(int year) ve Last(int days, DateOnly today) factory method'larını ekle. TryParse'ı "2026" biçimini de anlayacak şekilde genişlet ve geçersiz girdilerin reddedildiğini çıktıyla göster.
★★☆ Yazının başındaki hatalı ReportJob'u, factory method yerine constructor'da bir Func<IReportExporter> alacak şekilde yeniden yaz. Alt class'lara hâlâ ihtiyaç var mı? CA2214 uyarısı ne oldu?
★★★ ReportExporterFactory'ye bir SpreadsheetExporter (IDisposable) ekle ve ReportService.Export'u, factory'den aldığı exporter IDisposable ise dispose edecek şekilde güncelle. Markdown exporter'ı ise keyed service olarak container'dan geldiği için dispose etmemesi gerektiğini nasıl ayırt edeceksin? İki farklı tasarım öner ve birini kodla.
Bu yazıda tek bir ürünü üretmenin yollarını gördük. Bir sonraki yazıda ürünler aile hâlinde geliyor: blob storage, queue ve secret client'ı aynı cloud provider'dan, birbirine karışmadan üretmek ve bu işi yıllardır yapan ADO.NET'in DbProviderFactory'si ile Abstract Factory.