Modelowanie kontraktów i szkieletów aplikacji. Zrozumienie różnic między abstrakcją a implementacją interfejsową.
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.
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.
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(); } } } }
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.
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.
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.
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.
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.
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.
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ą.
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".
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.
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.
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.
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.