Abstract Factory: Birbirine Uyumlu Object Ailelerini Birlikte Üretmek
Abstract Factory, birbiriyle uyumlu olması gereken ürünleri (blob storage, queue, secret store gibi) tek bir factory'den üreterek geçersiz kombinasyonları imkânsız kılar; bedeli, aileye yeni ürün eklemenin bütün factory'leri değiştirmesidir. Configuration ile aile seçimi, ADO.NET'in DbProviderFactory'si ve CanCreateBatch, keyed services ile tenant başına factory 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 faturaları PDF olarak bir blob storage'a yüklüyor ve yazdırma worker'ına bir queue üzerinden haber veriyor. Şirket bir süredir Azure'dan AWS'ye (Amazon Web Services) geçiyor ve configuration'da her ürünün kendi ayarı var. Geçiş sırasında biri queue ayarını Aws yapmış, blob ayarına dokunmamış. İki ayar da tek başına geçerli; ikisi de code review'dan geçti. Worker mesajı aldığında ne olur?
Blob storage, queue ve secret store'un üç ailesi (Azure, AWS ve yerel geliştirme) bu yazıdaki bütün örneklerde ortak. Gerçek SDK'lar (Software Development Kit) yerine bellekte çalışan bir simülasyon kullanıyorum; önemli olan tek ayrıntı, her bulutun kendi referans biçimine sahip olması:
public interface IBlobStore
{
string Upload(string path, byte[] content); // geriye kalıcı bir referans döner
byte[] Download(string reference);
}
public interface IMessageQueue
{
void Send(string body);
string? Receive();
}
public interface ISecretStore
{
string Get(string name);
}
// Simülasyon: gerçek projede bu class'lar Azure SDK'sındaki BlobContainerClient ve QueueClient'ı,
// AWS SDK'sındaki AmazonS3Client ve AmazonSQSClient'ı sarar. Referans biçimi her bulutta farklıdır.
public abstract class SimulatedBlobStore(string scheme) : IBlobStore
{
private static readonly Dictionary<string, byte[]> Objects = [];
public string Upload(string path, byte[] content)
{
string reference = $"{scheme}://{path}";
Objects[reference] = content;
return reference;
}
public byte[] Download(string reference) =>
reference.StartsWith($"{scheme}://", StringComparison.Ordinal) && Objects.TryGetValue(reference, out byte[]? content)
? content
: throw new InvalidOperationException($"{GetType().Name} cannot read '{reference}'.");
}
public abstract class SimulatedQueue : IMessageQueue
{
private readonly Queue<string> _messages = new();
public void Send(string body) => _messages.Enqueue(body);
public string? Receive() => _messages.TryDequeue(out string? body) ? body : null;
}
public sealed class AzureBlobStore() : SimulatedBlobStore("azure-blob");
public sealed class AzureStorageQueue : SimulatedQueue;
public sealed class S3BlobStore() : SimulatedBlobStore("s3");
public sealed class SqsQueue : SimulatedQueue;
public sealed class LocalBlobStore() : SimulatedBlobStore("file");
public sealed class LocalQueue : SimulatedQueue;
public sealed class DictionarySecretStore(string source, Dictionary<string, string> secrets) : ISecretStore
{
public string Get(string name) => secrets.TryGetValue(name, out string? value)
? value
: throw new KeyNotFoundException($"Secret '{name}' not found in {source}.");
}
Ve uygulamanın kendisi:
using System.Text;
using Bookstore.Cloud;
// Her ürün kendi ayarından seçiliyor; biri yanlışlıkla farklı yazılmış
Dictionary<string, string> config = new()
{
["Cloud:Blob"] = "Azure",
["Cloud:Queue"] = "Aws"
};
IBlobStore blobs = config["Cloud:Blob"] switch
{
"Azure" => new AzureBlobStore(),
"Aws" => new S3BlobStore(),
_ => new LocalBlobStore()
};
IMessageQueue queue = config["Cloud:Queue"] switch
{
"Azure" => new AzureStorageQueue(),
"Aws" => new SqsQueue(),
_ => new LocalQueue()
};
// Fatura PDF'i yükleniyor, yazdırma worker'ına mesaj gidiyor
string reference = blobs.Upload("invoices/1042.pdf", Encoding.UTF8.GetBytes("%PDF invoice 1042"));
queue.Send(reference);
// Worker, mesajı aldığı bulutun storage'ından okuyor (SQS dinleyen worker AWS'de çalışır)
IBlobStore workerBlobs = new S3BlobStore();
string message = queue.Receive()!;
Console.WriteLine($"Message: {message}");
Console.WriteLine(Encoding.UTF8.GetString(workerBlobs.Download(message)));
İki Doğru Ayar, Bir Yanlış Aile
Çıktı (stack trace kısaltıldı):
Message: azure-blob://invoices/1042.pdf
Unhandled exception. System.InvalidOperationException: S3BlobStore cannot read 'azure-blob://invoices/1042.pdf'.
Fatura Azure'a yüklendi, mesaj AWS'deki queue'ya gitti. SQS'i (Simple Queue Service) dinleyen worker AWS'de çalışıyor ve yalnızca S3'ü (Simple Storage Service) okuyabiliyor; elindeki referans ise bir Azure blob'unu gösteriyor. Ayarların her biri doğru bir değer taşıyor ama birlikte geçersiz bir kombinasyon oluşturuyorlar. Daha kötüsü, bu kombinasyonu üreten kodda hiçbir şey yanlış görünmüyor: iki switch, iki ayar, iki geçerli seçim. Gerçek hayatta bu hata genelde production'a geçişin ilk saatinde, worker'ın log'larında ortaya çıkıyor.
Problem tek tek ürünlerde değil, ürünlerin birbirine uyumunda. Blob storage, queue ve secret store bir aile oluşturuyor; aynı bulutun üyeleri birbirinin referansını, kimlik bilgisini ve ağ ayarını anlıyor. GoF (Gang of Four: Gamma, Helm, Johnson ve Vlissides'in 1994 tarihli Design Patterns kitabı) bu problemi Abstract Factory ile çözer. Amacı: birbiriyle ilişkili ya da birbirine bağımlı object ailelerini, concrete class'larını belirtmeden oluşturmak için bir interface sağlamak. Kitabın kendi örneği de bir aile tutarlılığı problemi: Motif ve Presentation Manager gibi farklı görünüm standartlarını destekleyen bir kullanıcı arayüzü kütüphanesinde scroll bar'ın bir standarttan, pencerenin başka bir standarttan gelmemesi. Pattern'in diğer adı da bunu anlatıyor: Kit.
Abstract Factory: Ailenin Tutarlılığı
Çözüm, "hangi blob store, hangi queue" sorularını tek bir "hangi aile" sorusuna indirmek. Her aile, kendi ürünlerinin hepsini üreten bir factory'dir:
namespace Bookstore.Cloud;
// Abstract factory: bir ailenin bütün ürünlerini birlikte üretir
public interface ICloudFactory
{
string Name { get; }
IBlobStore CreateBlobStore();
IMessageQueue CreateQueue();
ISecretStore CreateSecretStore();
}
public sealed class AzureCloudFactory : ICloudFactory
{
public string Name => "Azure";
public IBlobStore CreateBlobStore() => new AzureBlobStore();
public IMessageQueue CreateQueue() => new AzureStorageQueue();
public ISecretStore CreateSecretStore() => new DictionarySecretStore("Key Vault", new() { ["print-api-key"] = "kv-123" });
}
public sealed class AwsCloudFactory : ICloudFactory
{
public string Name => "Aws";
public IBlobStore CreateBlobStore() => new S3BlobStore();
public IMessageQueue CreateQueue() => new SqsQueue();
public ISecretStore CreateSecretStore() => new DictionarySecretStore("Secrets Manager", new() { ["print-api-key"] = "sm-456" });
}
public sealed class LocalCloudFactory : ICloudFactory
{
public string Name => "Local";
public IBlobStore CreateBlobStore() => new LocalBlobStore();
public IMessageQueue CreateQueue() => new LocalQueue();
public ISecretStore CreateSecretStore() => new DictionarySecretStore("environment", new() { ["print-api-key"] = "dev-key" });
}
GoF'un rol adlarıyla: ICloudFactory abstract factory, AzureCloudFactory, AwsCloudFactory ve LocalCloudFactory concrete factory'ler; IBlobStore, IMessageQueue ve ISecretStore abstract product'lar, simülasyondaki class'lar da concrete product'lar. Dikkat edersen her concrete factory'nin her method'u aslında bir factory method; GoF de Abstract Factory'lerin çoğunlukla factory method'larla implement edildiğini söyler. İki pattern'in farkı rolde: Factory Method tek bir ürünün kararını alt class'a bırakır, Abstract Factory ise bir ürün ailesinin tamamını tek bir object'te toplar.
GoF bu pattern'in sonuçlarını dört maddede toplar. Concrete class'ları client'tan saklar: uygulama kodu S3BlobStore adını hiç görmez. Aile değiştirmeyi kolaylaştırır: bütün aile tek bir factory instance'ını değiştirerek değişir. Ürünler arasında tutarlılığı sağlar: aynı factory'den gelen ürünler aynı aileden olmak zorunda. Dördüncü madde ise bir uyarı: aileye yeni bir ürün türü eklemek zordur. Bu sonuncusuna biraz sonra döneceğiz.
Seçimi Configuration'a Bırakmak
Factory'yi seçmek uygulama başlarken bir kez yapılan bir karar ve doğal yeri DI (dependency injection) registration'ı. Ürünler de factory'den türetilerek container'a kaydediliyor; uygulama kodu factory'yi bile görmeden doğrudan IBlobStore ve IMessageQueue enjekte edebiliyor:
using System.Text;
using Bookstore.Cloud;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
IConfiguration config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?> { ["Cloud:Provider"] = "Local" })
.AddCommandLine(args)
.Build();
ServiceCollection services = new();
// Tek karar noktası: aile bir kez seçilir, ürünler o aileden gelir
services.AddSingleton<ICloudFactory>(config["Cloud:Provider"] switch
{
"Azure" => new AzureCloudFactory(),
"Aws" => new AwsCloudFactory(),
"Local" => new LocalCloudFactory(),
null => throw new InvalidOperationException("Cloud:Provider is missing."),
string other => throw new InvalidOperationException($"Unknown cloud provider '{other}'.")
});
services.AddSingleton(sp => sp.GetRequiredService<ICloudFactory>().CreateBlobStore());
services.AddSingleton(sp => sp.GetRequiredService<ICloudFactory>().CreateQueue());
services.AddSingleton(sp => sp.GetRequiredService<ICloudFactory>().CreateSecretStore());
using ServiceProvider provider = services.BuildServiceProvider();
IBlobStore blobs = provider.GetRequiredService<IBlobStore>();
IMessageQueue queue = provider.GetRequiredService<IMessageQueue>();
ISecretStore secrets = provider.GetRequiredService<ISecretStore>();
queue.Send(blobs.Upload("invoices/1042.pdf", Encoding.UTF8.GetBytes("%PDF invoice 1042")));
string message = queue.Receive()!;
Console.WriteLine($"Family: {provider.GetRequiredService<ICloudFactory>().Name}");
Console.WriteLine($"Message: {message}");
Console.WriteLine($"Content: {Encoding.UTF8.GetString(blobs.Download(message))}");
Console.WriteLine($"Api key: {secrets.Get("print-api-key")}");
Aynı derlenmiş uygulamayı üç farklı ayarla çalıştırdım:
$ dotnet run
Family: Local
Message: file://invoices/1042.pdf
Content: %PDF invoice 1042
Api key: dev-key
$ dotnet run -- --Cloud:Provider=Aws
Family: Aws
Message: s3://invoices/1042.pdf
Content: %PDF invoice 1042
Api key: sm-456
$ dotnet run -- --Cloud:Provider=Gcp
Unhandled exception. System.InvalidOperationException: Unknown cloud provider 'Gcp'.
Artık configuration'da ürün başına değil aile başına tek bir ayar var: Cloud:Provider. Yarım geçiş yapmak mümkün değil; ya bütün aile AWS'de ya hiçbiri. Bilinmeyen bir değer de sessizce yerel aileye düşmüyor, uygulama başlarken açık bir mesajla duruyor. Gerçek bir ASP.NET Core uygulamasında aynı kayıt builder.Configuration üzerinden yapılır ve appsettings.Production.json ile ortam bazında değişir; ASP.NET Core Web API (Application Programming Interface) serisindeki Configuration ve Options Pattern yazısı bu katmanları ayrıntılı anlatıyor.
Ürünleri singleton olarak kaydettim çünkü gerçek SDK'ların client'ları (BlobContainerClient, AmazonSQSClient gibi) thread-safe tasarlanmış ve uygulama boyunca tek instance olarak kullanılması öneriliyor. Concrete factory'lerin kendileri de genelde tek instance olur; GoF da bu yüzden Abstract Factory'nin sık sık Singleton ile birlikte geldiğini söyler. DI dünyasında bu, factory'yi AddSingleton ile kaydetmek demek; önceki yazıda gördüğümüz gibi klasik Singleton'a gerek yok.
ADO.NET'in DbProviderFactory'si: .NET'teki Gerçek Abstract Factory
.NET'in kendi kütüphanesinde bu pattern'in en eski ve en saf örneği ADO.NET'te (.NET'in veri erişim katmanı; adını eski ActiveX Data Objects teknolojisinden alır). DbProviderFactory bir veritabanı sağlayıcısının bütün ürün ailesini üretir: connection, command, parameter, data adapter, command builder ve .NET 6'dan beri batch. SQLite ve SQL (Structured Query Language) Server sağlayıcılarını yan yana koyalım:
using System.Data.Common;
using Microsoft.Data.SqlClient;
using Microsoft.Data.Sqlite;
// Uygulama başlarken provider'lar invariant name ile kaydedilir
DbProviderFactories.RegisterFactory("Microsoft.Data.Sqlite", SqliteFactory.Instance);
DbProviderFactories.RegisterFactory("Microsoft.Data.SqlClient", SqlClientFactory.Instance);
foreach (string name in DbProviderFactories.GetProviderInvariantNames())
{
DbProviderFactory factory = DbProviderFactories.GetFactory(name);
Console.WriteLine($"{name}:");
Console.WriteLine($" {factory.CreateConnection()!.GetType().Name}, {factory.CreateCommand()!.GetType().Name}, {factory.CreateParameter()!.GetType().Name}");
Console.WriteLine($" CanCreateDataAdapter={factory.CanCreateDataAdapter}, CanCreateCommandBuilder={factory.CanCreateCommandBuilder}, CanCreateBatch={factory.CanCreateBatch}");
}
// Provider'dan bağımsız kod: yalnızca DbProviderFactory'yi tanıyor
Console.WriteLine($"Books: {CountBooks(DbProviderFactories.GetFactory("Microsoft.Data.Sqlite"), "Data Source=:memory:")}");
try
{
SqliteFactory.Instance.CreateBatch();
}
catch (NotSupportedException ex)
{
Console.WriteLine($"SqliteFactory.CreateBatch: {ex.GetType().Name}");
}
static long CountBooks(DbProviderFactory factory, string connectionString)
{
using DbConnection connection = factory.CreateConnection()!;
connection.ConnectionString = connectionString;
connection.Open();
using DbCommand create = connection.CreateCommand();
create.CommandText = "CREATE TABLE Books (Isbn TEXT PRIMARY KEY, Title TEXT NOT NULL)";
create.ExecuteNonQuery();
foreach ((string isbn, string title) in new[] { ("978-0441013593", "Dune"), ("978-0141439518", "Emma") })
{
using DbCommand insert = connection.CreateCommand();
insert.CommandText = "INSERT INTO Books (Isbn, Title) VALUES (@isbn, @title)";
DbParameter p1 = factory.CreateParameter()!;
p1.ParameterName = "@isbn";
p1.Value = isbn;
DbParameter p2 = factory.CreateParameter()!;
p2.ParameterName = "@title";
p2.Value = title;
insert.Parameters.Add(p1);
insert.Parameters.Add(p2);
insert.ExecuteNonQuery();
}
using DbCommand count = connection.CreateCommand();
count.CommandText = "SELECT COUNT(*) FROM Books";
return (long)count.ExecuteScalar()!;
}
Microsoft.Data.SqlClient:
SqlConnection, SqlCommand, SqlParameter
CanCreateDataAdapter=True, CanCreateCommandBuilder=True, CanCreateBatch=True
Microsoft.Data.Sqlite:
SqliteConnection, SqliteCommand, SqliteParameter
CanCreateDataAdapter=False, CanCreateCommandBuilder=False, CanCreateBatch=False
Books: 2
SqliteFactory.CreateBatch: NotSupportedException
CountBooks SQLite'ı tanımıyor; yalnızca DbProviderFactory, DbConnection, DbCommand ve DbParameter ile konuşuyor. Factory SQLite ailesinden geldiği için connection da command da parameter da aynı aileden ve birbirleriyle uyumlu. SQL Server'ın factory'sini verseydin aynı kod (SQL'i değişmeden) SqlConnection ve SqlCommand ile çalışırdı. Gerçek uygulamalarda provider'ları birden çok desteklemek isteyen library'ler (ör. raporlama araçları, migration araçları) bu yolu kullanır. Dapper serisinde gördüğümüz Dapper'ın extension method'ları da aynı ADO.NET soyutlamasının (IDbConnection) üzerinde çalışıyor.
Çıktının asıl ilginç kısmı ikinci ve dördüncü satırlar: SQL Server ailesi data adapter, command builder ve batch üretebiliyor, SQLite ailesi üretemiyor. Ve SqliteFactory.CreateBatch() çağrısı NotSupportedException fırlattı. Bu, GoF'un dördüncü uyarısının .NET'teki canlı hâli.
Aileye Yeni Ürün Eklemek: GoF'un Kendi Uyarısı
Abstract Factory aile eklemeyi ucuzlatır ama ürün eklemeyi pahalılaştırır. Bookstore'un Azure ailesine bir cache eklemek istediğimizi düşünelim ve bunu en doğal yoldan, interface'e bir method ekleyerek yapalım:
public interface ICloudFactory
{
string Name { get; }
IBlobStore CreateBlobStore();
IMessageQueue CreateQueue();
ISecretStore CreateSecretStore();
ICache CreateCache(); // aileye yeni ürün
}
public interface ICache
{
void Set(string key, string value);
}
AzureCloudFactory'ye CreateCache'i eklediğimiz hâlde build şunu söylüyor:
Factories.cs(34,39): error CS0535: 'AwsCloudFactory' does not implement interface member 'ICloudFactory.CreateCache()'
Factories.cs(42,41): error CS0535: 'LocalCloudFactory' does not implement interface member 'ICloudFactory.CreateCache()'
Yeni bir ürün, abstract factory'nin interface'ini ve dolayısıyla onu implement eden bütün concrete factory'leri değiştirir. Senin kodunda üç factory var ve üçü de senin elinde; iş bir öğleden sonra sürer. ADO.NET'te ise concrete factory'ler başka şirketlerin yazdığı sağlayıcılarda (Npgsql, MySqlConnector, Oracle) yaşıyor. Microsoft .NET 6'da batch'i eklerken abstract bir method ekleseydi dünyadaki bütün sağlayıcıların derlenmesi bozulurdu. Bunun yerine bir virtual CreateBatch() (varsayılanı NotSupportedException) ve bir virtual CanCreateBatch (varsayılanı false) eklendi. Sağlayıcılar kendi sürümlerinde destek ekledikçe override ettiler; SQL Server'ınki etti, SQLite'ınki henüz etmedi. Aynı yolu Bookstore'da da izleyebiliriz:
namespace Bookstore.Cloud;
public interface ICache
{
void Set(string key, string value);
}
public sealed class AzureCache : ICache
{
public void Set(string key, string value) { }
}
// ADO.NET'in DbProviderFactory'sindeki yol: yeni ürün virtual + Can* property ile gelir
public abstract class CloudFactory
{
public abstract string Name { get; }
public abstract IBlobStore CreateBlobStore();
public abstract IMessageQueue CreateQueue();
public virtual bool CanCreateCache => false;
public virtual ICache CreateCache() => throw new NotSupportedException($"{Name} family has no cache yet.");
}
public sealed class AzureCloudFactory : CloudFactory
{
public override string Name => "Azure";
public override IBlobStore CreateBlobStore() => new AzureBlobStore();
public override IMessageQueue CreateQueue() => new AzureStorageQueue();
public override bool CanCreateCache => true;
public override ICache CreateCache() => new AzureCache();
}
public sealed class AwsCloudFactory : CloudFactory
{
public override string Name => "Aws";
public override IBlobStore CreateBlobStore() => new S3BlobStore();
public override IMessageQueue CreateQueue() => new SqsQueue();
}
C#'ta üçüncü bir seçenek, interface'te default implementation'lı bir method (C# 8 ile gelen default interface method): ICache CreateCache() => throw new NotSupportedException();. Etkisi abstract base class'taki virtual method'a benziyor ama bir farkla: default implementation'a yalnızca interface tipi üzerinden erişilebilir, concrete class üzerinden çağıramazsın. Üç yolun da bedeli aynı: derleyicinin "bu ailede cache yok" bilgisini compile time'da yakalaması yerine runtime'da bir kontrol (CanCreateCache) ya da bir exception. Tutarlılık garantisi biraz zayıflıyor, karşılığında aileye ürün eklemek bütün ekosistemi kırmıyor. Hangisini seçeceğin, concrete factory'lerin kimin elinde olduğuna bağlı.
Aynı Anda İki Aile: Keyed Services ile Tenant Başına Factory
Şimdiye kadarki örneklerde uygulama başlarken bir aile seçiliyordu. Abstract Factory'nin gerçekten parladığı yer, aynı process'te birden fazla ailenin birlikte yaşaması. Bookstore'un franchise mağazalarını barındırdığını ve her mağazanın (tenant) kendi bulutunu seçebildiğini düşün. .NET 8 ile gelen keyed services, factory'leri adla kaydedip runtime'da adla çözmeyi sağlıyor:
using System.Text;
using Bookstore.Cloud;
using Microsoft.Extensions.DependencyInjection;
ServiceCollection services = new();
services.AddKeyedSingleton<CloudFactory, AzureCloudFactory>("Azure");
services.AddKeyedSingleton<CloudFactory, AwsCloudFactory>("Aws");
services.AddSingleton<InvoicePublisher>();
using ServiceProvider provider = services.BuildServiceProvider();
InvoicePublisher publisher = provider.GetRequiredService<InvoicePublisher>();
// Aynı uygulamada iki aile birden: her tenant kendi bulutunda
foreach (Tenant tenant in new Tenant[] { new("north-books", "Azure"), new("east-books", "Aws") })
Console.WriteLine(publisher.Publish(tenant, 1042));
public sealed record Tenant(string Id, string Cloud);
public sealed class InvoicePublisher(IServiceProvider services)
{
public string Publish(Tenant tenant, int invoiceNo)
{
CloudFactory cloud = services.GetRequiredKeyedService<CloudFactory>(tenant.Cloud);
IBlobStore blobs = cloud.CreateBlobStore();
IMessageQueue queue = cloud.CreateQueue();
queue.Send(blobs.Upload($"{tenant.Id}/invoices/{invoiceNo}.pdf", Encoding.UTF8.GetBytes("%PDF")));
if (cloud.CanCreateCache)
cloud.CreateCache().Set($"invoice:{invoiceNo}", "published");
return $"{tenant.Id,-12} {queue.Receive()} (cache: {cloud.CanCreateCache})";
}
}
north-books azure-blob://north-books/invoices/1042.pdf (cache: True)
east-books s3://east-books/invoices/1042.pdf (cache: False)
İki tenant aynı InvoicePublisher'ı kullanıyor ama her biri kendi ailesinin ürünlerini alıyor; bir tenant'ın blob'u yanlışlıkla diğerinin queue'suna gidemez. CanCreateCache kontrolü de bir önceki bölümdeki kararın bedelini gösteriyor: AWS ailesinde cache yok ve kod bunu runtime'da soruyor.
InvoicePublisher'ın IServiceProvider alması bilinçli bir taviz. Factory'yi tenant'a göre runtime'da seçmek zorundayız ve bunu ya container ile ya da composition root'ta kurulmuş bir IReadOnlyDictionary<string, CloudFactory> ile yapabiliriz. Büyük bir uygulamada ikincisini, ya da container'ı bilen küçük bir ICloudFactoryResolver interface'ini tercih ederdim; bir önceki yazıda gördüğümüz service locator uyarısı burada da geçerli.
Factory Method, Abstract Factory ve DI: Hangisi Ne Zaman
Üç kavram sık sık karışıyor, çünkü hepsi "oluşturmayı başka bir yere taşımak" ile ilgili. Farkları, cevap verdikleri soruda:
| Araç | Cevapladığı soru | Değişim noktası | .NET örneği |
|---|---|---|---|
| Static factory method | Bu tipi nasıl, hangi adla oluşturmalı? | Tipin kendisi | Channel.CreateBounded |
| Factory Method | Akış aynı, ürünü kim seçsin? | Alt class | DbConnection.CreateDbCommand |
| Abstract Factory | Birbirine uyumlu bir ürün ailesini kim üretsin? | Factory object'i | DbProviderFactory |
| DI container | Bu interface'in implementation'ı ve ömrü ne? | Registration | AddSingleton, AddKeyedSingleton |
Pratikte bu araçlar birbirinin rakibi değil, birlikte çalışıyor: Abstract Factory'nin method'ları factory method, abstract factory container'a kayıtlı bir singleton, ürünleri de container üzerinden enjekte ediliyor. Önemli olan, her seviyede hangi sorunun cevaplandığını bilmek.
Sık Düşülen Tuzaklar
Ürünleri tek tek ayarlardan seçmek. Yazının başındaki hata bunun ta kendisi. Ürünler bir aile oluşturuyorsa, configuration'da da tek bir aile ayarı olmalı. Ürün başına ayar, geçersiz kombinasyonları mümkün kılar ve hiçbir derleyici onları yakalamaz.
Abstract factory'yi tek ürün için yazmak. İçinde yalnızca CreateBlobStore() olan bir ICloudFactory, bir aile değil tek bir ürün üretiyor; ona Abstract Factory demek yalnızca bir dolaylılık eklemek. Tek ürünü seçmek için DI registration ya da önceki yazıdaki factory yeter.
Generic bir Create<T>() ile "esnek" factory yazmak. GoF'un implementation bölümü, ürün türünü parametreyle alan tek bir Make(kind) method'unu "genişletilebilir factory" olarak anar ve bedelini de söyler: tip güvenliği gider, client dönen object'i cast etmek zorunda kalır. C#'ta bu genelde container'ı başka adla taşıyan bir service locator'a dönüşür.
Concrete product'ları aile dışından da oluşturulabilir bırakmak. new S3BlobStore() uygulama kodunun herhangi bir yerinde yazılabiliyorsa, factory'nin tutarlılık garantisi bir rica olarak kalır. Concrete class'ları internal yapıp yalnızca factory'leri ve interface'leri public açmak, garantiyi derleyiciye devreder.
Simülasyonu gerçek sanmak. Bu yazıdaki aileler bellekte çalışıyor ve gerçek bulutların bütün farklarını (tutarlılık modeli, mesaj boyutu limiti, retry davranışı) yansıtmıyor. Gerçek bir geçişte bu farklar, abstraction'ın sızdığı yerlerdir ve contract test'leri gerektirir.
Ne Zaman Kullanma
Çoğu uygulama production'da tek bir aile kullanır ve hiç değiştirmez. "Belki bir gün AWS'ye geçeriz" diye bugünden ICloudFactory kurmak, değişimin yönünü bilmeden duvar örmektir; SOLID (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) ve Design Principles serisindeki DRY (Don't Repeat Yourself), KISS (Keep It Simple, Stupid) ve YAGNI (You Aren't Gonna Need It) yazısında bunun bedelini konuşmuştuk. Tek ailede kalacaksan ürünleri doğrudan container'a kaydet; test için sahte ailen varsa bunu test projesinin registration'ında yap. Abstract Factory'yi, ailelerin gerçekten birlikte var olduğu (çok bulutlu tenant'lar, aynı anda birden fazla veritabanını destekleyen bir library) ya da aile tutarlılığının bozulmasının pahalıya patladığı yerlere sakla. Ve ürün ailesinin sık sık büyüyeceğini biliyorsan, interface yerine ADO.NET'in yolunu, yani virtual method ve Can* property'lerini baştan düşün.
Küçük Bir Deneme
★☆☆ LocalCloudFactory'ye bir CreateSecretStore testi ekle: "print-api-key" dışında bir secret istendiğinde gelen exception mesajını yazdır. Mesaj hangi ailenin hangi kaynağına bakıldığını söylüyor mu?
★★☆ Concrete product class'larını ayrı bir class library'ye taşı, internal yap ve yalnızca factory'leri public bırak. Uygulama projesinde new S3BlobStore() yazmayı dene ve derleyicinin verdiği hata kodunu not et.
★★★ DbProviderFactory örneğini Docker'da çalışan bir SQL Server'a bağlanacak şekilde genişlet: aynı CountBooks fonksiyonunu iki provider ile çalıştır. SQL'de hangi satırı değiştirmen gerekti? Abstract Factory, SQL lehçe farklarını neden çözmüyor?
Bir sonraki yazıda creational pattern'lerin belki de en çok yanlış anlaşılanına geçiyoruz: Builder. GoF'un director'lı Builder'ı ile fluent builder'ın neden aynı şey olmadığını, required ve init'in telescoping constructor problemini nereye kadar çözdüğünü ve WebApplicationBuilder'ın Build()'den sonra neden kilitlendiğini göreceğiz.