Gdy aplikacja obsługuje kilka kanałów płatności, formatów raportów albo dostawców AI, szybko pojawia się pytanie, kto ma zdecydować, jaki obiekt utworzyć. Angielskie określenie c# factory pattern opisuje właśnie sposób przeniesienia tej decyzji poza kod biznesowy. Pokażę, jak zaimplementować wzorzec Factory w C#, czym różni się Simple Factory od Factory Method i Abstract Factory oraz jak połączyć fabrykę z dependency injection w .NET.
Fabryka porządkuje wybór implementacji i upraszcza rozwój aplikacji
- Oddziel decyzję o wyborze klasy od kodu, który korzysta z obiektu.
- Simple Factory sprawdza się przy prostym wyborze jednej implementacji.
- Factory Method przenosi tworzenie obiektu do klas pochodnych.
- Abstract Factory tworzy całe rodziny powiązanych obiektów.
-
Dependency injection ogranicza ręczne używanie
newi ułatwia testowanie.

Po co wydzielać tworzenie obiektów
Najprostszy kod często zaczyna się od instrukcji new umieszczonej dokładnie tam, gdzie obiekt jest potrzebny. Problem pojawia się wtedy, gdy jedna część aplikacji musi obsługiwać kilka implementacji tego samego interfejsu, a wybór zależy od konfiguracji, typu żądania lub danych użytkownika.
Bez fabryki warunki wyboru szybko rozlewają się po kontrolerach, serwisach i handlerach. Każda zmiana, na przykład dodanie eksportu XML, wymaga znalezienia wielu miejsc, w których występuje podobny if albo switch. Fabryka zbiera tę wiedzę w jednym miejscu, a reszta aplikacji pracuje na abstrakcji.
W praktyce zyskujemy trzy rzeczy. Po pierwsze, klient nie zna klas konkretnych. Po drugie, konstrukcja obiektu może obejmować walidację i dodatkowe zależności. Po trzecie, test może otrzymać prostą atrapę zamiast prawdziwego klienta API, generatora plików czy dostawcy płatności.
Nie traktuję jednak fabryki jako obowiązkowej warstwy przed każdym konstruktorem. Jeśli klasa ma prosty konstruktor, jedną implementację i nie ma żadnej decyzji do ukrycia, dodatkowa fabryka będzie tylko dekoracją.
Trzy odmiany fabryki, które łatwo pomylić
W projektach C# słowo „fabryka” bywa używane dla kilku różnych rozwiązań. To nie jest wyłącznie kwestia nazewnictwa, ponieważ każda odmiana rozwiązuje inny problem z tworzeniem obiektów.
| Wariant | Co tworzy | Kiedy ma sens |
|---|---|---|
| Simple Factory | Jeden obiekt wybrany na podstawie parametru | Mała liczba wariantów i prosta reguła wyboru |
| Factory Method | Obiekt tworzony przez klasę pochodną | Gdy algorytm jest wspólny, ale produkt zależy od podklasy |
| Abstract Factory | Rodzinę kompatybilnych obiektów | Gdy kilka produktów musi pochodzić z tego samego wariantu |
Simple Factory
Simple Factory to zwykle zwykła klasa z metodą Create. Nie jest osobnym wzorcem z katalogu GoF, ale w codziennym kodzie .NET jest bardzo praktycznym sposobem na schowanie prostego wyboru implementacji.
public enum ReportFormat
{
Csv,
Json
}
public interface IReportExporter
{
string Export(Report report);
}
public sealed class ReportExporterFactory
{
public IReportExporter Create(ReportFormat format) => format switch
{
ReportFormat.Csv => new CsvReportExporter(),
ReportFormat.Json => new JsonReportExporter(),
_ => throw new ArgumentOutOfRangeException(nameof(format), format, null)
};
}
Ten wariant jest czytelny, dopóki liczba formatów pozostaje niewielka. Jeśli fabryka zaczyna zawierać kilkadziesiąt warunków, a każda implementacja wymaga wielu zależności, przeniesienie instrukcji switch do osobnej klasy nie rozwiązuje problemu. Wtedy lepiej użyć rejestru obiektów albo konfiguracji DI.
Factory Method
W Factory Method klasa bazowa definiuje ogólny przebieg działania, ale pozostawia klasom pochodnym decyzję, jaki produkt utworzyć. To dobre rozwiązanie, gdy algorytm jest stały, a tylko jeden etap zależy od konkretnego typu.
public abstract class DocumentProcessor
{
public void Process(string content)
{
var parser = CreateParser();
var document = parser.Parse(content);
Save(document);
}
protected abstract IDocumentParser CreateParser();
private static void Save(Document document)
{
Console.WriteLine($"Zapisano dokument: {document.Title}");
}
}
public sealed class CsvDocumentProcessor : DocumentProcessor
{
protected override IDocumentParser CreateParser()
{
return new CsvDocumentParser();
}
}
Klasa bazowa nie musi znać szczegółów parsera CSV, XML czy JSON. Wywołuje metodę fabrykującą, a podklasa dostarcza właściwą implementację. Ten wariant bywa jednak cięższy niż zwykła fabryka, dlatego sięgam po niego głównie wtedy, gdy rzeczywiście istnieje wspólny szablon procesu.
Przeczytaj również: Mikroserwisy w .NET bez chaosu - granice, wzorce i wdrożenie
Abstract Factory
Abstract Factory przydaje się wtedy, gdy nie tworzysz jednego obiektu, tylko zestaw obiektów, które powinny do siebie pasować. Dobrym przykładem może być interfejs użytkownika dla różnych platform albo zestaw usług płatniczych jednego dostawcy.
public interface IPaymentFactory
{
IPaymentProcessor CreateProcessor();
IRefundProcessor CreateRefundProcessor();
}
public sealed class StripePaymentFactory : IPaymentFactory
{
public IPaymentProcessor CreateProcessor()
{
return new StripePaymentProcessor();
}
public IRefundProcessor CreateRefundProcessor()
{
return new StripeRefundProcessor();
}
}
public sealed class CheckoutService
{
private readonly IPaymentFactory _factory;
public CheckoutService(IPaymentFactory factory)
{
_factory = factory;
}
public void Process(decimal amount)
{
var processor = _factory.CreateProcessor();
processor.Charge(amount);
}
}
Największą wartością nie jest samo ukrycie operatora new. Chodzi o to, aby kod nie połączył przypadkiem procesora Stripe z mechanizmem zwrotów PayPal. Fabryka abstrakcyjna pilnuje spójności całej rodziny produktów.
Praktyczna implementacja fabryki w C#
Załóżmy, że aplikacja generuje raporty w formacie CSV i JSON. Kod biznesowy powinien powiedzieć „potrzebuję eksportera JSON”, ale nie powinien wiedzieć, jak ten eksporter jest zbudowany ani jakie biblioteki wykorzystuje.
using System.Text.Json;
public sealed record Report(string Title, IReadOnlyList Rows);
public enum ExportFormat
{
Csv,
Json
}
public interface IReportExporter
{
ExportFormat Format { get; }
string Export(Report report);
}
public sealed class CsvReportExporter : IReportExporter
{
public ExportFormat Format => ExportFormat.Csv;
public string Export(Report report)
{
return string.Join(Environment.NewLine, report.Rows);
}
}
public sealed class JsonReportExporter : IReportExporter
{
public ExportFormat Format => ExportFormat.Json;
public string Export(Report report)
{
return JsonSerializer.Serialize(report);
}
}
public sealed class ReportExporterFactory
{
private readonly IReadOnlyDictionary _exporters;
public ReportExporterFactory(IEnumerable exporters)
{
_exporters = exporters.ToDictionary(exporter => exporter.Format);
}
public IReportExporter Create(ExportFormat format)
{
if (_exporters.TryGetValue(format, out var exporter))
{
return exporter;
}
throw new ArgumentOutOfRangeException(nameof(format), format, "Nieobsługiwany format.");
}
}
W tym przykładzie fabryka nie zna klas konkretnych w swoim kodzie. Otrzymuje kolekcję implementacji i buduje mapę na podstawie właściwości Format. Dzięki temu dodanie nowego eksportera zwykle polega na dopisaniu klasy i rejestracji, a nie na rozbudowywaniu wielkiego switch.
Użycie jest proste:
var factory = new ReportExporterFactory(
new IReportExporter[]
{
new CsvReportExporter(),
new JsonReportExporter()
});
var report = new Report("Sprzedaż", new[] { "styczeń", "luty" });
var exporter = factory.Create(ExportFormat.Json);
var output = exporter.Export(report);
Metoda Create zwraca interfejs, więc kod wywołujący nie musi znać typu JsonReportExporter. To drobna różnica w kodzie, ale duża różnica w architekturze, gdy implementacje zaczynają mieć własne zależności, konfigurację i zasady walidacji.
Jak połączyć fabrykę z dependency injection
W aplikacji ASP.NET Core ręczne tworzenie eksporterów nie jest już potrzebne. Wbudowany kontener DI może zarejestrować wiele implementacji tego samego interfejsu, a fabryka otrzyma je przez IEnumerable.
services.AddTransient();
services.AddTransient();
services.AddTransient();
Taki układ dobrze działa, gdy wybór zależy od danych przychodzących w żądaniu. Fabryka pozostaje miejscem, w którym opisujemy regułę wyboru, a kontener DI zajmuje się budowaniem zależności.
Jeżeli konstruktor implementacji wymaga konfiguracji, można użyć fabryki delegatowej podczas rejestracji:
services.AddTransient(serviceProvider =>
{
var options = serviceProvider
.GetRequiredService>();
return new JsonReportExporter(options.Value);
});
Nie polecam wstrzykiwania IServiceProvider do każdej klasy tylko po to, aby pobierać z niego zależności w metodzie biznesowej. To szybko zamienia fabrykę w Service Locator, czyli ukryty mechanizm rozwiązywania zależności. Lepiej pobrać potrzebne implementacje w konstruktorze albo zamknąć logikę w jasno nazwanej fabryce.
W większym systemie fabryka dobrze współgra z architekturą heksagonalną w .NET. Interfejs eksportera może być portem aplikacji, a klasy CSV i JSON adapterami technicznymi. Dzięki temu logika biznesowa nie zależy bezpośrednio od biblioteki do serializacji ani od szczegółów zapisu pliku.
W .NET 8 i nowszych można także użyć usług kluczowanych, gdy wybór sprowadza się do prostego klucza, na przykład "csv" lub "json". Przy bardziej rozbudowanych regułach jawna fabryka zazwyczaj lepiej pokazuje intencję i łatwiej ją przetestować.
Kiedy fabryka pomaga, a kiedy komplikuje kod
Najlepszy moment na wprowadzenie fabryki pojawia się wtedy, gdy tworzenie obiektu zaczyna być decyzją architektoniczną, a nie zwykłym wywołaniem konstruktora. W moich projektach najczęściej ma to miejsce w czterech sytuacjach.
- Istnieje kilka implementacji jednego kontraktu, a wybór następuje w czasie działania programu.
- Konstrukcja obiektu wymaga konfiguracji, walidacji lub kilku zależności.
- Wiele miejsc w aplikacji powtarza ten sam warunek wyboru.
- Trzeba pilnować, aby kilka obiektów pochodziło z jednej kompatybilnej rodziny.
Fabryka nie jest dobrym wyborem, gdy tworzy jedną klasę i tylko przekazuje parametry do jej konstruktora. W takim przypadku bezpośrednie użycie new jest często czytelniejsze i tańsze w utrzymaniu.
Trzeba też uważać na fabrykę, która staje się centralnym magazynem wszystkich klas w systemie. Jeżeli jedna metoda zna kilkanaście niezwiązanych ze sobą typów, problemem nie jest brak kolejnego warunku, tylko zbyt szeroka odpowiedzialność. Lepiej podzielić ją według obszarów biznesowych albo wykorzystać rejestrację implementacji w DI.
Przydatne jest również rozróżnienie fabryki od pozostałych narzędzi architektonicznych. Szerszy przegląd znajdziesz w materiale o wzorcach projektowych w .NET, ale w praktyce najważniejsze pytanie brzmi prosto: czy ukrywam zmienną decyzję, czy tylko dokładam klasę bez realnej korzyści?
Fabryka powinna upraszczać decyzję, nie ją ukrywać
Najzdrowsza implementacja wzorca Factory ma jasno określoną odpowiedzialność. Wie, jak wybrać właściwą implementację, ale nie przejmuje logiki biznesowej produktu, obsługi całego procesu ani zarządzania cyklem życia wszystkich usług.
- Zwracaj interfejs lub klasę abstrakcyjną, jeśli klient nie musi znać typu konkretnego.
- Obsłuż nieznany wariant kontrolowanym wyjątkiem lub walidacją konfiguracji.
- Testuj osobno regułę wyboru i każdą implementację produktu.
- Przy rosnącej liczbie wariantów rozważ rejestr obiektów oraz dependency injection.
Jeśli wybór jest prosty, zacznij od małej fabryki. Jeśli tworzysz kompatybilne zestawy obiektów, wybierz Abstract Factory. Najważniejsze jest nie samo użycie nazwy wzorca, lecz usunięcie zmienności z miejsca, które powinno zajmować się pracą biznesową.
