Zaawansowane techniki obiektowe

Rok akademicki: 2026/27
Instrukcja laboratoryjna – Część 3: Wzorce Czynnościowe (Behawioralne), Architektura Zdarzeniowa, Paradygmat Funkcyjny i Komunikacja Rozproszona
Prowadzący: Tomasz Goluch
Wersja: 1.3


1. Wprowadzenie do Wzorców Czynnościowych (Behawioralnych)

1.1. Cel i Istota Wzorców Czynnościowych

Celem trzeciej części laboratorium jest poznanie, zrozumienie i praktyczna implementacja wzorców czynnościowych (behawioralnych, operacyjnych – ang. Behavioral Patterns) w języku C# (standard C# 12 / .NET 8+). Wzorce czynnościowe opisują sposób, w jaki klasy lub obiekty oddziałują między sobą, komunikują się i dzielą odpowiedzialności, a także precyzują algorytmy oraz przepływ sterowania w trakcie wykonywania programu (runtime).

Podczas gdy wzorce kreacyjne (Część 1) koncentrowały się na procesie powoływania instancji, a wzorce strukturalne (Część 2) na kompozycji klas i obiektów w większe struktury, wzorce czynnościowe odpowiadają na pytanie: „Jak rozdzielić odpowiedzialność algorytmiczną i komunikacyjną pomiędzy obiektami, aby zachować luźne powiązania, wysoką testowalność i brak wycieków pamięci?”.


1.2. Logiczna Kolejność Wzorców Czynnościowych

Wzorce behawioralne stanowią najliczniejszą grupę w klasyfikacji GoF oraz nowoczesnej inżynierii oprogramowania. Aby ułatwić ich przyswojenie oraz dostrzeżenie naturalnej ewolucji między nimi, zostały one podzielone na cztery spójne grupy tematyczne:

  1. Grupa I – Hermetyzacja Algorytmów, Żądań i Przepływu Sterowania:

    • Łańcuch Zobowiązań (2.1): Punkt wyjścia pokazujący przekazywanie żądania wzdłuż potoku obiektów obsługujących (TaskHandler).
    • Metoda Szablonowa vs Paradygmat Funkcyjny (2.2): Wynika bezpośrednio ze struktury TaskHandler z punktu 2.1. Pokazuje definiowanie szkieletu algorytmu w klasie bazowej oraz jego nowoczesną ewolucję w stronę funkcji wyższego rzędu (Func<T>, Action).
    • Strategia (2.3): Rozwija ideę wymienności algorytmów – zamiast dziedziczenia (jak w Metodzie Szablonowej) hermetyzuje całe algorytmy w wymiennych obiektach (np. IComparer<T>).
    • Polecenie / Command i MVVM (2.4): Hermetyzuje żądanie jako obiekt (ICommand), łącząc świat obiektowy z reaktywnym UI (WPF / MVVM RelayCommand) oraz domykając porównanie z Metodą Szablonową i Strategią.
  2. Grupa II – Zarządzanie Stanem, Historią i Cyklem Życia Zasobów:

    • Stan / State (3.1): Hermetyzuje zachowania zależne od wewnętrznego stanu obiektu w osobnych klasach, eliminując rozbudowane instrukcje warunkowe if/switch.
    • Pamiątka / Memento (3.2): Umożliwia bezpieczne zapisywanie i odtwarzanie migawek stanu obiektu bez naruszania jego hermetyzacji (fundament operacji Undo/Redo we wzorcu Polecenie).
    • RAII i IDisposable (3.3): Wiąże cykl życia zasobów niezarządzanych (pamięć, pliki, gniazda sieciowe) z czasem życia obiektu i blokiem using.
  3. Grupa III – Architektura Zdarzeniowa, Reaktywność i Systemy Rozproszone:

    • Obserwator / Observer i event w C# (4.1): Definiuje relację \(1:N\) powiadamiania o zmianie stanu oraz ukazuje niebezpieczeństwo wycieków pamięci (Lapsed Listener Problem) przy klasycznych zdarzeniach C#.
    • Weak Event Pattern (4.2): Bezpośrednia naprawa problemu wycieków pamięci Obserwatora z punktu 4.1 przy użyciu słabych referencji (WeakReference i WeakEventManager).
    • Mediator / Sekretarka i CQRS (4.3): Zmienia topologię komunikacji z siatki bezpośrednich powiązań \(N:M\) na układ gwiaździsty z centralnym pośrednikiem (biblioteka MediatR).
    • Event Aggregator, Wydawca-Abonent (Pub/Sub) i Saga (4.4): Skaluje wzorce komunikacyjne od architektury wtyczkowej (EventAggregator) aż po asynchroniczne mikroserwisy, brokery wiadomości (MassTransit) i rozproszone transakcje kompensacyjne (Saga).
  4. Grupa IV – Trawersowanie Struktur, Podwójne Wysłanie i Gramatyki:

    • Iterator / Kursor (5.1): Ujednolica sekwencyjny dostęp do struktur liniowych i nieliniowych (drzewa BFS/DFS, grafy) poprzez IEnumerable<T> oraz yield return.
    • Odwiedzający / Wizytator a Metody Rozszerzające (5.2): Pozwala dodawać nowe operacje do hierarchii obiektów bez modyfikacji ich klas, rozwiązując problem podwójnego wysłania (Double Dispatch), którego nie potrafią obsłużyć statyczne metody rozszerzające.
    • Interpreter (5.3): Reprezentuje gramatykę języka w postaci drzewa wyrażeń obiektowych (symboli terminalnych i nieterminalnych).
                        ┌──────────────────────────────────────────────┐
                        │     WZORCE CZYNNOŚCIOWE / BEHAWIORALNE       │
                        └──────────────────────┬───────────────────────┘
                                               │
         ┌─────────────────────────────────────┴─────────────────────────────────────┐
         ▼                                                                           ▼
┌─────────────────────────────────────────┐                 ┌─────────────────────────────────────────┐
│ GRUPA I: ALGORYTMY, ŻĄDANIA I POTOKI    │                 │ GRUPA II: STAN, HISTORIA I ZASOBY       │
├─────────────────────────────────────────┤                 ├─────────────────────────────────────────┤
│ 2.1. Łańcuch Zobowiązań (Potok ogniw)   │                 │ 3.1. Stan (Maszyna stanów zamiast if)   │
│ 2.2. Metoda Szablonowa (Klasowa vs Func)│                 │ 3.2. Pamiątka (Migawki stanu / Undo)    │
│ 2.3. Strategia (Wymienne algorytmy)     │                 │ 3.3. RAII / IDisposable (Czas życia)    │
│ 2.4. Polecenie (ICommand i MVVM)        │                 └─────────────────────────────────────────┘
└─────────────────────────────────────────┘                                          │
         │                                                                           ▼
         ▼                                                  ┌─────────────────────────────────────────┐
┌─────────────────────────────────────────┐                 │ GRUPA IV: STRUKTURY, DISPATCH I DRZEWA  │
│ GRUPA III: ZDARZENIA I ROZPROSZENIE     │                 ├─────────────────────────────────────────┤
├─────────────────────────────────────────┤                 │ 5.1. Iterator (IEnumerable, BFS/DFS)    │
│ 4.1. Obserwator i pułapka wycieków event│                 │ 5.2. Wizytator (Double Dispatch vs Ext.)│
│ 4.2. Weak Event (WeakReference i GC)    │                 │ 5.3. Interpreter (Gramatyki i drzewa)   │
│ 4.3. Mediator (Sekretarka, MediatR/CQRS)│                 └─────────────────────────────────────────┘
│ 4.4. Event Aggregator, Pub/Sub i Saga   │
└─────────────────────────────────────────┘

2. Hermetyzacja Algorytmów, Żądań i Przepływu Sterowania

2.1. Łańcuch Zobowiązań / Odpowiedzialności (Chain of Responsibility – Zakres Obiektowy)

Wzorzec Łańcuch Zobowiązań pozwala na przekazywanie żądań kolejno wzdłuż łańcucha potencjalnych obiektów je obsługujących. Każde ogniwo w łańcuchu decyduje, czy jest w stanie obsłużyć dane żądanie, a jeśli nie – przekazuje je dalej do swojego następnika (_successor).

Elementy wzorca:

Kod klasy bazowej TaskHandler oraz klas konkretnych ogniw:

public abstract class TaskHandler
{
    protected TaskHandler _successor;

    public TaskHandler SetNext(TaskHandler successor)
    {
        _successor = successor;
        return successor;
    }

    // Główna metoda sterująca przepływem (wykorzystuje wzorzec Template Method)
    public void Handle(int taskId, string taskType)
    {
        if (CanHandle(taskType))
        {
            Execute(taskId, taskType);
        }
        else if (_successor != null)
        {
            _successor.Handle(taskId, taskType);
        }
        else
        {
            // Tutaj lądują WSZYSTKIE nieobsłużone zadania na końcu łańcucha
            Console.WriteLine($"[ALERT] Task {taskId} ({taskType}) przeszedł przez cały łańcuch i NIE został obsłużony!");
        }
    }

    // Metody abstrakcyjne, które muszą zaimplementować klasy konkretne
    protected abstract bool CanHandle(string taskType);
    protected abstract void Execute(int taskId, string taskType);
}

// 1. ConcreteHandlerA (Obsługa zadań niskiego priorytetu)
public class LowPriorityHandler : TaskHandler
{
    protected override bool CanHandle(string taskType) => taskType == "Low";

    protected override void Execute(int taskId, string taskType)
    {
        Console.WriteLine($"Task {taskId} (Low) obsłużony przez LowPriorityHandler.");
    }
}

// 2. ConcreteHandlerB (Obsługa zadań wysokiego priorytetu)
public class HighPriorityHandler : TaskHandler
{
    protected override bool CanHandle(string taskType) => taskType == "High";

    protected override void Execute(int taskId, string taskType)
    {
        Console.WriteLine($"Task {taskId} (High) obsłużony przez HighPriorityHandler.");
    }
}

Użycie w kodzie:

var low = new LowPriorityHandler();
var high = new HighPriorityHandler();

low.SetNext(high); // Tworzenie łańcucha: Low -> High

low.Handle(101, "Low");
low.Handle(102, "High");
low.Handle(103, "Medium"); // Nieobsłużone przez żadne ogniwo

Przykładowe wyjście:

Task 101 (Low) obsłużony przez LowPriorityHandler.
Task 102 (High) obsłużony przez HighPriorityHandler.
[ALERT] Task 103 (Medium) przeszedł przez cały łańcuch i NIE został obsłużony!

💡 Zastosowanie w ekosystemie .NET:
Na wzorcu Łańcucha Zobowiązań opiera się cały potok przetwarzania żądań HTTP (Middleware Pipeline) w ASP.NET Core (app.UseAuthentication() -> app.UseAuthorization() -> app.UseRouting()), potoki wiadomości DelegatingHandler w HttpClient oraz zachowania potokowe IPipelineBehavior<TRequest, TResponse> w bibliotece MediatR.


2.2. Metoda Szablonowa (Template Method – Zakres Klasowy) vs Paradygmat Funkcyjny

Wzorzec Metody Szablonowej definiuje szkielet algorytmu w metodzie klasy bazowej, pozostawiając szczegóły implementacji poszczególnych kroków dla podklas. Pozwala to klasom pochodnym na nadpisywanie wybranych etapów algorytmu bez możliwości zmiany jego ogólnej struktury oraz kolejności wykonywania operacji.

A. Podejście tradycyjne (obiektowe / klasowe oparte na dziedziczeniu)

W podejściu klasycznym klasa bazowa definiuje nadrzędną metodę sterującą przepływem (niewirtualną lub oznaczoną jako sealed), która wywołuje w ustalonej sekwencji metody abstrakcyjne bądź chronione (tzw. operacje pierwotne).

Doskonałym przykładem zastosowania tej konstrukcji jest metoda Handle w klasie bazowej TaskHandler z punktu 2.1:

public abstract class TaskHandler
{
    protected TaskHandler _successor;

    public TaskHandler SetNext(TaskHandler successor)
    {
        _successor = successor;
        return successor;
    }

    // Główna metoda sterująca przepływem (wzorzec Template Method)
    public void Handle(int taskId, string taskType)
    {
        if (CanHandle(taskType))
        {
            Execute(taskId, taskType);
        }
        else if (_successor != null)
        {
            _successor.Handle(taskId, taskType);
        }
        else
        {
            Console.WriteLine($"[ALERT] Task {taskId} ({taskType}) nie został obsłużony!");
        }
    }

    // Kroki zmiennego algorytmu delegowane do podklas
    protected abstract bool CanHandle(string taskType);
    protected abstract void Execute(int taskId, string taskType);
}

Nadrzędny schemat postępowania z żądaniem (Handle) jest stały i zdefiniowany odgórnie. Konkretne klasy pochodne (np. LowPriorityHandler) implementują jedynie specyficzne dla siebie warunki brzegowe (CanHandle) oraz samą logikę wykonania operacji (Execute).

B. Podejście funkcyjne (Higher-Order Functions)

W podejściu funkcyjnym, zamiast klasycznego dziedziczenia i budowania głębokich hierarchii klas, wykorzystuje się funkcje wyższego rzędu (Higher-Order Functions). „Szkielet” algorytmu staje się wówczas metodą, która przyjmuje inne funkcje (Func<T>, Action) jako parametry wejściowe.

Przykład implementacji procesu obsługi zamówienia:

public void ProcessOrder(Func<bool> validate, Action save)
{
    // Stały krok algorytmu
    Console.WriteLine("Inicjalizacja potoku przetwarzania...");

    // Zmienny krok wstrzyknięty jako funkcja walidująca
    if (validate()) 
    {
        // Zmienny krok wstrzyknięty jako akcja zapisu
        save();
        Console.WriteLine("Proces zamówienia zakończony sukcesem.");
    }
    else
    {
        Console.WriteLine("Przerwano potok: błąd walidacji.");
    }
}

Struktura metody ProcessOrder jest niezmienna, jednak reguły weryfikacji oraz sposób zapisu danych są wstrzykiwane dynamicznie w postaci wyrażeń lambda w miejscu wywołania:

ProcessOrder(
    validate: () => order.TotalAmount > 0 && order.Items.Any(),
    save: () => database.Save(order)
);

Podejście to charakteryzuje się znacznie większą elastycznością, eliminuje konieczność tworzenia jednorazowych podklas i radykalnie ułatwia testowanie jednostkowe.


2.3. Strategia (Strategy – Zakres Obiektowy)

Wzorzec Strategia pozwala na zdefiniowanie rodziny algorytmów, hermetyzację każdego z nich w osobnej klasie implementującej wspólny interfejs i uczynienie ich wzajemnie wymiennymi. Strategia pozwala na dynamiczną podmianę algorytmu w trakcie działania programu (runtime), oddzielając klienta od szczegółów technicznych implementacji algorytmu.

Przykładowa implementacja oraz natywny odpowiednik IComparer<T> w .NET:

Klasycznym przykładem w bibliotece standardowej .NET jest interfejs IComparer<T> przekazywany do metody List<T>.Sort(). Możemy w locie zmienić sposób sortowania kolekcji (np. najpierw po nazwisku, a chwilę później po wieku lub wynagrodzeniu), wstrzykując inną strategię porównywania:

public record Employee(string Name, int Age, decimal Salary);

// 1. Interfejs Strategii (w .NET wbudowany jako IComparer<T>)
public class SortByAgeStrategy : IComparer<Employee>
{
    public int Compare(Employee? x, Employee? y) => x!.Age.CompareTo(y!.Age);
}

public class SortBySalaryDescendingStrategy : IComparer<Employee>
{
    public int Compare(Employee? x, Employee? y) => y!.Salary.CompareTo(x!.Salary);
}

// Użycie w kodzie:
var employees = new List<Employee>
{
    new("Jan Kowalski", 42, 12000m),
    new("Anna Nowak", 29, 15500m)
};

// Wymiana strategii w czasie wykonywania (runtime):
employees.Sort(new SortByAgeStrategy());
employees.Sort(new SortBySalaryDescendingStrategy());

Alternatywa w nowoczesnym .NET: Microsoft DI, Autofac i biblioteka Polly

W kontenerach DI wzorzec Strategii realizuje się poprzez rejestrację wielu implementacji pod jednym interfejsem i wybieranie właściwej strategii za pomocą klucza (IIndex<TKey, TStrategy> w Autofac lub [FromKeyedServices] w .NET 8), natomiast w obszarze odporności na błędy (Resilience) standardem produkcyjnym jest biblioteka Polly:

// Wstrzyknięcie wszystkich dostępnych strategii do selektora (Strategy Resolver):
public class PaymentProcessor(IEnumerable<IPaymentStrategy> strategies)
{
    public void ExecutePayment(string providerName, decimal amount)
    {
        var strategy = strategies.First(s => s.CanHandle(providerName));
        strategy.Pay(amount);
    }
}

2.4. Polecenie (Command – Zakres Obiektowy) oraz Wzorzec Architektoniczny MVVM

Wzorzec Polecenie hermetyzuje żądanie jako obiekt, umożliwiając parametryzowanie klientów różnymi żądaniami, tworzenie kolejek operacji, harmonogramowanie zadań lub logowanie historii operacji (wsparcie dla Undo / Redo). Obiekt polecenia spaja samą operację z jej odbiorcą (Receiver), a moment jej wywołania jest delegowany do obiektu wywołującego (Invoker).

A. Klasyczna implementacja GoF (Pilot i Urządzenie):

// 1. Command (Interfejs Polecenia)
public interface ICommand
{
    void Execute();
}

// 2. ConcreteCommandA (Polecenie Włącz)
public class TurnOnCommand : ICommand
{
    private readonly IDevice _tv;
    public TurnOnCommand(IDevice tv) => _tv = tv;
    public void Execute() => _tv.TurnOn();
}

// 2. ConcreteCommandB (Polecenie Wyłącz)
public class TurnOffCommand : ICommand
{
    private readonly IDevice _tv;
    public TurnOffCommand(IDevice tv) => _tv = tv;
    public void Execute() => _tv.TurnOff();
}

// 3. Receiver (Odbiorca)
public interface IDevice 
{ 
    void TurnOn(); 
    void TurnOff(); 
}

public class Television : IDevice
{
    public void TurnOn() => Console.WriteLine("Telewizor włączony.");
    public void TurnOff() => Console.WriteLine("Telewizor wyłączony.");
}

// 4. Invoker (Pilot)
public class RemoteControl
{
    private ICommand? _command;
    public void SetCommand(ICommand command) => _command = command;
    public void PressButton() => _command?.Execute();
}
Użycie w kodzie:
var tv = new Television();
var turnOn = new TurnOnCommand(tv);
var turnOff = new TurnOffCommand(tv);
var remote = new RemoteControl();

remote.SetCommand(turnOn);
remote.PressButton();

remote.SetCommand(turnOff);
remote.PressButton();

Przykładowe wyjście:

Telewizor włączony.
Telewizor wyłączony.

B. Ewolucja wzorca Polecenie we frameworkach UI: WPF / .NET MAUI / Avalonia i wzorzec MVVM

Wzorce projektowe ewoluują wraz z frameworkami. W środowisku WPF (Windows Presentation Foundation) oraz nowoczesnych wieloplatformowych frameworkach pokrewnych (Avalonia UI, .NET MAUI) wzorzec Polecenie stanowi fundament wzorca architektonicznego MVVM (Model-View-ViewModel):

using System.Windows.Input;

// W MVVM używamy interfejsu ICommand z przestrzeni System.Windows.Input
public class RelayCommand : ICommand 
{
    private readonly Action _execute;
    private readonly Func<bool>? _canExecute;

    public RelayCommand(Action execute, Func<bool>? canExecute = null)
    {
        _execute = execute ?? throw new ArgumentNullException(nameof(execute));
        _canExecute = canExecute;
    }

    public bool CanExecute(object? parameter) => _canExecute == null || _canExecute();
    public void Execute(object? parameter) => _execute();

    public event EventHandler? CanExecuteChanged;
    public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}

Widok (XAML) wiąże się deklaratywnie (Binding) bezpośrednio z właściwością typu ICommand w ViewModelu. Dzięki temu osiągamy trzy fundamentalne korzyści architektoniczne:

  1. Izolacja logiki biznesowej: W klasycznym podejściu Code-Behind (np. BadIncrementButton_Click) sprawdzanie limitu biznesowego i modyfikacja zmiennej odbywają się bezpośrednio obok odwołań do kontrolek UI (BadCountTextBlock.Text). Nie da się wówczas przetestować reguły „nie przekraczaj wartości 5” bez utworzenia ciężkiego obiektu okna MainWindow.
  2. Hermetyzacja stanu UI: W przypadku tradycyjnych zdarzeń (events) programista musi w kodzie ręcznie odwoływać się do właściwości wizualnej BadIncrementButton.IsEnabled = false. Gdy korzystamy z kontraktu ICommand, framework UI sam odpytuje metodę CanExecute() i automatycznie zarządza włączaniem oraz wyłączaniem kontrolki na ekranie.
  3. Czyste środowisko testowe: Klasa MainViewModel jest prostą klasą POCO (Plain Old C# Object), całkowicie odseparowaną od cyklu życia obiektów interfejsu graficznego. Testy jednostkowe wykonują się w czasie mikrosekund na dowolnym systemie (również w kontenerach Linux Docker w potoku CI/CD) bez konieczności uruchamiania wirtualnego wątku STA (Single-Threaded Apartment) wymaganego przez kontrolki WPF.

C. Analogie i powiązania: Metoda Szablonowa vs Polecenie (Command)

Wzorzec Metody Szablonowej (2.2) wykazuje bezpośrednie pokrewieństwo koncepcyjne ze wzorcem Polecenie (2.4). Główne punkty wspólne oraz różnice obejmują:

  1. Zakres i poziom działania (Obiekt vs Metoda): Wzorzec Polecenie hermetyzuje całe żądanie wraz z jego stanem i odbiorcą jako osobny obiekt implementujący wspólny interfejs (ICommand). Metoda Szablonowa działa bardziej punktowo – operuje wewnątrz pojedynczej metody, zarządzając sekwencją jej kroków.
  2. Separacja logiki stałej od zmiennej (DIP): Oba rozwiązania dążą do odizolowania stałego mechanizmu wywoławczego od dynamicznie wymiennej logiki wykonawczej, wspierając zasadę Dependency Inversion Principle (DIP). W Poleceniu obiekt wywołujący (Invoker) nie zna szczegółów komendy; w Metodzie Szablonowej szkielet algorytmu nie zna szczegółów implementacji poszczególnych etapów.
  3. Ewolucja w stronę paradygmatu funkcyjnego:
    • Wzorzec Polecenie w nowoczesnych frameworkach wykorzystuje klasę RelayCommand, do której właściwa akcja biznesowa jest przekazywana przez konstruktor w postaci delegatu Action. Zapobiega to eksplozji małych klas dla każdego przycisku w aplikacji.
    • Metoda Szablonowa podlega identycznej transformacji – zamiast tworzenia kolejnych podklas nadpisujących metody chronione protected abstract, zmienne kroki procesu przekazuje się bezpośrednio jako parametry Func<T> lub Action.

3. Zarządzanie Stanem, Historią i Cyklem Życia Zasobów

3.1. Stan (State – Zakres Obiektowy)

Wzorzec Stan pozwala obiektowi na dynamiczną zmianę swojego zachowania w zależności od jego wewnętrznego stanu. Obiekt kontekstu deleguje operacje do aktualnie przypisanego obiektu stanu i może wymieniać ten obiekt na inny w trakcie działania programu, co z zewnątrz sprawia wrażenie, jakby obiekt zmienił swoją klasę. Wzorzec ten pozwala wyeliminować rozbudowane, trudne w utrzymaniu bloki warunkowe (if/else lub switch) sterujące zachowaniem obiektu.

Przykład implementacji (Cykl życia dokumentu / zamówienia):

// Interfejs Stanu
public interface IOrderState
{
    void Pay(OrderContext context);
    void Ship(OrderContext context);
}

// Kontekst przechowujący aktualny stan
public class OrderContext
{
    public IOrderState CurrentState { get; set; } = new NewOrderState();

    public void Pay() => CurrentState.Pay(this);
    public void Ship() => CurrentState.Ship(this);
}

// Konkretny Stan 1: Nowe zamówienie
public class NewOrderState : IOrderState
{
    public void Pay(OrderContext context)
    {
        Console.WriteLine("Płatność zaakceptowana. Przejście do stanu: Opłacone.");
        context.CurrentState = new PaidOrderState();
    }

    public void Ship(OrderContext context)
        => Console.WriteLine("[BŁĄD] Nie można wysłać nieopłaconego zamówienia!");
}

// Konkretny Stan 2: Opłacone zamówienie
public class PaidOrderState : IOrderState
{
    public void Pay(OrderContext context)
        => Console.WriteLine("[INFO] Zamówienie zostało już wcześniej opłacone.");

    public void Ship(OrderContext context)
    {
        Console.WriteLine("Paczka przekazana kurierowi. Przejście do stanu: Wysłane.");
        context.CurrentState = new ShippedOrderState();
    }
}

// Konkretny Stan 3: Wysłane zamówienie
public class ShippedOrderState : IOrderState
{
    public void Pay(OrderContext context) => Console.WriteLine("[BŁĄD] Zamówienie już zamknięte.");
    public void Ship(OrderContext context) => Console.WriteLine("[BŁĄD] Paczka jest już w drodze.");
}

💡 Biblioteka Stateless w .NET:
W systemach produkcyjnych .NET do tworzenia hierarchicznych maszyn stanów powszechnie wykorzystuje się bibliotekę Stateless (stworzoną przez twórcę Autofac, Nicholasa Blumhardta), pozwalającą definiować stany i wyzwalacze (Triggers) za pomocą płynnego API (Fluent API).


3.2. Pamiątka (Memento – Zakres Obiektowy)

Wzorzec Pamiątka (ang. Memento) pozwala na zapisanie i odtworzenie poprzedniego stanu wewnętrznego obiektu (Originator – Obiekt Pierwotny) bez naruszania jego hermetyzacji (tj. bez ujawniania prywatnych pól na zewnątrz klasy).

Elementy wzorca:

// Niemutowalna Pamiątka (w C# 12 idealnie reprezentowana przez record)
public record EditorMemento(string TextContent, int CursorPosition, DateTime Timestamp);

// Originator (Obiekt Pierwotny)
public class TextEditor
{
    public string Content { get; set; } = string.Empty;
    public int Cursor { get; set; } = 0;

    public EditorMemento SaveSnapshot() => new(Content, Cursor, DateTime.UtcNow);

    public void RestoreSnapshot(EditorMemento memento)
    {
        Content = memento.TextContent;
        Cursor = memento.CursorPosition;
    }
}

// Caretaker (Opiekun zarządzający historią Undo)
public class HistoryCaretaker
{
    private readonly Stack<EditorMemento> _history = new();

    public void Backup(TextEditor editor) => _history.Push(editor.SaveSnapshot());

    public void Undo(TextEditor editor)
    {
        if (_history.TryPop(out var memento))
        {
            editor.RestoreSnapshot(memento);
        }
    }
}

3.3. Zabór Zasobu Jest Inicjalizacją (RAII – Zakres Obiektowy) i Wzorzec IDisposable

RAII (ang. Resource Acquisition Is Initialization – Zabór Zasobu Jest Inicjalizacją) to fundamentalna technika programistyczna (wywodząca się z C++ i zaadaptowana w .NET poprzez interfejsy IDisposable oraz IAsyncDisposable), która polega na powiązaniu cyklu życia zasobów zewnętrznych (pamięć natywna, uchwyty plików, połączenia z bazą danych, gniazda sieciowe, blokady synchronizacyjne) z czasem życia obiektu.

public sealed class TelemetryFileSession : IDisposable
{
    private readonly FileStream _fileStream;
    private bool _disposed;

    public TelemetryFileSession(string filePath)
    {
        // RAII: Nabycie zasobu w konstruktorze
        _fileStream = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None);
    }

    public void WriteData(ReadOnlySpan<byte> data)
    {
        ObjectDisposedException.ThrowIf(_disposed, this);
        _fileStream.Write(data);
    }

    public void Dispose()
    {
        if (!_disposed)
        {
            // Deterministyczne zwolnienie uchwytu plikowego systemu operacyjnego
            _fileStream.Dispose();
            _disposed = true;
        }
    }
}

// Użycie gwarantujące zwolnienie zasobu nawet w razie wyjątku:
using var session = new TelemetryFileSession("flight_log.bin");
session.WriteData(new byte[] { 0x01, 0xFF });

💡 Automatyczne zarządzanie IDisposable w kontenerach IoC/DI:
Zarówno Autofac (ILifetimeScope), jak i Microsoft.Extensions.DependencyInjection (IServiceScope) śledzą wszystkie utworzone przez siebie instancje implementujące IDisposable lub IAsyncDisposable. W momencie zamknięcia zasięgu (using var scope = container.BeginLifetimeScope()) kontener automatycznie wywołuje Dispose() na wszystkich powołanych w tym zasięgu obiektach w odwrotnej kolejności ich utworzenia.


4. Architektura Zdarzeniowa, Reaktywność i Systemy Rozproszone

4.1. Obserwator (Observer – Zakres Obiektowy) i Ryzyko Wycieków Pamięci w C#

Wzorzec Obserwator definiuje relację jeden-do-wielu (\(1:N\)) pomiędzy obiektami, w której, gdy stan obiektu Obserwowanego (Subject) ulega zmianie, wszystkie zależne od niego obiekty Obserwujące (Observer) są automatycznie powiadamiane i aktualizowane.

A. Klasyczna implementacja obiektowa GoF (ISubject oraz IObserver):

public interface ISubject
{
    // Dołącza obserwatora do tematu.
    void Attach(IObserver observer);
    // Odłącza obserwatora od tematu.
    void Detach(IObserver observer);
    // Powiadamia wszystkich obserwatorów o zdarzeniu.
    void Notify();
}

public interface IObserver
{
    // Odbiera aktualizację stanu od tematu
    void Update(ISubject subject);
}

// Temat (Subject) posiada istotny stan i powiadamia obserwatorów, gdy stan ulega zmianie.
public class Subject : ISubject
{
    public int State { get; set; } = 0;

    // Lista subskrybentów
    private List<IObserver> _observers = new List<IObserver>();

    public void Attach(IObserver observer)
    {
        Console.WriteLine("Subject: Attached an observer.");
        this._observers.Add(observer);
    }

    public void Detach(IObserver observer)
    {
        this._observers.Remove(observer);
        Console.WriteLine("Subject: Detached an observer.");
    }

    // Wyzwala aktualizację u każdego subskrybenta.
    public void Notify()
    {
        Console.WriteLine("Subject: Notifying observers...");
        foreach (var observer in _observers)
        {
            observer.Update(this);
        }
    }

    // Logika biznesowa wyzwalająca powiadomienie po zmianie stanu
    public void SomeBusinessLogic()
    {
        Console.WriteLine("\nSubject: I'm doing something important.");
        this.State = new Random().Next(0, 10);
        Thread.Sleep(15);
        Console.WriteLine("Subject: My state has just changed to: " + this.State);
        this.Notify();
    }
}

public class ConcreteObserverA : IObserver
{
    public void Update(ISubject subject)
    {
        if ((subject as Subject)!.State < 3)
        {
            Console.WriteLine("ConcreteObserverA: Reacted to the event.");
        }
    }
}

public class ConcreteObserverB : IObserver
{
    public void Update(ISubject subject)
    {
        var state = (subject as Subject)!.State;
        if (state == 0 || state >= 2)
        {
            Console.WriteLine("ConcreteObserverB: Reacted to the event.");
        }
    }
}
Użycie w kodzie:
var subject = new Subject();
var observerA = new ConcreteObserverA();
subject.Attach(observerA);

var observerB = new ConcreteObserverB();
subject.Attach(observerB);

subject.SomeBusinessLogic();
subject.SomeBusinessLogic();

subject.Detach(observerB);
subject.SomeBusinessLogic();

Przykładowe wyjście:

Subject: Attached an observer.
Subject: Attached an observer.

Subject: I'm doing something important.
Subject: My state has just changed to: 1
Subject: Notifying observers...
ConcreteObserverA: Reacted to the event.

Subject: I'm doing something important.
Subject: My state has just changed to: 4
Subject: Notifying observers...
ConcreteObserverB: Reacted to the event.
Subject: Detached an observer.

Subject: I'm doing something important.
Subject: My state has just changed to: 9
Subject: Notifying observers...

B. Natywna realizacja w języku C# przy użyciu delegatów i słowa kluczowego event

W języku C# wzorzec Obserwatora jest wbudowany bezpośrednio w składnię języka poprzez delegaty EventHandler<TEventArgs> oraz słowo kluczowe event:

public class GoingToVacationEventArgs : EventArgs
{
    public string Address { get; set; } = string.Empty;
    public string DefaultEmailResponse { get; set; } = string.Empty;
    public DateTime WhenItBegan { get; set; }
    public DateTime WhenItEnd { get; set; }
}

public class Person
{
    public event EventHandler<GoingToVacationEventArgs>? GotoVacationEvent;

    public void GoingToSopot()
    {
        GotoVacationEvent?.Invoke(this, new GoingToVacationEventArgs
        {
            Address = "Orzechowa 21",
            DefaultEmailResponse = "Jestem na urlopie",
            WhenItBegan = DateTime.Now,
            WhenItEnd = DateTime.Now.AddDays(10),
        });
    }
}

// Użycie w kodzie:
Person p = new Person();
p.GotoVacationEvent += (object? sender, GoingToVacationEventArgs eventArgs) =>
{
    Console.WriteLine("Poszedł na urlop:");
    Console.WriteLine(eventArgs.DefaultEmailResponse);
    Console.WriteLine($"Od Kiedy: {eventArgs.WhenItBegan}");
    Console.WriteLine($"Do Kiedy: {eventArgs.WhenItEnd}");
};
p.GoingToSopot();

C. Pułapka wycieków pamięci przy użyciu event (Lapsed Listener Problem)

Używanie słowa kluczowego event w C# niesie ze sobą poważne ryzyko wycieków pamięci (ang. memory leaks):

  1. Przyczyna: Gdy obiekt Obserwujący subskrybuje zdarzenie obiektu Obserwowanego (subject.NotifyMe += observer.OnNotify), wewnątrz listy delegatów obiektu subject zapisywana jest silna referencja (strong reference) wskazująca na instancję observer (Delegate.Target).
  2. Skutek: Nawet jeśli zmienna observer wyjdzie poza zasięg w kodzie klienckim i nie jest już nigdzie używana, Garbage Collector (GC) nie usunie jej z pamięci RAM, ponieważ długożyjący subject (np. Singleton) wciąż „trzyma” ją na swojej prywatnej liście subskrybentów!
  3. Rozwiązanie: Zawsze należy jawnie odpiąć zdarzenie operatorem -= w metodzie Dispose() obserwatora lub zastosować wzorzec Weak Event Pattern (opisany w punkcie 4.2).
❌ KLASYCZNY EVENT (Silna referencja blokuje Garbage Collector):
[ Globalny Subject (Singleton) ] ───(Silna referencja w liście event)───► [ Krótkotrwały Observer ]
                                                                           (ZOMBIE W PAMIĘCI RAM!
                                                                            GC nie może go usunąć)

✅ WEAK EVENT PATTERN (Słaba referencja pozwala GC zwolnić pamięć):
[ Globalny Subject ] ──► [ WeakEventManager ] ───(WeakReference<T>) - - ► [ Krótkotrwały Observer ]
                                                                           (GC bezpiecznie usuwa obiekt,
                                                                            gdy klient kończy pracę)

4.2. Wzorzec Słabego Zdarzenia (Weak Event Pattern – Zakres Obiektowy)

Weak Event to „naprawiony” Obserwator rozwiązujący problem wycieków pamięci spowodowanych przez „wiszące” subskrypcje zdarzeń. W platformie .NET wykorzystuje on klasę WeakReference (lub generyczną WeakReference<T>). Pozwala ona na trzymanie wskaźnika do obiektu w taki sposób, aby nie blokował on pracy Garbage Collectora. Jeśli jedynym odniesieniem do obiektu w całym procesie jest „słaba referencja”, GC może go bezpiecznie usunąć przy najbliższym cyklu odśmiecania pamięci.

// Zamiast bezpośredniego: source.SomeEvent += observer.OnEvent;
WeakEventManager<SourceClass, EventArgs>.AddHandler(source, "SomeEvent", observer.OnEvent);

Mechanizm działania pośrednika (WeakEventManager):

Zamiast bezpośredniego, silnego powiązania, wprowadzany jest pośrednik (Manager), który zarządza listą słabych referencji do subskrybentów:

  1. Subskrypcja: Obserwator rejestruje się w managerze (opakowany w WeakReference), a nie bezpośrednio na liście delegatów Źródła.
  2. Powiadomienie: Gdy wystąpi zdarzenie, manager przegląda swoją wewnętrzną listę słabych referencji.
  3. Weryfikacja i auto-czyszczenie: Jeśli obiekt pod słabą referencją wciąż żyje w pamięci (TryGetTarget(out var target) zwraca true), manager wywołuje jego metodę obsługi. Jeśli obiekt został już usunięty przez Garbage Collector (TryGetTarget zwraca false), manager automatycznie usuwa „martwy” rekord z listy subskrybentów.
🛠️ Dla dociekliwych: Własna, wieloplatformowa implementacja WeakEventManager w czystym .NET 8+ (Kliknij, aby rozwinąć)

Wbudowana klasa WeakEventManager<TEventSource, TEventArgs> wchodzi w skład biblioteki WindowsBase (część WPF). W aplikacjach serwerowych, konsolowych oraz wieloplatformowych (.NET 8 na systemach Linux / macOS) własny bezpieczny brokera słabych zdarzeń buduje się bezpośrednio w oparciu o WeakReference<Action<TEventArgs>> lub parę (WeakReference<object> Target, MethodInfo Method):

using System.Reflection;

public class WeakEventBroadcaster<TEventArgs> where TEventArgs : EventArgs
{
    private readonly List<(WeakReference<object>? Target, MethodInfo Method)> _handlers = new();

    public void Subscribe(EventHandler<TEventArgs> handler)
    {
        // Jeśli metoda jest instancyjna, zapisujemy słabą referencję do obiektu docelowego (Target)
        WeakReference<object>? weakTarget = handler.Target != null
            ? new WeakReference<object>(handler.Target)
            : null;

        _handlers.Add((weakTarget, handler.Method));
    }

    public void Publish(object sender, TEventArgs args)
    {
        for (int i = _handlers.Count - 1; i >= 0; i--)
        {
            var (weakTarget, method) = _handlers[i];

            if (weakTarget == null)
            {
                // Metoda statyczna – wywołujemy bezpośrednio
                method.Invoke(null, new object[] { sender, args });
            }
            else if (weakTarget.TryGetTarget(out object? targetInstance))
            {
                // Obiekt obserwatora nadal żyje w RAM – dostarczamy powiadomienie
                method.Invoke(targetInstance, new object[] { sender, args });
            }
            else
            {
                // Obiekt obserwatora został usunięty przez GC! Czyścimy martwą subskrypcję:
                _handlers.RemoveAt(i);
            }
        }
    }

    public int ActiveSubscribersCount => _handlers.Count;
}

Kiedy stosować wzorzec Weak Event?

Wzorzec ten (mimo minimalnego narzutu wydajnościowego wynikającego z dodatkowej warstwy abstrakcji i weryfikacji WeakReference) jest kluczowy w sytuacjach, gdy:


4.3. Mediator / Sekretarka (Mediator – Zakres Obiektowy) i Architektura CQRS

Wzorzec Mediator (nazywany też Sekretarką) ogranicza bezpośrednie interakcje między obiektami, zmuszając je do współpracy wyłącznie za pośrednictwem centralnego obiektu (Mediatora). Zmniejsza to drastycznie liczbę powiązań między komponentami (Kolaborantami), zamieniając chaotyczną siatkę zależności „każdy z każdym” na uporządkowaną topologię gwiazdy.

Praktyczne zastosowanie w nowoczesnych mikroserwisach i .NET: Biblioteka MediatR oraz CQS/CQRS

W nowoczesnych aplikacjach .NET wzorzec Mediatora jest powszechnie realizowany za pomocą biblioteki MediatR w połączeniu z architekturą CQS / CQRS (Command Query Responsibility Segregation):

  1. Separacja komend od zapytań: Mediator pozwala oddzielić komendy zmieniające stan systemu (Commands) od zapytań tylko do odczytu (Queries).
  2. Eliminacja Constructor Over-injection: Zamiast wstrzykiwać 10 różnych serwisów domenowych do jednego kontrolera HTTP, wstrzykujemy wyłącznie interfejs IMediator i wysyłamy obiekt żądania: await _mediator.Send(new GetUserQuery(id));.
  3. Całkowite rozluźnienie powiązań: Nadawca żądania nie wie, jaka klasa je obsłuży – Mediator wykorzystuje kontener DI, aby odnaleźć odpowiedni IRequestHandler<TRequest, TResponse> w czasie wykonywania programu.
// 1. Definicja zapytania (Query) i odpowiedzi:
public record GetShipStatusQuery(string Callsign) : IRequest<string>;

// 2. Handler obsługujący zapytanie (odizolowany od kontrolera):
public class GetShipStatusHandler : IRequestHandler<GetShipStatusQuery, string>
{
    public Task<string> Handle(GetShipStatusQuery request, CancellationToken ct)
        => Task.FromResult($"Okręt {request.Callsign}: Osłony 100%, Reaktor stabilny.");
}

// 3. Użycie po stronie klienta (np. w kontrolerze lub serwisie):
string status = await mediator.Send(new GetShipStatusQuery("ORBITER-01"));

4.4. Od Event Aggregatora do Systemów Rozproszonych: Wydawca-Abonent (Pub/Sub) i Saga

Gdy komunikacja między komponentami wykracza poza pojedynczy moduł lub pojedynczy proces, klasyczny Obserwator i Mediator ewoluują w trzy specjalistyczne wzorce architektoniczne:

  1. Event Aggregator (Zakres Obiektowy – komunikacja wewnątrzprocesowa):

    • Działa jak centralna centrala telefoniczna (In-Memory Message Bus) dla komponentów, które w ogóle o sobie nie wiedzą.
    • Przykład: W aplikacji z architekturą wtyczkową, wtyczka „A” publikuje komunikat PluginLoadedEvent, a wtyczka „B” subskrybuje ten typ komunikatu i go odbiera, mimo że oba moduły nie mają do siebie żadnych referencji projektowych.
  2. Wydawca-Abonent / Publish-Subscribe (Zakres Obiektowy – systemy rozproszone):

    • Rozszerzenie wzorca Obserwatora i Event Aggregatora na systemy rozproszone, z wykorzystaniem zewnętrznego pośrednika – Brokera Wiadomości (Message Broker).
    • W ekosystemie .NET standardem jest biblioteka MassTransit lub Wolverine, która realizuje ten wzorzec poprzez kolejki i wymienniki (RabbitMQ, Azure Service Bus, Apache Kafka), dostarczając asynchronicznie wiadomości do wielu niezależnych mikroserwisów jednocześnie.
  3. Saga (Zakres Obiektowy – koordynacja transakcji rozproszonych):

    • Wzorzec zarządzania stanem w wieloetapowych transakcjach rozproszonych, w których nie można użyć klasycznej transakcji bazodanowej ACID.
    • Saga to „długożyjący proces” (deterministyczna maszyna stanów), który koordynuje sekwencję komunikatów pomiędzy usługami (np. OrderCreated \(\rightarrow\) PaymentCharged \(\rightarrow\) InventoryReserved \(\rightarrow\) PackageShipped).
    • Akcje kompensacyjne (Compensation): Jeśli na którymkolwiek etapie wystąpi błąd (np. brak towaru w magazynie po pobraniu płatności), Saga automatycznie uruchamia kroki cofające zatwierdzone wcześniej operacje (np. wysyła komendę RefundPaymentCommand).
Przepływ pomyślny (Happy Path):
[1. Utwórz Zamówienie] ──► [2. Pobierz Płatność] ──► [3. Zarezerwuj Towar] ──► [4. Wyślij Paczkę]
                                                               │
Awaria na etapie 3 (Brak towaru w magazynie!)                  ▼ (Błąd!)
[Anuluj Zamówienie]   ◄── [Zwróć Płatność (Refund)] ◄──────────┘
(Kompensacja Sagi 2)        (Kompensacja Sagi 1)

5. Trawersowanie Struktur, Podwójne Wysłanie i Gramatyki

5.1. Iterator / Kursor (Iterator – Zakres Obiektowy)

Wzorzec Iterator (zwany też kursorem) dostarcza ujednolicony sposób na sekwencyjny dostęp do elementów obiektu zbiorczego bez ujawniania jego wewnętrznej reprezentacji (tablicy, listy wiązanej, drzewa czy grafu).

Obiektem zbiorczym nie musi być prosta płaska lista. Iterator doskonale sprawdza się przy strukturach nieliniowych i hierarchicznych:

W języku C# wzorzec ten jest wbudowany natywnie poprzez interfejsy IEnumerable<T> oraz IEnumerator<T> (oraz ich asynchroniczny odpowiednik IAsyncEnumerable<T> z C# 8+), a kompilator automatycznie generuje maszynę stanów iteratora po użyciu słów kluczowych yield return:

public class TreeNode<T>
{
    public T Value { get; }
    public List<TreeNode<T>> Children { get; } = new();

    public TreeNode(T value) => Value = value;

    // Iterator BFS (Przeszukiwanie wszerz - warstwami) wygenerowany przez yield return:
    public IEnumerable<T> TraverseBreadthFirst()
    {
        var queue = new Queue<TreeNode<T>>();
        queue.Enqueue(this);

        while (queue.Count > 0)
        {
            var current = queue.Dequeue();
            yield return current.Value;

            foreach (var child in current.Children)
                queue.Enqueue(child);
        }
    }

    // Iterator DFS (Przeszukiwanie w głąb)
    public IEnumerable<T> TraverseDepthFirst()
    {
        var stack = new Stack<TreeNode<T>>();
        stack.Push(this);

        while (stack.Count > 0)
        {
            var current = stack.Pop();
            yield return current.Value;

            // Dodajemy w odwrotnej kolejności, aby zachować porządek od lewej do prawej
            for (int i = current.Children.Count - 1; i >= 0; i--)
                stack.Push(current.Children[i]);
        }
    }
}

5.2. Odwiedzający / Wizytator (Visitor – Zakres Obiektowy) a Metody Rozszerzające

Wzorzec Odwiedzający (ang. Visitor) pozwala na zdefiniowanie nowej operacji na elementach złożonej struktury obiektów (np. drzewa Kompozytu) bez konieczności modyfikowania klas tych elementów (zgodnie z zasadą Open/Closed). Umożliwia dodanie nowego zachowania, które zależy od dokładnego typu konkretnej klasy odwiedzanego elementu.

Dlaczego metody rozszerzające (extension methods) nie zastępują Wizytatora? Problem Double Dispatch

Choć metody rozszerzające w C# pozwalają „dokleić” nową funkcję do istniejącej klasy lub interfejsu, nie zastępują one w pełni wzorca Wizytatora:

// 1. Interfejs Wizytatora posiadający dedykowaną metodę dla każdego konkretnego typu w hierarchii:
public interface IUnitVisitor
{
    void VisitSoldier(Soldier soldier);
    void VisitCommander(CommanderComposite commander);
}

// 2. Rozszerzenie interfejsu komponentu o metodę Accept(IUnitVisitor):
public interface IUnitComponent
{
    void Accept(IUnitVisitor visitor);
}

public class Soldier : IUnitComponent
{
    public int Health { get; set; } = 100;

    // Pierwszy Dispatch: polimorficzne wywołanie Accept() na obiekcie IUnitComponent.
    // Drugi Dispatch: przekazanie silnie typowanego 'this' (jako Soldier!) do visitora:
    public void Accept(IUnitVisitor visitor) => visitor.VisitSoldier(this);
}

public class CommanderComposite : IUnitComponent
{
    public string Rank { get; set; } = "Pułkownik";
    private readonly List<IUnitComponent> _children = new();

    public void AddChild(IUnitComponent unit) => _children.Add(unit);

    public void Accept(IUnitVisitor visitor)
    {
        visitor.VisitCommander(this);
        foreach (var child in _children)
        {
            child.Accept(visitor);
        }
    }
}

// 3. Konkretny Wizytator (np. eksporter raportu gotowości bojowej):
public class ReadinessAuditVisitor : IUnitVisitor
{
    public void VisitSoldier(Soldier soldier)
        => Console.WriteLine($"[AUDYT ŻOŁNIERZA] Zdrowie: {soldier.Health}%");

    public void VisitCommander(CommanderComposite commander)
        => Console.WriteLine($"[AUDYT DOWÓDCY] Stopień: {commander.Rank}");
}

5.3. Interpreter (Zakres Klasowy)

Wzorzec Interpreter definiuje, w jaki sposób można zinterpretować formalny język (gramatykę) przy użyciu obiektów. Stanowi to reprezentację reguł gramatycznych w postaci hierarchii klas (Drzewa Składni Abstrakcyjnej – AST), a zdania w tym języku są ewaluowane poprzez wywołanie metody Interpret na instancjach tych klas.

Elementy wzorca (Proste Wyrażenia Logiczne):

// 1. AbstractExpression
public interface IExpression
{
    bool Interpret(Context context);
}

// 2. TerminalExpression (Zmienna)
public class VariableExpression : IExpression
{
    private readonly string _name;
    public VariableExpression(string name) => _name = name;
    public bool Interpret(Context context) => context.GetValue(_name);
}

// 3. NonTerminalExpression (Operator Logiczny OR)
public class OrExpression : IExpression
{
    private readonly IExpression _exp1;
    private readonly IExpression _exp2;

    public OrExpression(IExpression exp1, IExpression exp2)
    {
        _exp1 = exp1;
        _exp2 = exp2;
    }

    public bool Interpret(Context context) => _exp1.Interpret(context) || _exp2.Interpret(context);
}

// 4. Context (Kontekst - zawiera mapę zmiennych i ich wartości)
public class Context
{
    private readonly Dictionary<string, bool> _variables = new Dictionary<string, bool>();

    public bool GetValue(string name) => _variables.ContainsKey(name) ? _variables[name] : false;
    public void SetValue(string name, bool value) => _variables[name] = value;
}

Użycie w kodzie (Ewaluacja wyrażenia A OR B):

var context = new Context();
context.SetValue("A", true);
context.SetValue("B", false);

var A = new VariableExpression("A");
var B = new VariableExpression("B");
var statement = new OrExpression(A, B);

bool result = statement.Interpret(context);
Console.WriteLine($"Wynik wyrażenia 'A OR B': {result}"); // True

Przykładowe wyjście:

Wynik wyrażenia 'A OR B': True

💡 Zastosowanie w ekosystemie .NET:
Wzorzec Interpretera (w połączeniu z Wizytatorem ExpressionVisitor) stanowi serce przestrzeni nazw System.Linq.Expressions. Drzewa wyrażeń (Expression<Func<T, bool>>) są interpretowane przez dostawców takich jak Entity Framework Core i tłumaczone w locie z kodu C# na natywne zapytania języka SQL.


6. Podsumowanie Wzorców Czynnościowych w Ekosystemie .NET i Kontenerach IoC/DI

Wzorzec Czynnościowy Zakres Odpowiednik w .NET / Bibliotekach Produkcyjnych Rola w Architekturze i Kontenerach IoC/DI
Łańcuch Zobowiązań Obiektowy ASP.NET Core Middleware, IPipelineBehavior (MediatR). Sekwencyjne potoki walidacji, logowania i obsługi błędów.
Metoda Szablonowa Klasowy Szkielety klas abstrakcyjnych lub delegaty Func/Action. Separacja niezmiennego szkieletu algorytmu od kroków zmiennych.
Strategia Obiektowy IComparer<T>, polityki odpornościowe Polly, IIndex<K,V>. Wymienne algorytmy wstrzykiwane w runtime zgodnie z DIP i OCP.
Polecenie (Command) Obiektowy ICommand / RelayCommand (MVVM), IRequest (MediatR). Hermetyzacja intencji, kolejkowanie zadań, izolacja UI od logiki.
Stan (State) Obiektowy Biblioteka Stateless, obiektowe maszyny stanów. Eliminacja instrukcji warunkowych switch/if zależnych od stanu.
Pamiątka (Memento) Obiektowy Niemutowalne rekordy migawek, mechanizmy Undo/Redo. Bezpieczny zapis i odtwarzanie stanu wewnętrznego obiektu.
RAII / IDisposable Obiektowy IDisposable, IAsyncDisposable, bloki using, ILifetimeScope. Deterministyczne zwalnianie zasobów niezarządzanych.
Obserwator & WeakEvent Obiektowy event EventHandler<T>, IObservable<T> (Rx.NET), WeakReference. Powiadomienia \(1:N\) bez ryzyka wycieków pamięci (Lapsed Listener).
Mediator Obiektowy Biblioteka MediatR (architektura CQS / CQRS). Redukcja zależności z siatki \(N:M\) do układu gwiaździstego \(1:N\).
Pub/Sub & Saga Obiektowy MassTransit, Wolverine (RabbitMQ / Azure Service Bus). Komunikacja między mikroserwisami i transakcje kompensacyjne.
Iterator Obiektowy IEnumerable<T>, IAsyncEnumerable<T>, yield return. Polimorficzne przechodzenie drzew (BFS/DFS), grafów i kolekcji.
Wizytator (Visitor) Obiektowy ExpressionVisitor (LINQ), podwójne wysłanie (Double Dispatch). Dodawanie operacji na hierarchiach klas bez modyfikacji ich kodu.
Interpreter Klasowy System.Linq.Expressions, silniki reguł biznesowych. Ewaluacja gramatyk formalnych i drzew składniowych AST.

7. Zadania Laboratoryjne

Zadania laboratoryjne do Części 3 zostaną udostępnione w osobnym dokumencie:
📄 Zadanie Laboratoryjne do Części 3: Architektura Behawioralna Systemu Dowodzenia Flotą Orbitalną (zad_lab_3_wzorce_behaw.md)


8. Literatura i Źródła

  1. Refactoring Guru: Wzorce projektowe w C# – Wzorce Czynnościowe (Behawioralne) – https://refactoring.guru/pl/design-patterns/behavioral-patterns
  2. Infotraining: Wzorce projektowe w C-Sharp – https://infotraining.bitbucket.io/cs-dp/
  3. Maciej Szczepaniak: Wzorce projektowe – czym są i dlaczego warto je znać? – https://devszczepaniak.pl/wzorce-projektowe/
  4. Dokumentacja Microsoft Learn: Weak Event Patterns in .NET – https://learn.microsoft.com/en-us/dotnet/desktop/wpf/events/weak-event-patterns
  5. Jimmy Bogard: MediatR – Simple, unambitious mediator implementation in .NET – https://github.com/jbogard/MediatR
  6. MassTransit Documentation: Sagas, State Machines and Distributed Publish/Subscribe – https://masstransit.io/documentation/patterns/saga