Blog Dla Programistów C#/.NET

czwartek, 23 kwietnia 2026

Wzorce projektowe to sprawdzone rozwiązania typowych problemów programistycznych, które pomagają tworzyć bardziej przejrzysty i łatwiejszy w utrzymaniu kod. Dla architekta oprogramowania lub lidera technicznego (Tech Lead) znajomość kluczowych wzorców jest nieoceniona. Umożliwia świadomy dobór najlepszych rozwiązań architektonicznych oraz przekazywanie zespołowi dobrych praktyk programistycznych. W tym artykule przyjrzymy się 7 najważniejszym wzorcom projektowym w .NET wraz z przykładami implementacji w C# i omówieniem, kiedy warto je stosować.

7 Kluczowych Wzorców Projektowych .NET, Które Każdy Architekt Powinien Znać

1. Factory Method (Metoda wytwórcza)


Problem: Chcemy oddzielić logikę tworzenia obiektów od ich użycia, aby kod był bardziej elastyczny na zmiany.

Rozwiązanie: Wzorzec Factory Method polega na dostarczeniu metody (fabryki), która decyduje o tym, jakiego konkretnie obiektu utworzyć, ukrywając szczegóły tej decyzji przed resztą systemu. Dzięki temu można łatwo dodawać nowe typy obiektów bez modyfikowania kodu klienta. W .NET fabryki są często wykorzystywane przy tworzeniu usług lub obiektów zależnych. Zamiast używać konstruktora bezpośrednio, wywołujemy metodę wytwórczą.

Przykład: Rozważmy system obsługi płatności, który może korzystać z różnych metod płatności (karta, gotówka, przelew). Zamiast rozbudowanego switch w kodzie logiki biznesowej, możemy zastosować fabrykę PaymentFactory, która utworzy odpowiedni obiekt implementujący interfejs IPayment na podstawie zadanego typu:

interface IPayment { void Pay(decimal amount); }

class CardPayment : IPayment { public void Pay(decimal amount) { /* logika płatności kartą */ } }
class CashPayment : IPayment { public void Pay(decimal amount) { /* logika płatności gotówką */ } }

class PaymentFactory
{
public static IPayment Create(string method)
{
return method switch
{
"CARD" => new CardPayment(),
"CASH" => new CashPayment(),
_ => throw new ArgumentException("Nieznana metoda płatności")
};
}
}

/* Użycie: */
IPayment payment = PaymentFactory.Create("CARD");
payment.Pay(100m);

W powyższym kodzie fabryka decyduje, jakiego rodzaju obiekt IPayment zwrócić. Dzięki temu kod wywołujący PaymentFactory.Create nie musi znać szczegółów klas CardPayment czy CashPayment. Wystarczy, że operuje na interfejsie IPayment. Wzorzec Factory Method ułatwia dodawanie nowych sposobów płatności. Wystarczy dopisać nową klasę i zmodyfikować fabrykę, bez ruszania kodu wykorzystującego tę fabrykę.

Typowe zastosowania: Fabryki są przydatne, gdy proces tworzenia obiektu jest złożony, zależny od warunków lub kiedy chcemy zwrócić pewien wspólny interfejs/klasę bazową, a nie konkretną implementację. W .NET spotkamy je np. przy tworzeniu obiektów dostępu do danych (różne repozytoria), podczas konfiguracji Dependency Injection (kontener IoC wewnętrznie działa jak fabryka), czy przy generowaniu obiektów w zależności od konfiguracji aplikacji.


2. Singleton (Singleton)


Problem: W niektórych przypadkach w systemie potrzebny jest dokładnie jeden obiekt danej klasy - globalny punkt dostępu do zasobu - np. pojedyncza instancja serwisu logowania zdarzeń, konfiguracji aplikacji albo połączenia do bazy danych.

Rozwiązanie: Singleton to kreacyjny wzorzec projektowy zapewniający istnienie dokładnie jednej instancji klasy oraz globalny dostęp do niej. Klasa singleton ukrywa swój konstruktor i udostępnia statyczne pole lub właściwość zwracającą instancję. Dzięki temu gdziekolwiek w kodzie odwołamy się do tej klasy, otrzymamy ten sam obiekt.

Implementacja: W C# najprostszym sposobem jest użycie statycznej właściwości przechowującej instancję oraz prywatnego konstruktora:

class LogManager 
{
private static LogManager _instance;
private LogManager() { /* prywatny konstruktor */ }
public static LogManager Instance
=> _instance ??= new LogManager();

/* Przykładowa funkcjonalność singletonu: */
public void Log(string msg) { /* zapis logu */ }
}

Teraz wywołując LogManager.Instance.Log("Komunikat"); zawsze operujemy na tej samej, jedynej instancji LogManager.

Typowe zastosowania: Singletony bywają stosowane do zarządzania współdzielonymi zasobami, jak wspomniany logger, cache aplikacji, konfiguracja, czy menedżer połączeń. W nowoczesnych aplikacjach .NET ostrożnie podchodzi się do wzorca Singleton, nie należy nadużywać globalnego stanu, bo utrudnia to testowanie i rozszerzanie aplikacji. Często zamiast własnoręcznie tworzyć singleton, lepiej skorzystać z mechanizmu kontenera DI (Dependency Injection) i zarejestrować daną zależność jako singleton w kontenerze. Wówczas framework zagwarantuje, że wstrzykiwana będzie jedna współdzielona instancja.


3. Strategy (Strategia)


Problem: Potrzebujemy rodziny algorytmów realizujących zbliżone zadanie i chcemy móc łatwo podmieniać jeden algorytm na inny w trakcie działania programu. Na przykład różne strategie sortowania, różne sposoby kompresji plików, czy odmienne polityki naliczania rabatów.

Rozwiązanie: Strategia to wzorzec behawioralny, który definiuje zbiór algorytmów zamiennych w czasie działania aplikacji. Każdy algorytm jest zaimplementowany jako oddzielna klasa zgodna z wspólnym interfejsem. Klasa kontekstowa (wykorzystująca algorytm) otrzymuje strategię z zewnątrz i używa jej, nie znając szczegółów implementacji.

Implementacja: Przykładowo, zbudujmy system tworzenia kopii zapasowych z możliwością wyboru algorytmu kompresji danych. Zdefiniujemy interfejs ICompression oraz różne strategie kompresji:

interface ICompression { void Compress(string fileName); }

class ZipCompression : ICompression
{
public void Compress(string fileName)
{
Console.WriteLine($"Kompresowanie {fileName} do .zip");
}
}

class RarCompression : ICompression
{
public void Compress(string fileName)
{
Console.WriteLine($"Kompresowanie {fileName} do .rar");
}
}

class BackupService
{
private ICompression _compression;

public BackupService(ICompression compressionStrategy)
{
_compression = compressionStrategy;
}

public void CreateBackup(string file) {
_compression.Compress(file);
}
}

Teraz kliencki kod może zdecydować, której strategii użyć:

/* Wybór strategii kompresji w czasie działania: */
var backup = new BackupService(new ZipCompression());
backup.CreateBackup("dane.txt"); /*użyje kompresji ZIP */

backup = new BackupService(new RarCompression());
backup.CreateBackup("dane.txt"); /* użyje kompresji RAR */

Wzorzec Strategia sprawia, że logika kompresowania jest zamknięta w osobnych klasach, a BackupService nie musi znać szczegółów kompresji. Wystarczy, że woła metodę z interfejsu. Dodanie nowego sposobu kompresji (np. TarCompression) nie wymaga modyfikacji BackupService, a jedynie dostarczenia nowej klasy strategii.

Typowe zastosowania: Strategia znajduje zastosowanie, gdy nasz kod ma wiele wariantów zachowania zależnych od okoliczności. Zamiast wielu instrukcji if/else wybierających algorytm, lepiej przekazać obiekt strategii. Przykłady: różne metody walidacji danych, strategie ustalania opłat w zależności od rodzaju klienta, moduły logowania (np. różne formaty logów). W architekturze .NET strategie często współpracują z DI, np. wstrzykujemy konkretną implementację interfejsu zależnie od konfiguracji.


4. Adapter (Adapter)


Problem: Czasami zachodzi potrzeba połączenia naszego kodu z zewnętrzną biblioteką lub modułem o niepasującym interfejsie. Kod kliencki oczekuje określonego interfejsu/metod, ale dostarczona klasa ma inny zestaw metod.

Rozwiązanie: Adapter to wzorzec strukturalny, który tłumaczy interfejs jednej klasy na interfejs oczekiwany przez klienta. Działa jak tłumacz: zewnętrzna klasa (tzw. Adaptee) zostaje opakowana w klasę adaptera implementującą nasz oczekiwany interfejs (Target). W efekcie reszta systemu może korzystać z adaptera tak, jakby to była natywna klasa zgodna z naszym projektem, a adapter wewnątrz deleguje wywołania do oryginalnej biblioteki.

Przykład: Wyobraźmy sobie, że nasza aplikacja używa interfejsu INotification do wysyłania powiadomień, ale musimy skorzystać z zewnętrznej biblioteki, która oferuje klasę ExternalEmailSender o innej sygnaturze metod. 

Zamiast przerabiać całą aplikację, tworzymy adapter:
• INotification - nasz interfejs z metodą Send(string message).
• ExternalEmailSender - klasa zewnętrzna z metodą SendEmail(string content).
• EmailNotificationAdapter - adapter implementujący INotification, mający wewnątrz instancję ExternalEmailSender. Metoda Send adaptera wywoła wewnętrznie ExternalEmailSender.SendEmail, dopasowując parametry jeśli to konieczne.

Dzięki temu kod naszej aplikacji może nadal używać interfejsu INotification, nie zauważając różnicy, a adapter zadba o przekazanie żądania do biblioteki zewnętrznej.

Typowe zastosowania: Wzorzec Adapter jest powszechnie używany przy integracji różnych systemów lub bibliotek. W .NET przykładem może być adapter umożliwiający korzystanie z innej bazy danych bez zmiany logiki (np. implementacja interfejsu repozytorium, która wewnątrz korzysta z innego ORM lub API). Innym przykładem są adaptery do testów jednostkowych, gdy klasa produkcyjna oczekuje np. interfejsu plikowego IFileSystem, a w testach podstawiamy adapter działający na systemie plików w pamięci. Adaptery pomagają zaoszczędzić czas, dostosowując niekompatybilne interfejsy zamiast zmieniać istniejący kod biznesowy.


5. Decorator (Dekorator)


Problem: Mamy obiekt realizujący pewne podstawowe funkcje i chcemy dynamicznie rozszerzyć jego zachowanie o dodatkowe funkcjonalności, nie zmieniając jego kodu. Ważne, by móc nakładać wiele rozszerzeń jednocześnie i elastycznie je konfigurować.

Rozwiązanie: Dekorator to wzorzec strukturalny, który pozwala przydzielać obiektom nowe obowiązki poprzez "opakowanie" ich w dodatkowe obiekty. Dekorator posiada ten sam interfejs co obiekt dekorowany i deleguje do niego wywołania, dodając przy tym własne działania przed lub po przekazaniu wywołania do wnętrza.

Przykład: Załóżmy, że mamy interfejs INotifier z metodą Send(string message), a jego podstawowa implementacja EmailNotifier wysyła e-mail. Teraz chcemy opcjonalnie rozszerzyć powiadomienia o wysyłkę SMS oraz logowanie wysłanych komunikatów, ale nie poprzez dziedziczenie wielu wariantów, tylko elastyczne dodawanie funkcji. 

Możemy stworzyć dekoratory:
• SmsNotifierDecorator - implementuje INotifier, wewnątrz posiada referencję do INotifier (to będzie nasz obiekt dekorowany, np. instancja EmailNotifier). W swojej implementacji Send najpierw deleguje wysłanie e-maila do obiektu wewnętrznego, a następnie dodaje wysyłkę SMS.
• LoggingNotifierDecorator - również implementuje INotifier i dekoruje inny INotifier. Wysyłając, np. najpierw zapisuje zdarzenie do logów, potem deleguje do wewnętrznego Send.

W ten sposób możemy złożyć obiekty: najpierw bazowy EmailNotifier, na to nakładamy SmsNotifierDecorator, a na to LoggingNotifierDecorator. Ostateczny obiekt będzie spełniał interfejs INotifier i przy wywołaniu Send wykona kolejno: zapis do logu, wysyłkę e-mail, wysyłkę SMS, wszystko konfigurowalne poprzez składanie dekoratorów. Wzorzec dekoratora jest często spotykany w .NET, np. strumienie (Streams) w bibliotece .NET są zrealizowane w ten sposób (możemy owinąć strumień pliku dekoratorem dodającym buforowanie, a następnie dekoratorem kompresującym, itp.).

Typowe zastosowania: Dekorator przydaje się, gdy chcemy uniknąć mnożenia podklas przy dodawaniu funkcjonalności. Pozwala w locie dodawać takie aspekty jak logowanie, cache'owanie, autoryzacja, bez potrzeby modyfikacji kodu bazowego. W architekturze webowej możemy dekorować usługi lub handlery żądań, żeby dołożyć np. mechanizmy automatycznego retry po błędzie czy monitorowanie czasu wykonania.


6. Observer (Obserwator)


Problem: Potrzebujemy mechanizmu powiadamiania wielu obiektów o zdarzeniu zachodzącym w innym obiekcie. Przykładowo: zmiana stanu obiektu powinna automatycznie powiadomić zainteresowane komponenty (np. zmiana danych w modelu informuje widok w GUI, albo zdarzenie systemowe powiadamia zarejestrowane moduły).

Rozwiązanie: Obserwator to wzorzec behawioralny polegający na tym, że jeden obiekt (wydawca, subject) informuje inne obiekty (obserwatorów) o zmianie stanu, zwykle poprzez wywołanie ich metod powiadamiających. Obiekty te wcześniej zarejestrowały się, by otrzymywać takie powiadomienia. Dzięki temu uzyskujemy luźne powiązanie, wydawca nie musi znać szczegółów klas obserwatorów, wywołuje tylko ustaloną metodę interfejsu.

Przykład w .NET: Platforma .NET ma wbudowaną obsługę wzorca Obserwator w postaci delegatów i zdarzeń. Gdy np. kontrolka GUI ma zdarzenie Button.Click, obiekty zainteresowane (obserwatorzy) mogą się podpiąć pod to zdarzenie. Gdy użytkownik kliknie przycisk, kontrolka (subject) wywołuje wszystkie zarejestrowane obsługi zdarzenia (observery). Innym przykładem jest interfejs INotifyPropertyChanged w modelu MVVM. Obiekt implementujący ten interfejs powiadamia obserwatorów (np. mechanizm wiązania danych w UI), że dana właściwość uległa zmianie, co pozwala automatycznie zaktualizować interfejs użytkownika.

Własną implementację obserwatora można stworzyć definiując interfejs (np. IObserver z metodą Update()), jednak częściej w C# po prostu korzystamy z wbudowanych zdarzeń, które działają na podobnej zasadzie.

Typowe zastosowania: Wzorzec Obserwator jest fundamentem systemów zdarzeniowych. Przydaje się zawsze, gdy istnieje relacja "jeden-do-wielu" między obiektami i chcemy zapewnić automatyczną reakcję na zdarzenia. Należy jednak uważać na potencjalne problemy z zarządzaniem subskrypcjami (np. pamiętać o odpinaniu się od zdarzeń, aby nie powodować wycieków pamięci).


7. Dependency Injection (Wstrzykiwanie zależności)


Problem: Klasy w dużych systemach często posiadają zależności. Korzystają z innych klas do wykonania swoich zadań. Jeśli tworzymy te zależności wewnątrz klasy (np. przez new w kodzie), to mocno wiążemy implementację z konkretnymi klasami, co utrudnia modyfikacje i testowanie (np. podmianę bazy danych na inną implementację, czy zastąpienie prawdziwej usługi atrapą w testach).

Rozwiązanie: Wstrzykiwanie zależności (Dependency Injection) to wzorzec projektowy (a zarazem wyraz szerszej zasady odwrócenia zależności z SOLID), który polega na przekazywaniu zależności do obiektu z zewnątrz zamiast tworzenia ich wewnątrz. Najczęściej stosuje się wstrzykiwanie poprzez konstruktor, wymagane zależności są wtedy parametrami konstruktora. Może być też wstrzykiwanie poprzez właściwość lub metodę, ale konstruktor jest najczęstszy.

Przykład: Rozważmy klasę OrderService, która potrzebuje dostępu do magazynu danych zamówień. Zamiast tworzyć w niej konkretną implementację np. SqlOrderRepository, zdefiniujmy interfejs IOrderRepository i w konstruktorze OrderService zażądajmy obiektu tej zależności:

interface IOrderRepository { Order GetById(int id); /* ... */ }

class OrderService
{
private IOrderRepository _repo;

public OrderService(IOrderRepository repo)
{
_repo = repo;
}

public Order GetOrder(int id)
{
return _repo.GetById(id);
}
}

Dzięki temu OrderService nie wie nic o szczegółach przechowywania danych, może to być SQL, plik, a nawet atrapa do testów, byle implementowało IOrderRepository. W praktyce w .NET korzystamy z kontenera IoC (np. wbudowanego w ASP.NET Core), który na podstawie konfiguracji dostarczy właściwą implementację. W startupie aplikacji rejestrujemy np. services.AddScoped<IOrderRepository, SqlOrderRepository>(). Gdy klasa OrderService jest potrzebna, framework automatycznie wstrzyknie do niej zarejestrowany IOrderRepository.

Typowe zastosowania: Wstrzykiwanie zależności jest używane praktycznie wszędzie we współczesnych aplikacjach .NET. Umożliwia pisanie kodu zgodnego z zasadą odwrócenia zależności, co zwiększa modularność systemu. Dla architekta wzorzec DI to podstawa projektowania aplikacji warstwowych, pozwala na łatwą podmianę komponentów (np. logiki dostępu do danych, usług zewnętrznych) i znacząco ułatwia pisanie testów jednostkowych (można wstrzykiwać mocki).


Podsumowanie


Znajomość wzorców projektowych i umiejętność ich odpowiedniego zastosowania to ważna część warsztatu architekta oprogramowania i lidera technicznego. Omówione powyżej wzorce - od kreacyjnych (jak Fabryka, Singleton), przez strukturalne (Adapter, Dekorator) po behawioralne (Strategia, Obserwator) oraz architektoniczne podejście jak wstrzykiwanie zależności, stanowią niezbędnik w świecie .NET. Stosując je świadomie, możemy tworzyć rozwiązania bardziej elastyczne, łatwiejsze w rozwoju i zgodne z najlepszymi praktykami. Jednocześnie warto pamiętać, że wzorce nie są celem samym w sobie, kluczem jest wybór właściwego wzorca do danego problemu i nieprzesadzanie z "przeinżynierowaniem" kodu.

Jeśli chcesz dalej rozwijać swoje umiejętności architektoniczne i programistyczne w .NET, rozważ dołączenie do jednego z moich kursów online. W ramach szkoleń omawiamy m.in. praktyczne zastosowania tych wzorców w rzeczywistych projektach i wiele innych zaawansowanych zagadnień. Pełną listę moich kursów znajdziesz tutaj - z pewnością znajdziesz coś dla siebie.

Autor artykułu:
Kazimierz Szpin
Kazimierz Szpin
CTO & Founder - FindSolution.pl
Programista C#/.NET. Specjalizuje się w Blazor, ASP.NET Core, ASP.NET MVC, ASP.NET Web API, WPF oraz Windows Forms.
Autor bloga ModestProgrammer.pl
Dodaj komentarz

Wyszukiwarka

© Copyright 2026 modestprogrammer.pl | Sztuczna Inteligencja | Regulamin | Polityka prywatności. Design by Kazimierz Szpin. Wszelkie prawa zastrzeżone.