01
Zadanie wprowadzające: System Wielokanałowych Płatności Elektronicznych
Cel

Student demonstruje umiejętność projektowania systemów opartych na klasach abstrakcyjnych i interfejsach w celu zapewnienia maksymalnej rozszerzalności kodu. Zadanie ma na celu pokazanie sytuacji, w której klasa bazowa definiuje wspólny szkielet algorytmu (klasa abstrakcyjna), a interfejsy służą do definiowania opcjonalnych kompetencji obiektów (np. autoryzacji). Student uczy się poprawnego łączenia tych dwóch mechanizmów w jednej hierarchii klas.

Scenariusz

Budujesz nowoczesny system bramki płatniczej dla dużego portalu aukcyjnego, który musi obsługiwać różne typy transakcji: płatność kartą, szybki przelew oraz system BLIK. Ponieważ każda płatność posiada wspólne cechy (kwotę, walutę, status), zdecydowałeś się na stworzenie abstrakcyjnej klasy bazowej MetodaPlatnosci, która uniemożliwi tworzenie "anonimowych" płatności. Klasa ta musi definiować abstrakcyjną metodę Przetworz(), której implementacja będzie różna dla każdego operatora. Dodatkowo niektóre formy płatności, jak karta kredytowa, wymagają dodatkowego stopnia bezpieczeństwa – do tego celu przygotujesz interfejs IAutoryzowalny z metodą Autoryzuj(). Twoim zadaniem jest zaimplementowanie konkretnych klas dla każdego kanału płatności, dbając o to, aby system potrafił zarządzać listą transakcji w sposób polimorficzny. Program w konsoli powinien symulować proces zakupowy, w którym użytkownik wybiera metodę, a system – w zależności od tego, czy dana metoda implementuje interfejs autoryzacji – wykonuje dodatkowe kroki sprawdzające. To zadanie uczy, jak projektować kontrakty w oprogramowaniu, które są odporne na błędy i łatwe do zrozumienia dla innych programistów. Finalny raport powinien zawierać zestawienie wszystkich przetworzonych płatności oraz ich statusów końcowych. Jest to klasyczny przykład architektury typu "plugin", gdzie nowe metody płatności mogą być dodawane bez zmiany istniejącego silnika bramki.

Sugerowane kroki do wykonania
  1. Zdefiniuj klasę abstrakcyjną MetodaPlatnosci z chronionym polem kwota i publiczną właściwością Status.
  2. Dodaj w tej klasie metodę abstrakcyjną public abstract void Przetworz().
  3. Stwórz interfejs IAutoryzowalny zawierający sygnaturę metody bool Autoryzuj().
  4. Zaimplementuj klasę KartaPlatnicza dziedziczącą po MetodaPlatnosci i implementującą IAutoryzowalny.
  5. Zaimplementuj klasę PrzelewBankowy dziedziczącą tylko po MetodaPlatnosci.
  6. W klasie KartaPlatnicza zaimplementuj metodę Autoryzuj() (np. prośba o PIN w konsoli).
  7. W metodzie Main utwórz listę List<MetodaPlatnosci> i dodaj do niej obiekty różnych typów.
  8. Uruchom pętlę foreach przechodzącą przez wszystkie płatności.
  9. Wewnątrz pętli użyj operatora is lub as, aby sprawdzić, czy aktualna płatność implementuje interfejs IAutoryzowalny.
  10. Jeśli tak – wywołaj autoryzację przed właściwym przetworzeniem płatności i wyświetl wynik.
Kod rozwiązania
using System;
using System.Collections.Generic;

// Interfejs - "Co system potrafi robić"
interface IAutoryzowalny
{
    bool Autoryzuj();
}

// Klasa abstrakcyjna - "Czym system jest"
abstract class MetodaPlatnosci
{
    public decimal Kwota { get; set; }
    public string Status { get; protected set; } = "Oczekująca";

    public abstract void Przetworz();
}

class Karta : MetodaPlatnosci, IAutoryzowalny
{
    public bool Autoryzuj()
    {
        Console.WriteLine("[KARTA] Proszę o PIN...");
        return true; // Symulacja sukcesu
    }

    public override void Przetworz()
    {
        Status = "Zatwierdzona kartą";
        Console.WriteLine($"Płatność kartą na kwotę {Kwota:c} zakończona.");
    }
}

class Blik : MetodaPlatnosci
{
    public override void Przetworz()
    {
        Status = "Zrealizowana BLIK";
        Console.WriteLine($"Płatność BLIK na kwotę {Kwota:c} gotowa.");
    }
}

class Program
{
    static void Main()
    {
        List<MetodaPlatnosci> bramka = new List<MetodaPlatnosci>();
        bramka.Add(new Karta { Kwota = 150.00m });
        bramka.Add(new Blik { Kwota = 45.50m });

        foreach (var p in bramka)
        {
            if (p is IAutoryzowalny aut)
            {
                if (aut.Autoryzuj()) p.Przetworz();
            }
            else
            {
                p.Przetworz();
            }
        }
    }
}
                    
4.1
Silnik Geometryczny: Klasa Abstrakcyjna Figura
Cel

Zrozumienie fundamentalnej różnicy między metodą wirtualną a abstrakcyjną. Student uczy się wymuszać implementację zachowań w klasach pochodnych poprzez definicje w klasie bazowej, która sama nie może posiadać instancji.

Scenariusz

Zaprojektuj rdzeń graficznego systemu CAD, który musi obsługiwać różnorodne kształty geometryczne. Ponieważ pojęcie "figura" jest zbyt ogólne, by można było obliczyć jej pole bez znajomości szczegółów, musisz stworzyć klasę abstrakcyjną o nazwie "Figura". Powinna ona zawierać nazwę kształtu oraz dwie metody abstrakcyjne: ObliczPole() oraz ObliczObwod(). Twoim zadaniem jest stworzenie klas konkretnych, takich jak "Kolo" oraz "Kwadrat", które dostarczą precyzyjnych wzorów matematycznych dla tych operacji. W klasie bazowej możesz jednak zaimplementować zwykłą metodę wirtualną WyswietlInfo(), która będzie wspólnym szablonem prezentacji danych dla wszystkich figur. Program w konsoli powinien stworzyć tablicę figur, wypełnić ją różnymi obiektami i w pętli obliczyć ich parametry statystyczne. To zadanie uczy, jak budować hierarchie, w których „rodzic” dyktuje zasady, a „dzieci” je realizują. Dzięki temu masz pewność, że każda nowa figura dodana do systemu (np. Trójkąt) będzie musiała posiadać funkcję liczenia pola. Program powinien na koniec wypisać sumę pól wszystkich figur znajdujących się w pamięci, co jest doskonałym testem polimorfizmu opartego na abstrakcji.

Sugerowane kroki do wykonania
  1. Zdefiniuj publiczną klasę abstrakcyjną Figura.
  2. Dodaj publiczną właściwość Nazwa typu string ustawianą w konstruktorze.
  3. Zadeklaruj w klasie Figura dwie metody z modyfikatorem abstract: ObliczPole i ObliczObwod.
  4. Zaimplementuj klasę Kolo dziedziczącą po Figura, dodając pole promienia.
  5. W klasie Kolo nadpisz wszystkie metody abstrakcyjne (override), używając stałej Math.PI.
  6. Zaimplementuj klasę Kwadrat dziedziczącą po Figura, dodając pole boku.
  7. W Main utwórz tablicę typu Figura[] z przynajmniej trzema różnymi obiektami.
  8. Użyj pętli foreach do wywołania metod obliczeniowych dla każdego elementu.
  9. Wyświetl sformatowany raport zawierający nazwę, pole i obwód każdej figury.
  10. Spróbuj utworzyć obiekt samej klasy Figura (new Figura()) i zaobserwuj błąd kompilacji.
4.2
System Logowania: Interfejs ILogger
Cel

Opanowanie definiowania i implementowania interfejsów jako kontraktów zachowania. Student uczy się programować "pod interfejs", co pozwala na łatwą wymianę komponentów systemu w trakcie działania aplikacji.

Scenariusz

W dużym systemie bankowym rejestrowanie zdarzeń (logowanie błędu, logowanie udanej transakcji) musi odbywać się w różnych miejscach w zależności od poziomu zagrożenia. Twoim zadaniem jest stworzenie interfejsu o nazwie "ILogger", który posiada jedną metodę: Log(string wiadomosc). Następnie zaimplementuj dwie klasy realizujące ten kontrakt: "ConsoleLogger", który wypisuje teksty na ekranie, oraz "FileLogger", który symuluje zapis danych do pliku (wypisując odpowiedni komunikat w konsoli z prefiksem [FILE]). Dzięki takiemu podejściu, Twoja główna aplikacja nie musi wiedzieć, gdzie dokładnie trafia log – ona po prostu wywołuje metodę z interfejsu. Stwórz klasę "SystemBankowy", która w swoim konstruktorze przyjmuje dowolny obiekt typu ILogger i zapisuje go w prywatnym polu. Podczas symulacji przelewu, system bankowy powinien wysłać kilka komunikatów diagnostycznych do zadeklarowanego loggera. W metodzie Main przetestuj system dwukrotnie: raz wstrzykując mu logger konsolowy, a drugi raz plikowy. To ćwiczenie pokazuje potęgę tzw. "odwrócenia sterowania" (Inversion of Control) i uczy budowania modularnego oprogramowania. Program kończy pracę dynamicznym podsumowaniem liczby wysłanych logów.

Sugerowane kroki do wykonania
  1. Zdefiniuj interfejs ILogger z metodą void Log(string mensaje).
  2. Utwórz klasę ConsoleLogger implementującą ten interfejs za pomocą Console.WriteLine.
  3. Utwórz klasę FileLogger implementującą interfejs (symulacja zapisu do pliku).
  4. Zdefiniuj klasę SystemOperacyjny, która posiada pole typu ILogger.
  5. Dodaj w SystemOperacyjny metodę Uruchom(), która wywołuje metodę we wskazanym loggerze.
  6. W Main utwórz instancję ConsoleLogger i przekaż ją do nowo tworzonego systemu.
  7. Wywołaj akcję w systemie i zaobserwuj wynik na konsoli.
  8. Zmień logger w locie (przypisanie nowej klasy do pola ILogger) i wywołaj akcję ponownie.
  9. Zwróć uwagę, że system nie musiał być modyfikowany, aby zmienić miejsce zapisu logów.
  10. Podsumuj lekcję krótkim wnioskiem o zaletach stosowania kontraktów interfejsowych.
4.3
Urządzenie Wielofunkcyjne: Multiple Interfaces
Cel

Demonstracja unikalnej cechy interfejsów, jaką jest możliwość implementacji wielu kontraktów przez jedną klasę (czego nie można zrobić z klasami). Student uczy się składać funkcjonalności obiektów z mniejszych, niezależnych modułów.

Scenariusz

Budujesz system sterowania dla nowoczesnego biura, który obsługuje zaawansowane urządzenia peryferyjne. Musisz zdefiniować dwa niezależne interfejsy: "IDrukowalne" (z metodą Drukuj) oraz "ISkanowalne" (z metodą Skanuj). Następnie stwórz trzy klasy: "DrukarkaTania" (implementuje tylko pierwszy interfejs), "SkanerReczny" (tylko drugi) oraz nowoczesne "UrzadzenieKombi", które implementuje oba interfejsy naraz. Twoim zadaniem jest opracowanie algorytmu, który zarządza listą biurowych maszyn i dla każdej z nich sprawdza jej możliwości techniczne. Jeśli maszyna jest drukowalna, program wysyła do niej dokument testowy; jeśli potrafi skanować – prosi o odczyt strony. Program musi wykazać, że klasa UrzadzenieKombi reaguje na oba te zapytania. To ćwiczenie jest kluczowe dla zrozumienia, jak w języku C# obchodzi się brak wielokrotnego dziedziczenia klas. Dzięki interfejsom Twój kombajn biurowy może być jednocześnie drukarką i skanerem, zachowując przy tym przejrzystą strukturę kodu. Finalny raport powinien listować nazwy wszystkich urządzeń wraz z ich potwierdzonymi funkcjami technicznymi. Program kończy pracę po pomyślnym przetestowaniu całego parku maszynowego.

Sugerowane kroki do wykonania
  1. Zdefiniuj interfejs IDrukowalne z metodą Drukuj().
  2. Zdefiniuj interfejs ISkanowalne z metodą Skanuj().
  3. Stwórz klasę Drukarka implementującą tylko IDrukowalne.
  4. Stwórz klasę Skaner implementującą tylko ISkanowalne.
  5. Zaprojektuj klasę Kombajn implementującą oba interfejsy: IDrukowalne, ISkanowalne.
  6. W Main utwórz listę obiektów (można użyć typu Object lub interfejsów).
  7. Przejdź przez listę pętlą foreach.
  8. Wykorzystaj słowo kluczowe is do dynamicznego sprawdzenia czy obiekt implementuje dany interfejs.
  9. Jeśli obiekt "is IDrukowalne", rzutuj go i wywołaj metodę Drukuj().
  10. Powtórz sprawdzenie dla ISkanowalne i przetestuj „Kombajn” pod kątem obu funkcji.
4.4
Menedżer Powiadomień: Polimorfizm Interfejsowy
Cel

Zastosowanie interfejsów do budowy elastycznego systemu rozsyłającego wiadomości. Student uczy się projektowania kolekcji obiektów opartych o wspólny interfejs, co umożliwia przetwarzanie ich w ujednolicony sposób bez względu na klasę pochodną.

Scenariusz

Zlecono Ci stworzenie centralnego serwera powiadomień dla portalu informacyjnego. System musi wysyłać wiadomości do subskrybentów za pomocą trzech kanałów: Email, SMS oraz Push. Twoim zadaniem jest stworzenie interfejsu "INotifier" z metodą SendMessage(string msg). Każda z klas implementujących ten interfejs powinna w specyficzny sposób wyświetlać informację o wysyłce (np. dopisując do tekstu numer telefonu w przypadku SMS lub adres serwera w przypadku Push). Główny program powinien posiadać listę obiektów typu INotifier, do której użytkownik może dodawać dowolną liczbę różnych powiadamiaczy. W momencie wystąpienia "ważnego wydarzenia", system pętlą przechodzi przez listę i wywołuje metodę SendMessage na każdym elemencie, nie wiedząc i nie dbając o to, czy jest to wiadomość tekstowa czy pocztowa. To zadanie obrazuje najważniejszą korzyść z interfejsów: możliwość traktowania różnych niepowiązanych klas w ten sam sposób, o ile spełniają one wspólny kontrakt. Dzięki temu dodanie nowego kanału (np. powiadomienie na Slack) zajmie Ci zaledwie kilka minut pracy. Program powinien na końcu zliczyć, ile powiadomień zostało pomyślnie wysłanych przez każdy z kanałów. Jest to modelowy przykład wzorca projektowego "Observer".

Sugerowane kroki do wykonania
  1. Stwórz interfejs INotifier z jedną metodą SendMessage.
  2. Zaimplementuj klasę EmailNotifier z prywatnym polem adresu e-mail.
  3. Zaimplementuj klasę SmsNotifier z polem numeru telefonu.
  4. Zaimplementuj klasę PushNotifier dla aplikacji mobilnych.
  5. W Main zadeklaruj kolekcję typu List<INotifier>.
  6. Dodaj do listy po jednym obiekcie każdej klasy pochodnej.
  7. Zdefiniuj zmienną string z treścią ogłoszenia (np. "Nowy artykuł w serwisie!").
  8. Użyj pętli foreach, aby rozesłać tę wiadomość do wszystkich aktywnych kanałów w kolekcji.
  9. Zauważ, że typem elementu w pętli jest INotifier, co umożliwia dostęp do metody SendMessage.
  10. Przetestuj program dodając dwa różne e-maile do tej samej listy.
4.5
Mechanika Gry: Klasa Abstrakcyjna vs Metoda Wirtualna
Cel

Pogłębiona analiza różnicy między całkowitą abstrakcją a częściową implementacją w klasie bazowej. Student uczy się modelować obiekty, które posiadają niezmienne cechy wspólne oraz zachowania specyficzne dla konkretnych odmian.

Scenariusz

Budujesz silnik dla prostej gry strategicznej, w której występują różne jednostki bojowe: "Rycerz" oraz "Strzelec". Podstawą systemu jest klasa abstrakcyjna "Jednostka", która posiada kilka cech wspólnych dla wszystkich wojowników, takich jak nazwa i punkty życia (HP). Klasa ta powinna implementować zwykłą metodę wirtualną PoruszSie(), która domyślnie wypisuje komunikat o marszu. Jednak kluczowa funkcja Atakuj() musi być oznaczona jako abstrakcyjna, ponieważ Rycerz używa miecza na bliski dystans, a Strzelec łuku z oddali – nie da się zdefiniować ogólnego sposobu walki. Twoim zadaniem jest stworzenie tych klas i nadpisanie metody ataku tak, aby każda jednostka prezentowała swój unikalny styl walki oraz zadawane obrażenia. Dodatkowo wzbogać Rycerza o umiejętność jazdy konnej poprzez nadpisanie metody PoruszSie(). W programie głównym przeprowadź symulację potyczki, w której obie jednostki wykonują serię ruchów i ataków. To zadanie pokaże Ci, jak klasy abstrakcyjne pozwalają na oszczędność kodu przy jednoczesnym wymuszaniu kluczowej logiki tam, gdzie jest ona unikalna. Program kończy się wyświetleniem statystyk końcowych (zdrowia) po zakończonej rundzie walki. Jest to doskonały wstęp do zaawansowanego programowania gier komputerowych.

Sugerowane kroki do wykonania
  1. Zdefiniuj klasę abstrakcyjną Jednostka z polami Zdrowie i Nazwa.
  2. Zaimplementuj w niej zwykłą metodę wirtualną PoruszSie() z domyślnym tekstem.
  3. Dodaj w klasie bazowej metodę abstrakcyjną public abstract void Atakuj().
  4. Stwórz klasę Rycerz, nadpisując metodę Atakuj() ("Mieczem!") oraz PoruszSie() ("Konno!").
  5. Stwórz klasę Lucznik, nadpisując tylko metodę Atakuj() ("Z łuku!").
  6. W Main utwórz obiekty obu klas i przypisz je do wspólnej listy typu Jednostka.
  7. Uruchom pętlę i dla każdego wojownika wywołaj ruch i atak.
  8. Zwróć uwagę, że Lucznik używa metody PoruszSie() z klasy bazowej, a Rycerz swojej własnej.
  9. Wyświetl stan zdrowia jednostek po symulowanym starciu.
  10. Dodaj komentarz wyjaśniający zalety łączenia metod wirtualnych z abstrakcyjnymi.
4.6
Dom inteligentny: Kolekcja urządzeń ISwitchable
Cel

Wykorzystanie interfejsów do zarządzania różnorodnymi urządzeniami domowymi w sposób ujednolicony. Student uczy się projektowania systemów kontrolnych, które operują na wspólnych funkcjonalnościach bez wnikania w detale implementacyjne.

Scenariusz

Wyobraź sobie, że piszesz oprogramowanie sterujące funkcjami inteligentnego domu. W systemie masz urządzenia o zupełnie różnym przeznaczeniu: "Lampa", "Telewizor" oraz "Klimatyzacja". Wspólną cechą tych wszystkich sprzętów jest to, że można je włączyć i wyłączyć – zdefiniuj więc interfejs "ISwitchable" z metodami TurnOn() oraz TurnOff(). Twoim zadaniem jest zaimplementowanie tego interfejsu w każdej z klas urzadzeń, dodając specyficzną logikę (np. lampa zmienia jasność, a klimatyzacja ustawia temperaturę startową). Dodatkowo stwórz klasę "Pilot", która w swojej pamięci przechowuje kolekcję wszystkich podłączonych urządzeń wspierających ten interfejs. Pilot posiada jedną unikalną metodę "MasterSwitch(bool state)", która jednym kliknięciem aktywuje lub deaktywuje wszystkie sprzęty w całym domu. Program w konsoli powinien pozwolić na dodanie kilku bazowych urządzeń i przetestowanie głównego wyłącznika. Dzięki zastosowaniu interfejsu, Twój Pilot będzie działał poprawnie z każdym nowym sprzętem, który pojawi się na rynku (np. inteligentnym czajnikiem), o ile tylko będzie on implementował ISwitchable. Taka architektura jest podstawą nowoczesnych systemów IoT. Finalny raport powinien pokazać aktualny status energetyczny całego mieszkania.

Sugerowane kroki do wykonania
  1. Stwórz interfejs ISwitchable z metodami TurnOn i TurnOff.
  2. Zaimplementuj klasę Swiatlo (wypisz "Światło włączone" w TurnOn).
  3. Zaimplementuj klasę Radio (wypisz "Radio gra" w TurnOn).
  4. Stwórz klasę KontrolerDomu posiadającą List<ISwitchable>.
  5. Dodaj metodę DodajUrzadzenie(ISwitchable u).
  6. Zaimplementuj w Kontrolerze funkcję WylaczWszystko(), która w pętli wywołuje TurnOff na każdym elemencie.
  7. W Main utwórz KontrolerDomu i dodaj do niego dwa różne światła i jedno radio.
  8. Przetestuj włączenie urządzeń pojedynczo.
  9. Zademonstruj użycie metody WylaczWszystko() i sprawdź komunikaty na konsoli.
  10. Refleksja: Jak łatwo można by dodać "Inteligentną Lodówkę" do tego systemu?