Wzorce projektowe w C# i .NET - które naprawdę warto znać?

Przemysław Kwiatkowski 28 czerwca 2026
Diagram ukazuje warstwy aplikacji, agregaty (Buyer, Order) i wzorce projektowe repozytoriów oraz Unit of Work.

Spis treści

Gdy aplikacja zaczyna rosnąć, podobne problemy wracają w różnych miejscach: tworzenie obiektów, obsługa wielu wariantów algorytmu, integracja z zewnętrznym API czy dokładanie logowania bez przepisywania istniejącego kodu. Wzorce projektowe pomagają nadać tym problemom powtarzalną strukturę, ale tylko wtedy, gdy wynikają z realnej potrzeby. Pokażę, które rozwiązania są szczególnie użyteczne w C# i .NET, jak je dobierać oraz kiedy lepiej z nich zrezygnować.

Najważniejsze decyzje przy stosowaniu wzorców w .NET

  • Wzorzec rozwiązuje problem projektowy, a nie zastępuje znajomości języka ani dobrego podziału odpowiedzialności.
  • Factory, Strategy, Decorator i Adapter należą do najpraktyczniejszych rozwiązań w aplikacjach C#.
  • Dependency Injection upraszcza wymianę implementacji, testowanie i zarządzanie cyklem życia usług.
  • Singleton nie jest domyślną receptą na współdzielenie obiektu i często utrudnia testy.
  • Najlepszy moment na użycie wzorca pojawia się wtedy, gdy konkretny problem zaczyna się powtarzać.

Co naprawdę daje wzorzec, a czego nie załatwi

Wzorzec projektowy to sprawdzony sposób organizowania klas, obiektów i ich współpracy w sytuacji, która regularnie pojawia się podczas tworzenia oprogramowania. Nie jest gotowym fragmentem kodu do bezrefleksyjnego skopiowania. To raczej nazwany pomysł na rozwiązanie problemu, dzięki któremu zespół szybciej rozumie intencję autora.

Jeżeli mówię, że w danym miejscu zastosowano strategię, od razu wiadomo, że istnieje kilka wymiennych sposobów wykonania tej samej operacji. Gdy pojawia się dekorator, można oczekiwać, że dodatkowa funkcjonalność została dołączona bez modyfikowania podstawowej implementacji. Taki wspólny język ma dużą wartość podczas przeglądów kodu, szczególnie w większych zespołach.

Nie należy jednak mylić wzorca z biblioteką, frameworkiem ani stylem architektury. Clean Architecture, architektura heksagonalna i CQRS opisują organizację większych części systemu, natomiast Factory czy Strategy najczęściej rozwiązują problem na poziomie kilku klas. Jedno rozwiązanie może wspierać drugie, ale nie są tym samym.

Kategoria Główne pytanie Przykłady
Kreacyjne Jak bezpiecznie tworzyć obiekty? Factory, Builder, Singleton
Strukturalne Jak łączyć klasy i rozszerzać ich możliwości? Adapter, Decorator, Facade
Behawioralne Jak organizować zachowanie i komunikację? Strategy, Observer, Command, State

W praktyce nie uczę się całego katalogu na pamięć. Znacznie bardziej opłaca się rozpoznawać symptomy problemu. Jeśli klasa ma kilka rozgałęzień zależnych od typu płatności, prawdopodobnie potrzebuje strategii albo fabryki. Jeśli każda usługa tworzy własnego klienta HTTP, problem dotyczy zależności i cyklu życia obiektów, a nie braku kolejnego interfejsu.

Diagram ukazuje różne wzorce projektowe w świecie mikroserwisów, z przykładami baz danych i technologii.

Najbardziej użyteczne rozwiązania w codziennym C#

Factory, gdy sposób tworzenia obiektu zaczyna żyć własnym życiem

Fabryka oddziela kod korzystający z obiektu od szczegółów jego tworzenia. Dobrze sprawdza się wtedy, gdy konkretny typ zależy od konfiguracji, danych wejściowych albo wybranego kanału. Przykładem może być wybór klienta do wysyłki powiadomień przez e-mail, SMS lub usługę zewnętrzną.

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

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

Największą korzyścią jest zamknięcie decyzji w jednym miejscu. Kod biznesowy nie musi znać klas `EmailNotificationSender` ani `SmsNotificationSender`. Nie warto jednak tworzyć rozbudowanej fabryki dla dwóch prostych konstruktorów, jeśli zwykły konstruktor lub rejestracja w kontenerze DI rozwiązuje problem czytelniej.

Strategy, gdy algorytm ma kilka równorzędnych wersji

Strategia izoluje warianty zachowania za wspólnym interfejsem. Wyobraźmy sobie wyliczanie rabatu, ocenę ryzyka transakcji albo wybór sposobu kompresji pliku. Zamiast jednej metody z długim łańcuchem `if` każda reguła trafia do osobnej klasy.

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

public sealed class VipDiscount : IDiscountStrategy
{
    public decimal Calculate(Order order) => order.Total * 0.15m;
}

To rozwiązanie ułatwia testy, bo każdą regułę można sprawdzić niezależnie. W aplikacjach ASP.NET Core strategie często są rejestrowane przez Dependency Injection, czyli mechanizm dostarczania zależności z zewnątrz zamiast tworzenia ich wewnątrz klasy. Według dokumentacji Microsoftu kontener DI jest w .NET dostępny jako część podstawowego stosu, razem między innymi z konfiguracją i logowaniem.

Decorator, gdy trzeba dodać funkcję bez naruszania rdzenia

Dekorator opakowuje istniejącą usługę i dodaje do niej zachowanie, na przykład logowanie, cache, pomiar czasu albo ponawianie wywołania. Dzięki temu podstawowa klasa nie musi wiedzieć, czy jej wynik będzie zapisywany w pamięci podręcznej.

public sealed class CachedProductService : IProductService
{
    private readonly IProductService inner;

    public CachedProductService(IProductService inner)
    {
        this.inner = inner;
    }

    public async Task GetAsync(int id)
    {
        // odczyt z cache lub wywołanie inner.GetAsync(id)
        return await inner.GetAsync(id);
    }
}

Największą pułapką jest niekontrolowane zagnieżdżanie dekoratorów. Gdy jedno wywołanie przechodzi przez cache, logowanie, retry, autoryzację i metryki, trzeba jasno ustalić kolejność. Każda warstwa powinna mieć jedną czytelną odpowiedzialność, inaczej dekorator staje się tylko trudnym do śledzenia łańcuchem pośredników.

Adapter i Facade, gdy systemy nie mówią tym samym językiem

Adapter tłumaczy jeden interfejs na inny. Jest bardzo przydatny przy integracji z dostawcą płatności, systemem ERP, usługą Azure albo biblioteką, której nie chcemy ujawniać w całej aplikacji. Własny interfejs chroni resztę kodu przed zmianami w zewnętrznym SDK.

Fasada działa szerzej. Udostępnia prosty punkt wejścia do kilku złożonych komponentów, na przykład do procesu rejestracji użytkownika obejmującego zapis w bazie, wysłanie wiadomości i utworzenie profilu. Adapter tłumaczy interfejs, a fasada upraszcza współpracę z całym podsystemem.

Builder, gdy konstruktor przestaje być czytelny

Builder pomaga tworzyć obiekty z wieloma opcjonalnymi ustawieniami. W C# część jego zastosowań przejęły inicjalizatory obiektów, rekordy i nazwane argumenty, dlatego nie warto używać go automatycznie. Nadal ma sens przy złożonych regułach walidacji, wieloetapowym tworzeniu konfiguracji albo budowaniu zapytań.

Jeśli obiekt ma dwa wymagane parametry i jedno ustawienie opcjonalne, builder będzie prawdopodobnie przerostem formy nad treścią. Gdy jednak konstruktor przyjmuje 8-10 parametrów, a ich kolejność łatwo pomylić, osobny sposób budowania może wyraźnie poprawić bezpieczeństwo kodu.

Jak dobrać rozwiązanie do konkretnego problemu

Najpierw opisuję problem bez używania nazw wzorców. To proste ćwiczenie często pokazuje, że potrzebna jest zwykła refaktoryzacja, a nie rozbudowana architektura. Dopiero później sprawdzam, który schemat najlepiej pasuje do obserwowanego napięcia w kodzie.

  1. Znajdź powtarzalność i określ, co dokładnie się zmienia.
  2. Oddziel część stałą od wariantów, reguł lub zależności zewnętrznych.
  3. Wybierz najmniejszą abstrakcję, która rozwiązuje problem.
  4. Napisz test pokazujący oczekiwane zachowanie.
  5. Sprawdź, czy nowa struktura ułatwia kolejną zmianę, a nie tylko zwiększa liczbę klas.
Objaw w kodzie Rozwiązanie do rozważenia Na co uważać
Długi wybór typu przez `if` lub `switch` Strategy albo Factory Nie rozdzielaj logiki, która zmienia się tylko raz
Wiele dodatkowych operacji wokół jednej usługi Decorator Ustal kolejność i odpowiedzialność warstw
Obce API przecieka do domeny Adapter Nie kopiuj całego zewnętrznego modelu do własnego interfejsu
Trudny do czytania konstruktor Builder lub obiekt opcji Nie twórz buildera dla prostego obiektu
Klasy tworzą zależności przez `new` Dependency Injection Nie rejestruj wszystkiego jako singleton

Ważna jest też kolejność refaktoryzacji. Najpierw zapewniam testy dla obecnego zachowania, później wydzielam interfejs lub klasę, a dopiero na końcu zmieniam sposób rejestracji zależności. Wzorzec powinien zmniejszać koszt kolejnej zmiany. Jeśli po jego wprowadzeniu trzeba edytować więcej plików niż wcześniej, prawdopodobnie abstrakcja jest przedwczesna.

Jak wzorce wspierają architekturę aplikacji .NET

W większym systemie pojedyncze rozwiązania zaczynają tworzyć spójną architekturę. Adaptery oddzielają domenę od zewnętrznych usług, fabryki kontrolują tworzenie obiektów, strategie izolują reguły biznesowe, a dekoratory dodają przekrojowe funkcje. Taki podział ogranicza liczbę miejsc, które trzeba zmienić po wymianie dostawcy lub sposobu działania.

Dobrym przykładem jest aplikacja wykorzystująca kilku dostawców modeli AI. Interfejs może opisywać operację generowania odpowiedzi, adapter tłumaczyć API konkretnego dostawcy, strategia wybierać model zależnie od kosztu lub jakości, a dekorator rejestrować czas i liczbę tokenów. Każdy element rozwiązuje inny problem, więc nie trzeba budować jednej klasy odpowiedzialnej za wszystko.

Podobnie działa warstwa dostępu do płatności. Kod domenowy nie powinien znać szczegółów żądań HTTP, formatów odpowiedzi ani wyjątków konkretnego operatora. Adapter przejmuje tę wiedzę, a aplikacja pracuje na własnych pojęciach, takich jak `PaymentResult` czy `PaymentStatus`.

Dependency Injection jest tutaj szczególnie praktyczne, bo obiekt otrzymuje zależności w konstruktorze i nie tworzy ich samodzielnie. Dzięki temu w teście można podać atrapę, a w produkcji prawdziwą implementację. W nowoczesnym .NET kontener zarządza także cyklem życia usług, między innymi rozróżniając rejestracje typu transient, scoped i singleton.

Nie oznacza to, że każda aplikacja potrzebuje pełnej architektury heksagonalnej. Dla małego narzędzia konsolowego dodatkowe porty i adaptery mogą tylko zasłonić prosty przepływ. Skala domeny, liczba integracji i tempo zmian powinny decydować o poziomie abstrakcji.

Najczęstsze błędy i koszty złych abstrakcji

Używanie wzorca dlatego, że jest popularny

Najczęstszy błąd polega na dopasowywaniu problemu do poznanego wzorca. Programista uczy się Singletona, więc zaczyna traktować go jako uniwersalny sposób na współdzielenie obiektów. Tymczasem często wystarczy odpowiednia rejestracja usługi w kontenerze DI albo zwykłe przekazanie zależności.

Singleton jako ukryty stan globalny

Singleton może mieć sens dla bezstanowej, bezpiecznej współbieżnie usługi, której jedna instancja jest faktycznie wystarczająca. Nie powinien jednak przechowywać przypadkowego stanu użytkownika, danych żądania ani zmiennych kolekcji modyfikowanych bez synchronizacji.

W praktyce singletony utrudniają testy, bo jeden test może zmienić stan widoczny dla następnego. Z tego powodu szczególnie ostrożnie podchodzę do singletonów zawierających cache, konfigurację zmienianą w czasie działania albo obiekty zależne od kontekstu żądania.

Interfejs dla każdej klasy

Sam fakt dodania interfejsu nie czyni projektu elastycznym. Jeśli istnieje tylko jedna implementacja, nie ma realnego punktu wymiany, a interfejs może być wyłącznie dodatkowym plikiem do utrzymania. Abstrakcja ma wartość wtedy, gdy chroni granicę, ułatwia testowanie lub przewiduje uzasadnioną zmianę.

Repository jako warstwa bez celu

Repository bywa użyteczne, gdy ukrywa złożone zapytania, łączy kilka źródeł danych albo przedstawia domenowy sposób wyszukiwania. Nie zawsze ma sens jako mechaniczne opakowanie każdej metody ORM. Jeśli klasa tylko przekazuje wywołania do `DbSet`, może dodać więcej ceremonii niż wartości.

Przeczytaj również: Adapter pattern w C# - jak łączyć niezgodne interfejsy?

Nadmiar klas i pośredników

Po zastosowaniu kilku wzorców kod może wyglądać profesjonalnie, ale nadal być trudny do zrozumienia. Gdy prosta operacja przechodzi przez fabrykę, mediator, fasadę, trzy dekoratory i repozytorium, koszt poznawczy rośnie. Czytelność przepływu jest ważniejsza od liczby zastosowanych technik.

Jak rozwijać tę wiedzę bez tworzenia sztucznych projektów

Najlepiej zacząć od istniejącej aplikacji i znaleźć jeden fragment, który regularnie sprawia problemy. Może to być wybór dostawcy, wielki `switch`, powtarzające się logowanie albo klasa, której konstruktor przyjmuje zbyt wiele parametrów.

Wydziel małą zmianę, dodaj test i porównaj kod przed oraz po refaktoryzacji. Jeżeli nowa struktura ułatwiła dodanie kolejnego wariantu i nie ukryła logiki za nadmiarem abstrakcji, wzorzec spełnił swoje zadanie.

Nie chodzi o znajomość wszystkich nazw z katalogu. Największą korzyść daje umiejętność rozpoznania, co się zmienia, kto powinien za to odpowiadać i jak odizolować resztę systemu. Nazwa wzorca przychodzi dopiero później i służy głównie temu, by sprawniej rozmawiać o rozwiązaniu.

FAQ - Najczęstsze pytania

Factory ma sens, gdy typ obiektu zależy od konfiguracji, danych wejściowych lub wybranego kanału, na przykład e-maila, SMS-a albo usługi zewnętrznej. Jeśli tworzenie ogranicza się do dwóch prostych konstruktorów, zwykły konstruktor lub rejestracja w kontenerze DI będzie czytelniejsza.

Strategy izoluje kilka wymiennych wariantów tego samego algorytmu, na przykład reguły naliczania rabatu. Decorator dodaje funkcje takie jak logowanie, cache lub retry bez zmiany podstawowej klasy, natomiast Adapter tłumaczy zewnętrzny interfejs na własny interfejs aplikacji.

Builder pomaga przy złożonych regułach walidacji, wieloetapowym tworzeniu konfiguracji oraz obiektach z wieloma opcjonalnymi ustawieniami. Przy dwóch wymaganych parametrach i jednym opcjonalnym ustawieniu lepiej użyć inicjalizatora, rekordu lub nazwanych argumentów; Builder może być uzasadniony, gdy konstruktor ma 8-10 parametrów i łatwo pomylić ich kolejność.

Singleton może mieć sens dla bezstanowej i bezpiecznej współbieżnie usługi, której jedna instancja rzeczywiście wystarcza. Gdy przechowuje stan użytkownika, dane żądania, zmienny cache lub konfigurację zmienianą w czasie działania, staje się ukrytym stanem globalnym i może powodować zależności między testami.

Dependency Injection dostarcza zależności z zewnątrz, więc klasa nie tworzy ich samodzielnie przez new. Ułatwia to podawanie atrap w testach, wymianę implementacji oraz zarządzanie cyklem życia usług za pomocą rejestracji transient, scoped i singleton.

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi

fabryka
strategia
dekorator
adapter
wstrzykiwanie zależności
Autor Przemysław Kwiatkowski
Przemysław Kwiatkowski
Jestem Przemysław i od 15 lat zajmuję się programowaniem .NET, chmurą Azure oraz sztuczną inteligencją. Moja przygoda z tymi technologiami zaczęła się od fascynacji możliwościami, jakie dają, a z czasem przerodziła się w pasję do tworzenia rozwiązań, które realnie wpływają na pracę i życie ludzi. Na kursdotnet.pl staram się dzielić się swoją wiedzą w sposób przystępny, tłumacząc złożone zagadnienia i pomagając zrozumieć, jak te dynamicznie rozwijające się obszary IT mogą być wykorzystane w praktyce. Dokładam wszelkich starań, aby prezentowane przeze mnie materiały były rzetelne, aktualne i oparte na sprawdzonych źródłach, a także aby uporządkować wiedzę w sposób ułatwiający jej przyswojenie.

Udostępnij artykuł

Napisz komentarz