01
Zadanie wprowadzające: System Monitoringu Giełdowego i Subskrypcji Cien
Cel

Student demonstruje umiejętność implementacji modelu wydawca-subskrybent z wykorzystaniem zdarzeń (events) oraz delegatów. Zadanie ma na celu pokazanie, jak w nowoczesnym C# można odseparować logikę biznesową (zmiana ceny) od logiki reakcji (powiadomienia użytkownika) przy użyciu wyrażeń lambda. Student uczy się zarządzać listą subskrybentów oraz dynamicznie reagować na zmiany stanu obiektu.

Scenariusz

Wyobraź sobie, że budujesz rdzeń aplikacji dla biura maklerskiego, który musi informować inwestorów o gwałtownych ruchach cen na giełdzie. Klasa bazowa "Giełda" posiada słownik aktywów, których ceny zmieniają się losowo w symulowanym procesie. Twoim najważniejszym zadaniem jest zdefiniowanie delegata "InformatorCenowy" oraz opartego na nim zdarzenia "CenaUleglaZmienie". Klient aplikacji może "podpiąć się" pod to zdarzenie, przekazując fragment kodu (wyrażenie lambda), który zostanie wykonany za każdym razem, gdy cena konkretnej spółki przekroczy ustalony limit. Dzięki takiemu podejściu, system giełdowy nie musi wiedzieć, co klienci robią z otrzymaną informacją – może to być wysyłka e-maila, zapis do bazy danych lub po prostu wyświetlenie alertu na ekranie. Model ten pozwala na zachowanie czystości kodu i wysokiej modułowości – klasy inwestorów nie są bezpośrednio powiązane z klasą giełdy. Twoim wyzwaniem jest zapewnienie, aby powiadomienie zawierało kompletne dane: nazwę aktywa, starą cenę oraz nową cenę. Program powinien symulować kilka "ticków" giełdowych i pokazywać reakcje różnych subskrybentów na te same zdarzenia. Finalnie zademonstruj mechanizm odpinania subskrypcji, aby pokazać, że inwestor może w dowolnej chwili przestać śledzić rynek. Jest to klasyczny przykład wykorzystania programowania reaktywnego w środowisku .NET.

Sugerowane kroki do wykonania
  1. Zdefiniuj delegat o nazwie PriceChangedHandler przyjmujący nazwę spółki i nową cenę (decimal).
  2. Stwórz klasę Gielda, która zawiera publiczne zdarzenie (event) na podstawie tego delegata.
  3. Dodaj w klasie Gielda metodę ZmienCene(string spolka, decimal cena), która wywoła zdarzenie, jeśli są subskrybenci.
  4. W Main utwórz instancję klasy Gielda.
  5. Zasubskrybuj zdarzenie przez pierwszego inwestora za pomocą operatora += i wyrażenia lambda (wypisz alert na konsoli).
  6. Zasubskrybuj to samo zdarzenie przez drugiego inwestora (np. zapis logu do innego koloru konsoli).
  7. Wywołaj ZmienCene dla kilku różnych wartości i zaobserwuj, jak obie lambdy reagują jednocześnie.
  8. Użyj operatora -=, aby jeden z inwestorów zrezygnował z dalszych powiadomień.
  9. Ponownie zmień cenę i potwierdź, że tylko jeden subskrybent otrzymał wiadomość.
  10. Dodaj komentarz wyjaśniający, dlaczego check na null (onPriceChanged?.Invoke) jest niezbędny przy wywoływaniu zdarzenia.
Kod rozwiązania
using System;

// 1. Definicja delegata (kontraktu)
public delegate void NotifyDelegate(string text, decimal price);

class Gielda
{
    // 2. Definicja zdarzenia
    public event NotifyDelegate OnAlert;

    public void GenerujAlert(string akcja, decimal cena)
    {
        Console.WriteLine($"\n[SYSTEM] Rynek: {akcja} zmienia wartość na {cena:C}");
        // 3. Bezpieczne wywołanie zdarzenia
        OnAlert?.Invoke(akcja, cena);
    }
}

class Program
{
    static void Main()
    {
        Gielda g = new Gielda();

        // 4. Subskrypcja przez Lambda
        g.OnAlert += (s, p) => {
            if (p > 100) Console.WriteLine($"ALARM INWESTORA 1: Sprzedaj {s}! (drogo: {p})");
        };

        g.OnAlert += (s, p) => Console.WriteLine($"LOG SERWISU: Odczyt {s} = {p}");

        // Symulacja
        g.GenerujAlert("ORLEN", 65.20m);
        g.GenerujAlert("KGHM", 145.00m);
    }
}
                    
6.1
Kalkulator Funkcyjny (Delegaty jako parametry)
Cel

Zastosowanie delegatów do przekazywania logiki operacji matematycznych jako argumentów metod. Student uczy się odróżniać definicję operacji od mechanizmu jej wykonywania, co pozwala na budowę niezwykle elastycznych procesorów danych.

Scenariusz

Budujesz uniwersalny silnik obliczeniowy, który w przyszłości ma obsługiwać tysiące różnych wzorów matematycznych. Zamiast pisać wielką instrukcję "switch" dla każdego działania (dodawanie, mnożenie, potęgowanie), musisz zaprojektować metodę "WykonajDzialanie(double a, double b, Operacja op)". Gdzie "Operacja" jest delegatem przyjmującym dwa double i zwracającym wynik. Twoim zadaniem jest zaimplementowanie kilku konkretnych metod (Suma, Roznica) oraz wywołanie ich poprzez stworzony mechanizm silnika. Co więcej, musisz wykazać, że do Twojego kalkulatora można dopisać nową funkcjonalność "w locie" (np. obliczanie średniej), nie dotykając kodu źródłowego głównej metody obliczeniowej. Taki model pracy jest podstawą do budowania wtyczek i rozszerzeń w dużych systemach inżynierskich. Program w konsoli powinien poprosić o dwie liczby, a następnie kolejno zaprezentować wyniki dla wszystkich zarejestrowanych operacji. Dzięki delegatom, Twój kod staje się "lekki" i bardzo łatwy w utrzymaniu. Zakończ zadanie refleksją nad tym, jak ten wzorzec ułatwia testowanie jednostkowe poszczególnych działań matematycznych.

Sugerowane kroki do wykonania
  1. Zdefiniuj delegat: public delegate double OperacjaMat(double x, double y).
  2. Stwórz metodę SilnikObliczeń, która przyjmuje dwa parametry liczbowe oraz jeden parametr typu delegata.
  3. Napisz dwie tradycyjne metody: Suma i Iloczyn pasujące do sygnatury delegata.
  4. W Main wywołaj SilnikObliczeń, przekazując jako trzeci parametr nazwę metody Suma.
  5. Uruchom obliczenie ponownie, tym razem przekazując metodę Iloczyn.
  6. Zadeklaruj zmienną typu delegata i przypisz do niej jedną z metod, a następnie wywołaj ją bezpośrednio.
  7. Zademonstruj użycie wyrażenia lambda w miejscu trzeciego parametru (np. ból potęgowania).
  8. Wypisz wyniki wszystkich podejść z czytelnym opisem.
  9. Zwróć uwagę na bezpieczeństwo: sprawdź czy delegat nie jest nullem przed wywołaniem go wewnątrz silnika.
  10. Wyjaśnij w komentarzu, dlaczego delegaty nazywamy „wskaźnikami na metody”.
6.2
Uniwersalny Formater Logów (Func i Action)
Cel

Opanowanie pracy z gotowymi delegatami generycznymi Action i Func dostarczanymi przez platformę .NET. Student uczy się wykorzystywać te typy do szybkiej budowy systemów bez konieczności definiowania własnych nazw delegatów.

Scenariusz

Systemy operacyjne generują miliony logów dziennie, ale różni administratorzy chcą widzieć te dane w różnych formatach (np. JSON, CSV lub czysty tekst). Twoim zadaniem jest napisanie klasy "LogManager", która posiada metodę ProcesujWiadomosc. Metoda ta powinna przyjmować tekst logu oraz dwa delegaty generyczne: jeden typu "Func<string, string>" (który służy do sformatowania nagłówka) oraz drugi typu "Action<string>" (który decyduje, co zrobić z gotowym logiem – np. wyświetlić go na zielono w konsoli lub zapisać do zmiennej). Musisz przygotować kilka zestawów takich delegatów i pokazać, jak za pomocą jednej zmiany parametru całkowicie zmienia się wyjście Twojej aplikacji. Wykorzystanie Action i Func skraca kod i czyni go bardziej standardowym dla innych programistów C#. W tym zadaniu skup się na estetyce prezentacji danych – niech jeden format dodaje datę i godzinę, a inny unikalny prefiks [CRITICAL]. To ćwiczenie uczy projektowania potoków przetwarzania danych (pipelines), gdzie każdy etap (formatowanie, akcja końcowa) jest wymienny. Program kończy pracę po wyświetleniu trzech różnych wersji tego samego logu systemowego.

Sugerowane kroki do wykonania
  1. Zaimportuj przestrzeń nazw System.
  2. Zdefiniuj metodę GenerycznyLogger przyjmującą string msg, Func<string, string> format i Action<string> output.
  3. W Main utwórz zmienną formatującą typu Func, która dodaje prefix "[INFO] " oraz czas systemowy.
  4. Utwórz akcję typu Action, która wypisuje tekst na czerwono w konsoli (używając Console.ForegroundColor).
  5. Wywołaj GenerycznyLogger z nowo stworzonymi funkcjami.
  6. Zmień akcję tak, aby wypisywała tekst w ramce z gwiazdek, nie zmieniając metody GenerycznyLogger.
  7. Użyj wyrażenia lambda, aby na poczekaniu stworzyć nową funkcję transformującą tekst na wielkie litery.
  8. Przetestuj działanie programu dla trzech różnych kombinacji formatera i wyjścia.
  9. Pamiętaj o przywróceniu domyślnego koloru konsoli po akcji.
  10. Zapisz wniosek: kiedy lepiej użyć Func, a kiedy Action?
6.3
Inteligentny Budynek: Zdarzenia w klasie Czujnik
Cel

Dogłębne zrozumienie mechanizmu "event" i bezpieczeństwa przy jego wywoływaniu. Student uczy się projektować klasy reaktywne, które powiadamiają otoczenie o zmianach fizycznych mierzonych parametrów (np. temperatury).

Scenariusz

Projektujesz system bezpieczeństwa dla nowoczesnego biurowca klasy A. Sercem systemu jest klasa "CzujnikDymu", która co kilka sekund generuje odczyt poziomu zadymienia. Musisz zaimplementować publiczne zdarzenie o nazwie "AlarmPozarowy", które zostanie wywołane tylko wtedy, gdy poziom dymu przekroczy wartość 70 jednostek. Do tego zdarzenia powinny być podpięte różne moduły: "SystemZraszania", "PowiadomienieStraży" oraz "KomunikatGlosowy". Twoim zadaniem jest upewnienie się, że jeżeli nikt nie zasubskrybuje alarmu (np. serwis wyłączył moduły), program nie wyrzuci błędu przy próbie wywołania zdarzenia. Wykorzystaj nowoczesną składnię ze znakiem zapytania (Invoke?.), aby bezpiecznie obsłużyć brak subskrybentów. Symulacja w Main powinna pokazać rosnący poziom dymu: przy 20 jednostkach nic się nie dzieje, a przy 80 wszystkie moduły alarmowe powinny jednocześnie zareagować w konsoli. To zadanie uczy, jak budować hierarchie powiadomień bez tworzenia twardych powiązań między klasami (decoupling). Na koniec wyłącz (odsubskrybuj) system zraszania i sprawdź, czy straż pożarna nadal otrzymuje powiadomienie. Jest to kluczowe ćwiczenie z zakresu architektury systemowej.

Sugerowane kroki do wykonania
  1. Stwórz klasę Czujnik z polem Poziom.
  2. Zdefiniuj zdarzenie: public event Action<int> OnDanger.
  3. Zaimplementuj metodę Sprawdz(), która inkrementuje poziom dymu.
  4. Dodaj warunek: if (Poziom > 70) OnDanger?.Invoke(Poziom).
  5. W Main utwórz instancję Czujnika i trzy zmienne typu Action reprezentujące akcje ratunkowe.
  6. Przypisz akcje do zdarzenia czujnika korzystając z operatora +=.
  7. Wywołaj metodę Sprawdz() kilka razy w pętli for.
  8. Zademonstruj reakcję wszystkich modułów na przekroczenie progu bezpieczeństwa.
  9. Użyj -= aby odpiąć jeden z modułów i ponów test.
  10. Podsumuj działanie mechanizmu subskrypcji zdarzeń jednym zdaniem opisu technicznego.
6.4
Silnik Przetwarzania List (Predicate i Lambdy)
Cel

Zastosowanie wyrażeń lambda jako filtrów do przeszukiwania kolekcji. Student uczy się wykorzystywać delegat Predicate<T> do tworzenia uniwersalnych metod filtrujących, co znacznie redukuje powtarzalność kodu w aplikacjach bazodanowych.

Scenariusz

Pracujesz nad systemem rekrutacyjnym dla firmy z branży IT. Posiadasz listę kandydatów (klasa Kandydat z polami: Nazwisko, WynikEgzaminu, LataDoswiadczenia). Dział HR potrzebuje narzędzia, które pozwoli na błyskawiczne filtrowanie tej listy według zmieniających się kryteriów (np. "tylko osoby z doświadczeniem powyżej 5 lat", "tylko osoby z wynikiem powyżej 80%"). Zamiast pisać oddzielną metodę dla każdego filtra, musisz przygotować jedną uniwersalną metodę "FiltrujKandydatow", która jako parametr przyjmuje delagat typu Predicate<Kandydat>. Twoim zadaniem jest wywołanie tej metody w Main kilkukrotnie, przekazując różne wyrażenia lambda jako kryteria wyboru. Dzięki temu Twoje rozwiązanie jest niezwykle eleganckie – nowa reguła biznesowa zajmuje ułamek sekundy i nie wymaga zmian w logice filtrowania. Program powinien wyświetlić listę osób zakwalifikowanych do kolejnego etapu po zastosowaniu złożonego warunku (użycie operatora && w lambdzie). To ćwiczenie pokazuje, jak lambdy i delegaty zamieniają skomplikowane algorytmy w proste i czytelne polecenia. Finalnie porównaj czas i wysiłek potrzebny na napisanie filtrów tradycyjnych versus tych opartych na lambdach. Wyniki zaprezentuj w formie zestawienia porównawczego.

Sugerowane kroki do wykonania
  1. Zdefiniuj klasę Kandydat (Nazwisko, Wynik, Doswiadczenie).
  2. Utwórz List<Kandydat> z 6 osobami o różnych profilach.
  3. Napisz metodę static void WyswietlFiltrowane(List<Kandydat> lista, Predicate<Kandydat> filtr).
  4. Wewnątrz metody użyj pętli foreach i wywołaj filtr(k) dla każdego kandydata.
  5. W Main wywołaj metodę, przekazując lambdę: k => k.Wynik > 90.
  6. Wywołaj ponownie dla warunku: k => k.Doswiadczenie >= 2 && k.Doswiadczenie <= 4.
  7. Wywołaj dla szukania nazwiska na literę 'A' korzytając z k.Nazwisko.StartsWith("A").
  8. Zwróć uwagę na czytelność zapisu lambdy (brak słowa return przy jednej instrukcji).
  9. Przetestuj przypadek, w którym żaden kandydat nie spełnia warunku.
  10. Podsumuj korzyści płynące ze stosowania predykatów w logice biznesowej.
6.5
Rozbudowa Systemu: Metody Rozszerzające (Extension Methods)
Cel

Opanowanie techniki rozszerzania istniejących typów (nawet tych wbudowanych w C#) o nową funkcjonalność bez ich modyfikacji. Student uczy się pisać kod "fluent API", który jest bardziej intuicyjny i czytelny dla użytkownika końcowego.

Scenariusz

Pracujesz nad systemem analitycznym dla banku, który często musi przeliczać kwoty na różne waluty oraz sprawdzać, czy dany ciąg znaków jest poprawnym numerem konta. Zamiast tworzyć klasy narzędziowe typu "Helper", musisz "nauczyć" klasę double metody ToPln() oraz klasę string metody CzyToKonto(). Służą do tego metody rozszerzające, które są definiowane w statycznych klasach przy użyciu słowa kluczowego "this" przed pierwszym parametrem. Twoim zadaniem jest stworzenie takich metod i pokazanie ich użycia w konsoli tak, jakby były one naturalnymi częściami języka (np. 150.50.ToPln()). Taki sposób pisania kodu jest niezwykle popularny w nowoczesnym C# (praktycznie całe LINQ na tym bazuje). Program powinien sformatować kilka liczb jako kwoty walutowe oraz zweryfikować poprawność trzech testowych ciągów znaków (np. sprawdzenie długości i czy są same cyfry). To zadanie uczy, jak tworzyć "składnię przyjemną dla oka" i jak wzbogacać standardowe biblioteki .NET o własne reguły biznesowe. Zakończ projekt wyświetleniem sformatowanego raportu finansowego, w którym wszystkie ceny są wygenerowane przez Twoje nowe metody rozszerzające. Opisz w sprawozdaniu, dlaczego klasa rozszerzająca musi być statyczna.

Sugerowane kroki do wykonania
  1. Stwórz statyczną klasę o nazwie MojeRozszerzenia.
  2. Zdefiniuj w niej metodę public static string ToPln(this double kwota).
  3. Implementacja metody powinna zwracać kwotę z dopiskiem " PLN" i formatowaniem do 2 miejsc po przecinku.
  4. Dodaj drugą metodę rozszerzającą dla typu string: CzyEmail(this string s).
  5. Użyj s.Contains("@") jako uproszczonej walidacji wewnątrz metody rozszerzającej.
  6. W Main zadeklaruj zmienną double d = 123.456 i wywołaj d.ToPln().
  7. Spróbuj wywołać bezpośrednio na wartości: 50.0.ToPln() (pamiętaj o kropce dziętnej).
  8. Przetestuj walidację e-maila na kilku różnych stringach.
  9. Zauważ, że Twoje metody pojawiają się w podpowiedziach IntelliSense obok metod systemowych.
  10. Zapisz refleksję: jak metody rozszerzające wpływają na tzw. „czytelność kodu jak zdania w języku angielskim”.
6.6
Menedżer Multicasting - Rozsyłanie Powiadomień do Wielu Odbiorców
Cel

Zrozumienie właściwości delegatów polegającej na możliwości przechowywania referencji do wielu metod jednocześnie (Multicast Delegate). Student uczy się budować systemy rozgłoszeniowe, w których jedno wywołanie uruchamia cały łańcuch zdarzeń.

Scenariusz

Projektujesz system obsługi błędów dla krytycznego modułu bazy danych. W przypadku wystąpienia błędu krytycznego, aplikacja musi zareagować na kilka sposobów: zapisać log do pliku lokalnego, wyświetlić czerwony alert na konsoli oraz wysłać powiadomienie do administratora. Wykorzystaj mechanizm delegatów wielokrotnych (multicast), aby połączyć te trzy różne akcje (reprezentowane przez oddzielne metody) w jedną zmienną delegata. Twoim zadaniem jest pokazanie, jak za pomocą operatora "+=" można dynamicznie dokładać kolejne zadania do wykonania, a za pomocą "-=" łatwo je usuwać. Podczas symulacji awarii, program wywołuje tylko jedną instrukcję, która automatycznie uruchamia wszystkie podpięte funkcje ratunkowe. Pamiętaj, aby każda z metod wypisywała unikalny tekst, potwierdzający jej uruchomienie. To zadanie uczy, jak w elegancki sposób zarządzać złożonymi reakcjami systemu przy minimalnym nakładzie kodu w głównej logice sterującej. Zwróć uwagę, że kolejność wywołań wewnątrz delegata jest zgodna z kolejnością ich dodawania. Program powinien na koniec zademonstrować, co się stanie, gdy z listy wywołań zostanie usunięta jedna z metod. Jest to doskonałe przygotowanie do pracy z profesjonalnymi systemami typu Logging & Monitoring.

Sugerowane kroki do wykonania
  1. Zdefiniuj delegat Action<string> o nazwie AlarmHandler.
  2. Napisz trzy metody statyczne: ZapiszDoPliku, PokazWKonsoli, WyslijSMS.
  3. W Main utwórz zmienną zbiorczyAlarm typu AlarmHandler.
  4. Przypisz pierwszą metodę do zmiennej: zbiorczyAlarm = PokazWKonsoli.
  5. Użyj operatora +=, aby dołączyć ZapiszDoPliku oraz WyslijSMS.
  6. Wywołaj zbiorczyAlarm("AWARIA SERWERA NR 5").
  7. Zaobserwuj na ekranie komunikat od wszystkich trzech metod naraz.
  8. Odejmij ( -= ) metodę WyslijSMS ze zmiennej delegata.
  9. Ponownie wywołaj alarm i sprawdź, czy metoda SMS została tym razem pominięta.
  10. Wyjaśnij w sprawozdaniu, co się dzieje z wynikiem zwracanym (return) w przypadku delegata wielokrotnego.