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.
abstract class MetodaPlatnosci - klasa nie może mieć modyfikatora sealed.public abstract decimal Kwota { get; set; }public abstract void Przetworz();interface IAutoryzowalny - nazwa z prefixem "I" jest konwencją C#.class KartaPlatnicza : MetodaPlatnosci, IAutoryzowalnyis: if (platnosc is IAutoryzowalny autoryzowalna) - tworzy nową zmienną.as: var aut = platnosc as IAutoryzowalny; if (aut != null)List<MetodaPlatnosci> plats = new List<MetodaPlatnosci>();plats.Add(new Karta { Kwota = 100m });foreach (var p in plats) { p.Przetworz(); }public string Status { get; protected set; } = "Oczekująca";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.
public abstract class Figurapublic Figura(string nazwa) { Nazwa = nazwa; }public abstract double ObliczPole();public virtual void WyswietlInfo() { Console.WriteLine(Nazwa); }class Kolo : Figura - nie używasz : Figura po lewej stronie.public override double ObliczPole() { return Math.PI * Promien * Promien; }Figura[] figury = new Figura[3]; figury[0] = new Kolo(5);foreach (var f in figury) { f.ObliczPole(); }Math.PI - podaj wartość przybliżoną 3.14159...return Bok * Bok;return 2 * Math.PI * Promien;new Figura() - otrzymasz błąd "cannot instantiate abstract class".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.
public interface ILogger { void Log(string wiadomosc); }public class ConsoleLogger : ILogger { public void Log(string msg) => Console.WriteLine(msg); }public SystemBankowy(ILogger logger) { this.logger = logger; }private readonly ILogger logger;logger.Log("Przelew wykonany");system = new SystemBankowy(new FileLogger());Console.WriteLine($"[FILE] {msg}");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.
public interface IDrukowalne { void Drukuj(); }public interface ISkanowalne { void Skanuj(); }class Drukarka : IDrukowalneclass Kombajn : IDrukowalne, ISkanowalneis: if (urzadzenie is IDrukowalne druk) { druk.Drukuj(); }if: if (urzadzenie is ISkanowalne) { ((ISkanowalne)urzadzenie).Skanuj(); }Object[]: Object[] urzadzenia = { new Drukarka(), new Skaner(), new Kombajn() };class A : B, C {} // błądis zwrócą true.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".
public interface INotifier { void SendMessage(string msg); }class EmailNotifier : INotifier { public string Email { get; set; } public void SendMessage(string msg) => Console.WriteLine($"Email do {Email}: {msg}"); }class SmsNotifier : INotifier { public string Numer { get; set; } public void SendMessage(string msg) => Console.WriteLine($"SMS na {Numer}: {msg}"); }class PushNotifier : INotifier { public string DeviceId { get; set; } public void SendMessage(string msg) => Console.WriteLine($"Push do {DeviceId}: {msg}"); }List<INotifier> notifiers = new List<INotifier>();notifiers.Add(new EmailNotifier { Email = "jan@example.com" });foreach (var n in notifiers) { n.SendMessage(tresc); }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.
public abstract class Jednostkapublic string Nazwa { get; set; } public int Zdrowie { get; set; } = 100;public virtual void PoruszSie() { Console.WriteLine($"{Nazwa} idzie pieszo..."); }public abstract void Atakuj();public override void PoruszSie() { Console.WriteLine($"{Nazwa} jedzie konno!"); }public override void Atakuj() { Console.WriteLine($"{Nazwa} strzela z łuku!"); }List<Jednostka> jednostki = new List<Jednostka>();j.Ruch(); j.Atak();Zdrowie -= 20;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 urządzeń, 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.
public interface ISwitchable { void TurnOn(); void TurnOff(); }class Lampa : ISwitchable { public void TurnOn() => Console.WriteLine("Lampa świeci!"); public void TurnOff() => Console.WriteLine("Lampa zgaszona."); }class Telewizor : ISwitchable { public void TurnOn() => Console.WriteLine("Telewizor gra..."); public void TurnOff() => Console.WriteLine("Telewizor wyłączony."); }class KontrolerDomu { private List<ISwitchable> urzadzenia = new List<ISwitchable>(); }public void Dodaj(ISwitchable u) { urzadzenia.Add(u); }public void WlaczWszystko() { foreach(var u in urzadzenia) u.TurnOn(); }public void WylaczWszystko() { foreach(var u in urzadzenia) u.TurnOff(); }public void MasterSwitch(bool wlacz) { foreach(var u in urzadzenia) if (wlacz) u.TurnOn(); else u.TurnOff(); }kontroler.Dodaj(new Lampa()); kontroler.Dodaj(new Telewizor()); kontroler.Dodaj(new Klimatyzacja());