Wzorce projektowe C# - kiedy używać Strategy, Factory i Decorator?

Bruno Krawczyk 21 sierpnia 2026
Wybór c# design patterns: Factory, Builder, Strategy, Adapter, Facade. Pomaga wybrać odpowiedni wzorzec projektowy.

Spis treści

Gdy aplikacja w C# zaczyna rosnąć, problemem rzadko jest brak klas. Trudniej zdecydować, gdzie umieścić logikę, jak wymieniać implementacje i jak nie zamienić prostego modułu w plątaninę zależności. W tym tekście pokazuję, jak rozumieć wzorce projektowe C# opisane w anglojęzycznych materiałach jako c# design patterns, kiedy używać Strategy, Factory, Decorator czy Adapter oraz jak połączyć je z dependency injection w nowoczesnym .NET.

Najważniejsze decyzje, które ułatwiają projektowanie kodu w C#

  • Wzorzec rozwiązuje konkretny problem, a nie jest ozdobą architektury.
  • Strategy dobrze sprawdza się przy wymiennych algorytmach i regułach biznesowych.
  • Factory porządkuje tworzenie obiektów, gdy wybór implementacji zależy od danych lub konfiguracji.
  • Decorator pozwala dodać logowanie, cache albo autoryzację bez modyfikowania głównej klasy.
  • Dependency injection łączy wzorce i zarządza zależnościami, ale nie zastępuje dobrego projektu.
  • Nadmiar wzorców zwiększa liczbę abstrakcji i może utrudnić rozwój zamiast go przyspieszyć.

Po co w ogóle używać wzorców projektowych w C#

Wzorzec projektowy to sprawdzony sposób organizowania współpracujących klas. Nie jest gotową biblioteką ani fragmentem kodu do skopiowania, tylko nazwanym rozwiązaniem powtarzalnego problemu. Dzięki wspólnemu słownictwu łatwiej powiedzieć, że dana klasa pełni rolę strategii, fabryki albo dekoratora, zamiast długo opisywać jej strukturę.

Największą wartością wzorców nie jest sama elegancja kodu. Chodzi o to, by zmiana, której spodziewamy się za miesiąc, wymagała modyfikacji w jednym miejscu. Jeżeli sklep internetowy ma obsługiwać płatności kartą, BLIK-iem i przelewem, lepiej oddzielić algorytmy płatności niż budować jeden serwis z rozrastającym się łańcuchem if-else.

W praktyce wzorce pomagają przede wszystkim w czterech obszarach:

  • zmienność, czyli wymiana algorytmu lub implementacji bez przebudowy całej aplikacji,
  • tworzenie obiektów, gdy konstrukcja jest bardziej złożona niż pojedyncze new,
  • kompozycja zachowań, na przykład dodanie cache albo logowania,
  • testowalność, ponieważ zależności można zastąpić atrapami lub implementacjami testowymi.

Moja praktyczna zasada jest prosta. Najpierw identyfikuję zmianę, która regularnie wraca w kodzie, a dopiero później szukam wzorca. Odwrotna kolejność często kończy się klasami nazwanymi AbstractSomethingFactoryManager, które wyglądają profesjonalnie, ale rozwiązują problem, którego nikt naprawdę nie miał.

Najbardziej użyteczne wzorce w codziennym .NET

Nie wszystkie wzorce z klasycznego katalogu GoF mają dziś takie samo znaczenie. C# i biblioteki .NET oferują delegaty, zdarzenia, pattern matching, generics oraz wbudowany kontener DI, więc część dawnych konstrukcji można zapisać krócej. Najwięcej praktycznej wartości widzę w kilku wzorcach, które dobrze pasują do aplikacji webowych, usług Azure i systemów przetwarzających dane.

Strategy dla wymiennych reguł

Strategy oddziela algorytm od obiektu, który z niego korzysta. Dobrym przykładem jest naliczanie rabatu, wybór sposobu wysyłki albo filtrowanie danych według różnych kryteriów.

public interface IDiscountStrategy
{
    decimal Calculate(Order order);
}

public sealed class StandardDiscount : IDiscountStrategy
{
    public decimal Calculate(Order order) =>
        order.Total >= 500m ? order.Total * 0.95m : 0m;
}

public sealed class CustomerDiscount : IDiscountStrategy
{
    public decimal Calculate(Order order) =>
        order.IsPremium ? order.Total * 0.10m : 0m;
}

Klasa obsługująca zamówienie nie musi znać szczegółów obu algorytmów. Otrzymuje wybraną strategię i wywołuje ten sam kontrakt. To rozwiązanie jest szczególnie wygodne, gdy liczba wariantów rośnie, a każdy wariant powinien być testowany niezależnie.

Factory dla kontrolowanego tworzenia obiektów

Factory centralizuje decyzję o tym, jaki obiekt utworzyć. Przydaje się wtedy, gdy wybór zależy od typu dokumentu, dostawcy, konfiguracji albo danych wejściowych. Nie ma sensu budować fabryki dla jednej klasy z prostym konstruktorem, ale przy kilku implementacjach szybko porządkuje kod.

public interface INotificationSender
{
    Task SendAsync(string recipient, string message);
}

public interface INotificationSenderFactory
{
    INotificationSender Create(NotificationChannel channel);
}

public sealed class NotificationSenderFactory(
    EmailSender email,
    SmsSender sms) : INotificationSenderFactory
{
    public INotificationSender Create(NotificationChannel channel) =>
        channel switch
        {
            NotificationChannel.Email => email,
            NotificationChannel.Sms => sms,
            _ => throw new ArgumentOutOfRangeException(nameof(channel))
        };
}

Factory nie musi oznaczać rozbudowanej hierarchii klas. W nowoczesnym C# często wystarczy metoda z wyrażeniem switch albo fabryka oparta na słowniku. Najważniejsze jest przeniesienie decyzji o konstrukcji w jedno miejsce, a nie samo użycie konkretnej nazwy wzorca.

Decorator dla funkcji przekrojowych

Decorator opakowuje istniejący obiekt i zachowuje jego interfejs, jednocześnie dodając nowe zachowanie. Logowanie, cache, pomiar czasu, retry i kontrola uprawnień to typowe przykłady zastosowania.

public sealed class LoggingOrderService(
    IOrderService inner,
    ILogger logger) : IOrderService
{
    public async Task GetAsync(Guid id)
    {
        logger.LogInformation("Loading order {OrderId}", id);
        var order = await inner.GetAsync(id);
        logger.LogInformation("Order {OrderId} loaded", id);
        return order;
    }
}

Największa zaleta dekoratora polega na tym, że główna implementacja pozostaje skupiona na swoim zadaniu. Warto jednak kontrolować liczbę warstw. Pięć dekoratorów może być rozsądne w dużej usłudze, ale w małym module utrudni śledzenie przepływu.

Adapter i Facade dla granic systemu

Adapter tłumaczy jeden interfejs na inny. Używam go wtedy, gdy zewnętrzne SDK, stary moduł albo usługa dostawcy nie pasuje do modelu aplikacji. Dzięki temu reszta systemu nie musi znać obcego API.

Facade ma szerszy cel. Udostępnia prosty interfejs do bardziej złożonego podsystemu, na przykład procesu zakładania konta, który obejmuje walidację, zapis użytkownika, wysłanie wiadomości i publikację zdarzenia. W obu przypadkach granica jest ważniejsza niż sama nazwa wzorca, ponieważ chroni domenę przed szczegółami infrastruktury.

Observer i Command tam, gdzie pojawiają się zdarzenia oraz akcje

Observer pozwala wielu odbiorcom reagować na zmianę stanu. W .NET naturalnym odpowiednikiem bywają zdarzenia, komunikaty domenowe albo mechanizmy publikowania zdarzeń. Trzeba uważać, ponieważ ukryty przepływ sterowania szybko staje się trudny do debugowania.

Command zamienia operację w obiekt. Jest dobrym wyborem, gdy akcję trzeba kolejkować, logować, ponawiać albo obsługiwać asynchronicznie. W aplikacjach opartych na MediatR lub własnych handlerach ten pomysł często pojawia się jako osobny request i jego handler, choć sama biblioteka nie jest wzorcem.

Wzorzec Wybierz, gdy Najczęstsze ryzyko
Strategy Masz kilka wymiennych algorytmów Tworzenie strategii dla jednego wariantu
Factory Decyzja o implementacji zależy od danych Fabryka staje się kolejnym miejscem z dużym if-else
Decorator Chcesz dodawać zachowania bez zmiany klasy Zbyt długa i niejawna łańcuchowa kompozycja
Adapter Integrujesz obcy lub starszy interfejs Przenoszenie modelu dostawcy do całej domeny
Command Operację trzeba kolejkować lub śledzić Nadmierna liczba klas dla prostych akcji

Jak łączyć wzorce z dependency injection

Dependency injection, czyli wstrzykiwanie zależności, pozwala przekazać obiektowi to, czego potrzebuje, zamiast tworzyć zależności wewnątrz klasy. W .NET kontener usług często staje się miejscem, w którym składamy strategie, fabryki i dekoratory. Nie oznacza to jednak, że kontener powinien podejmować wszystkie decyzje biznesowe.

builder.Services.AddScoped();
builder.Services.AddScoped();

builder.Services.AddScoped();
builder.Services.AddScoped();

W przypadku wielu implementacji można wstrzyknąć kolekcję usług i wybrać właściwą na podstawie klucza. Przy prostym scenariuszu wystarczy lista strategii z właściwością CanHandle. Przy większej liczbie wariantów lepiej dołożyć fabrykę, która ukryje sposób wyszukiwania implementacji.

Wbudowany kontener .NET obsługuje trzy podstawowe czasy życia usług. Transient tworzy nową instancję przy każdym pobraniu, scoped zwykle jedną instancję na żądanie HTTP, a singleton współdzieli obiekt przez cały czas działania aplikacji. Ten wybór wpływa na poprawność programu, a nie tylko na wydajność.

Dokumentacja Microsoftu zwraca uwagę na problem tak zwanej captive dependency. Występuje on wtedy, gdy singleton przechowuje zależność scoped, przez co obiekt o krótszym czasie życia zostaje faktycznie zatrzymany na dłużej. Singleton powinien być też bezpieczny współbieżnie i nie powinien przypadkowo przechowywać stanu konkretnego użytkownika.

Decorator można zarejestrować ręcznie, opakowując implementację w fabryce, albo przy użyciu biblioteki wspierającej dekorowanie usług. Niezależnie od techniki warto sprawdzić testem integracyjnym, w jakiej kolejności wykonywane są warstwy. Kolejność logowania, cache i retry potrafi zmienić zachowanie całej operacji.

Wzorce, które dobrze wyglądają na diagramie, ale nie zawsze pomagają

Najczęstszy błąd polega na dopasowywaniu problemu do nazwy wzorca. Jeśli klasa ma prostą odpowiedzialność, a jedyną zmianą jest inny parametr konstruktora, dodatkowa abstrakcja może tylko zwiększyć koszt utrzymania. Prosty kod jest pełnoprawnym rozwiązaniem, nawet jeśli nie da się go opisać nazwą z katalogu GoF.

Ostrożnie podchodzę też do Singletona implementowanego ręcznie. W aplikacji .NET zwykle lepiej zarejestrować usługę jako singleton i pozwolić kontenerowi zarządzać jej cyklem życia. Własne statyczne pola utrudniają testowanie, ukrywają zależności i często prowadzą do globalnego stanu.

Repository również nie powinno być automatycznie dodawane do każdej aplikacji korzystającej z Entity Framework Core. DbContext już pełni rolę jednostki pracy i oferuje abstrakcję nad dostępem do danych. Dodatkowe repozytorium ma sens, gdy izoluje konkretny model, łączy kilka źródeł albo upraszcza testowanie ważnej logiki, ale nie wtedy, gdy tylko powiela metody Add, Update i Get.

Przeczytaj również: Pamięć podręczna in-memory w .NET - od IMemoryCache do Redis

Po czym poznać, że wzorzec jest potrzebny

  • Ta sama decyzja lub reguła pojawia się w kilku miejscach.
  • Zmiana jednego wariantu wymaga edycji dużej klasy.
  • Test wymaga uruchamiania bazy, sieci albo zewnętrznego SDK bez wyraźnej potrzeby.
  • Klasa ma kilka niezależnych powodów do zmiany.
  • Integracja z zewnętrznym systemem przecieka do warstwy domenowej.

Jeśli żaden z tych sygnałów nie występuje, zaczynam od prostszej implementacji. Wzorzec można dodać później, gdy problem stanie się rzeczywisty. Refaktoryzacja z prostego kodu do strategii lub dekoratora bywa znacznie łatwiejsza niż usuwanie niepotrzebnej hierarchii klas.

Jak wybierać wzorzec na podstawie problemu

Najlepiej zacząć od pytania, co dokładnie ma się zmieniać. Jeżeli zmienny jest algorytm, rozważ Strategy. Jeżeli zmienia się sposób tworzenia obiektu, spójrz na Factory. Jeżeli chcesz dołożyć zachowanie wokół istniejącej operacji, naturalnym kandydatem będzie Decorator.

Pytanie projektowe Najbardziej naturalny kierunek
Czy wybieram jeden z kilku algorytmów? Strategy
Czy utworzenie obiektu wymaga decyzji lub konfiguracji? Factory
Czy chcę dodać zachowanie bez zmiany istniejącej klasy? Decorator
Czy obcy interfejs nie pasuje do mojego modelu? Adapter
Czy złożona akcja powinna być kolejką, historią lub komunikatem? Command
Czy wiele komponentów ma reagować na jedno zdarzenie? Observer lub komunikaty domenowe

Warto także ocenić koszt poznawczy. Każdy interfejs, fabryka i dekorator zwiększa liczbę miejsc, które nowa osoba musi przeczytać. Dlatego patrzę nie tylko na elastyczność rozwiązania, lecz także na to, czy za pół roku inny programista szybko znajdzie miejsce odpowiedzialne za daną decyzję.

W systemach rozwijanych przez kilka zespołów wzorce dobrze działają jako granice modułów. Przykładowo adapter może ukryć dostawcę płatności, a strategia może zamknąć reguły naliczania prowizji. Taki podział ogranicza skutki zmian i pozwala testować logikę biznesową bez uruchamiania całej infrastruktury.

Testowanie wzorców bez testowania całego świata

Wzorzec ma sens wtedy, gdy ułatwia weryfikację zachowania. Strategię można przetestować na kilku zestawach danych, fabrykę na mapowaniu typu do implementacji, a dekorator na sprawdzeniu, czy wywołuje usługę wewnętrzną i zapisuje właściwy komunikat. Test powinien skupiać się na regule wynikającej z projektu, a nie na liczbie utworzonych obiektów.

Przykład dla strategii jest prosty. Wstrzykuję konkretną implementację do serwisu i sprawdzam wynik dla zamówienia o wartości granicznej, dla klienta premium oraz dla pustego koszyka. Nie potrzebuję do tego kontenera DI ani prawdziwej bazy danych.

Testy integracyjne są potrzebne na granicach. To tam sprawdzam, czy rejestracja usług nie prowadzi do błędnego czasu życia, czy dekoratory mają właściwą kolejność i czy adapter poprawnie tłumaczy błędy dostawcy. Według mnie właśnie te testy dają największą wartość przy architekturze opartej na kompozycji.

Nie warto jednak testować samej nazwy wzorca. Kod nie zyskuje jakości dlatego, że klasa nazywa się Factory. Zyskuje ją wtedy, gdy zmiana implementacji jest bezpieczna, zależności są widoczne, a testy potrafią szybko wykryć regresję.

Najlepszy wzorzec poznasz po zmianie, którą ułatwia

Wzorce projektowe w C# traktuję jako narzędzia do kontrolowania zmienności. Strategy pomaga wymieniać reguły, Factory porządkuje tworzenie obiektów, Decorator dodaje funkcje przekrojowe, a Adapter chroni kod przed obcymi interfejsami. Dependency injection składa te elementy w całość, ale nie podejmuje za nas decyzji architektonicznych.

Najbezpieczniejszy sposób nauki to wybrać mały, realny problem z własnego projektu i zaimplementować jedno rozwiązanie. Jeśli po zmianie kod jest łatwiejszy do testowania i rozszerzania, wzorzec spełnił swoje zadanie. Jeśli pojawiły się głównie kolejne interfejsy, fabryki i konfiguracja, prawdopodobnie potrzebny był prostszy kod.

FAQ - Najczęstsze pytania

Strategy sprawdza się, gdy masz kilka wymiennych algorytmów lub reguł biznesowych, na przykład różne sposoby naliczania rabatu, wysyłki albo filtrowania danych. Każdą strategię można testować niezależnie, bez rozbudowanego łańcucha if-else.

Factory jest przydatna, gdy wybór implementacji zależy od typu dokumentu, dostawcy, konfiguracji lub danych wejściowych. Centralizuje tę decyzję, często w zwykłej metodzie switch albo fabryce opartej na słowniku. Nie ma potrzeby tworzyć jej dla jednej klasy z prostym konstruktorem.

Decorator opakowuje istniejący obiekt i zachowuje jego interfejs, dodając zachowanie wokół głównej operacji. Można w ten sposób dodać logowanie, cache, retry, pomiar czasu albo kontrolę uprawnień bez modyfikowania podstawowej implementacji. Trzeba jednak kontrolować liczbę warstw, ponieważ zbyt długi łańcuch utrudnia śledzenie przepływu.

Transient tworzy nową instancję przy każdym pobraniu, scoped zwykle jedną instancję na żądanie HTTP, a singleton współdzieli obiekt przez cały czas działania aplikacji. Singleton powinien być bezpieczny współbieżnie i nie może przechowywać zależności scoped, ponieważ prowadzi to do problemu captive dependency. Dobór czasu życia wpływa na poprawność programu, nie tylko na wydajność.

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi

strategia
fabryka
dekorator
adapter
wstrzykiwanie zależności
Autor Bruno Krawczyk
Bruno Krawczyk
Mam na imię Bruno i od 8 lat zgłębiam tajniki programowania w ekosystemie .NET, chmury Azure oraz sztucznej inteligencji. Moja przygoda z technologią zaczęła się od ciekawości, jak złożone systemy mogą ułatwiać codzienne życie i rozwiązywać realne problemy. Dziś moją misją jest dzielenie się tą wiedzą, starając się przybliżyć nawet najbardziej skomplikowane zagadnienia w sposób zrozumiały i przystępny dla każdego. W moich artykułach na kursdotnet.pl skupiam się na praktycznych aspektach, analizuję najnowsze trendy i weryfikuję informacje, aby dostarczyć Wam treści, które są nie tylko dokładne i aktualne, ale przede wszystkim użyteczne w Waszej własnej ścieżce rozwoju technologicznego.

Udostępnij artykuł

Napisz komentarz