01
Zadanie wprowadzające: System Zarządzania Zasobami IT
Cel

Student demonstruje umiejętność implementacji dziedziczenia oraz polimorfizmu poprzez tworzenie hierarchii klas z wykorzystaniem słów kluczowych virtual, override oraz base. Zadanie ma na celu pokazanie praktycznego zastosowania indeksatorów do przeszukiwania zbiorów obiektów oraz wykorzystania modyfikatorów out i params w metodach zarządzających zasobami firmy.

Scenariusz

Zostałeś poproszony o przygotowanie szkieletu systemu do ewidencji sprzętu komputerowego w dużej korporacji. System musi bazować na ogólnej klasie opisującej zasób (np. model, numer seryjny), z której będą dziedziczyły klasy szczegółowe, takie jak Komputer i Serwer. Każdy typ zasobu posiada własny sposób generowania opisu technicznego, co wymaga zastosowania metod wirtualnych i ich nadpisywania w klasach pochodnych. Dodatkowo musisz stworzyć klasę KontrolerZasobow, która będzie pełniła rolę kontenera na obiekty i udostępni wygodny dostęp do nich za pomocą indeksatora (wyszukiwanie po numerze seryjnym lub indeksie). System powinien również oferować metodę statyczną do szybkiego liczenia statystyk, wykorzystującą parametr out do zwrócenia wielu wyników naraz (np. liczbę serwerów i stacji roboczych). Kolejnym wymaganiem jest funkcja do masowego dodawania tagów opisowych do konkretnego zasobu, w której wykorzystasz parametr params. Program w konsoli powinien utworzyć kilka zróżnicowanych obiektów, dodać je do kontrolera, a następnie przeprowadzić serię testów: wyszukanie zasobu, zmianę jego statusu oraz wyświetlenie polimorficznego raportu. Całość rozwiązania musi być odporna na typowe błędy i stanowić modelowy przykład zastosowania czystego kodu obiektowego w C#.

Sugerowane kroki do wykonania
  1. Zdefiniuj klasę bazową Zasob z polami: Model i NumerSeryjny oraz wirtualną metodą PokazInfo().
  2. Zaimplementuj klasę pochodną Komputer, która dodaje pole RodzajProcesora i nadpisuje metodę bazową.
  3. Stwórz klasę pochodną Serwer, wywołującą konstruktor bazowy za pomocą base i dodającą specyficzne dane (np. liczba dysków).
  4. Zaprojektuj klasę KontrolerZasobow przechowującą tablicę obiektów klasy Zasob.
  5. Dodaj do Kontrolera indeksator public Zasob this[int index], który zwraca zasób o podanym numerze.
  6. Zaimplementuj metodę do dodawania tagów (DodajTagi) przyjmującą dowolną liczbę argumentów typu string (parametr params).
  7. Utwórz metodę statyczną ObliczStatystyki, która przez parametry out zwróci liczbę obiektów każdego typu.
  8. W metodzie Main utwórz instancje klas pochodnych i przypisz je do wspólnej tablicy typu Zasob[].
  9. Przetestuj polimorfizm, wywołując metodę PokazInfo() w pętli dla każdego elementu tablicy.
  10. Zademonstruj działanie indeksatora oraz metody z wieloma parametrami wyjściowymi (out).
Kod rozwiązania
using System;

class Zasob
{
    public string Model { get; set; }
    public string SN { get; set; }

    public Zasob(string model, string sn) { Model = model; SN = sn; }

    public virtual void PokazInfo() => Console.WriteLine($"Zasób: {Model}, SN: {SN}");
}

class Komputer : Zasob
{
    public string Procesor { get; set; }
    
    // base wywołuje konstruktor rodzica
    public Komputer(string model, string sn, string cpu) : base(model, sn) { Procesor = cpu; }

    public override void PokazInfo() => Console.WriteLine($"PC: {Model}, CPU: {Procesor}");
}

class Magazyn
{
    private Zasob[] dane = new Zasob[5];
    private int licznik = 0;

    public void Dodaj(Zasob z) { if (licznik < 5) dane[licznik++] = z; }

    // Indeksator
    public Zasob this[int i] => dane[i];

    // Metoda z params
    public static void LogujZdarzenie(string opis, params string[] tagi)
    {
        Console.WriteLine($"Zdarzenie: {opis}. Tagi: {string.Join(", ", tagi)}");
    }

    // Metoda z out
    public void GetInfo(out int ilosc, out string pierwszyModel)
    {
        ilosc = licznik;
        pierwszyModel = dane[0]?.Model ?? "Brak";
    }
}

class Program
{
    static void Main()
    {
        Magazyn m = new Magazyn();
        m.Dodaj(new Komputer("Dell OptiPlex", "SN001", "i7"));
        m.Dodaj(new Zasob("Monitor LG", "SN002"));

        // Przykład polimorfizmu
        for(int i=0; i < 2; i++) m[i].PokazInfo();

        Magazyn.LogujZdarzenie("Inwentaryzacja", "KRK", "BUDYNEK1", "PILNE");

        int count; string s;
        m.GetInfo(out count, out s);
        Console.WriteLine($"Stan: {count}, Pierwszy w bazie: {s}");
    }
}
                    
3.1
Matematyczne Metody Narzędziowe (out i params)
Cel

Zapoznanie studenta z nietypowymi sposobami przekazywania argumentów do metod w C#. Celem zadania jest opanowanie zwracania wielu wyników jednocześnie oraz projektowanie metod akceptujących dowolną liczbę parametrów wejściowych.

Scenariusz

Zlecono Ci przygotowanie zestawu metod pomocniczych dla silnika obliczeniowego, który będzie wykorzystywany przez programistów w innym dziale. Pierwsza metoda ma za zadanie podzielić dwie liczby stałoprzecinkowe, ale musi jednocześnie zwrócić iloraz oraz resztę z dzielenia przy użyciu modyfikatora out. Takie podejście pozwala na uniknięcie pisania dwóch oddzielnych metod dla jednej operacji matematycznej. Kolejnym Twoim zadaniem jest stworzenie uniwersalnego sumatora, który przyjmuje dowolną liczbę liczb całkowitych (typ int) jako parametry, wykorzystując słowo kluczowe params. Dzięki temu użytkownik silnika będzie mógł wywołać metodę przekazując jej dwie, pięć lub nawet zero liczb bez konieczności jawnego tworzenia tablicy. Musisz zadbać o poprawną obsługę błędów, na przykład próbę dzielenia przez zero w pierwszej metodzie. Program w konsoli powinien zaprezentować działanie obu metod dla różnych przypadków testowych. Finalne rozwiązanie ma być proste, szybkie i zgodne ze standardami projektowania metod w środowisku .NET.

Sugerowane kroki do wykonania
  1. Stwórz klasę o nazwie KalkulatorZaawansowany.
  2. Dodaj metodę DzielenieZReszta przyjmującą dwie liczby typu int.
  3. W sygnaturze metody zdefiniuj dwa parametry wyjściowe: out int iloraz oraz out int reszta.
  4. Wewnątrz metody wykonaj obliczenia i przypisz wyniki do odpowiednich parametrów out.
  5. Zaimplementuj metodę SumujWiele, która używa modyfikatora params int[] liczby.
  6. Przejdź pętlą przez otrzymaną tablicę i oblicz sumę wszystkich jej elementów.
  7. W metodzie Main wywołaj DzielenieZReszta i przechwyć oba wyniki do nowo utworzonych zmiennych.
  8. Wywołaj metodę SumujWiele przekazując jej bezpośrednio kilka liczb (np. 1, 4, 7, 2).
  9. Wyświetl wszystkie uzyskane wyniki z opisem co oznaczają poszczególne liczby.
  10. Zweryfikuj zachowanie sumatora przy wywołaniu go bez podania żadnych argumentów.
3.2
Elektroniczny Magazyn Części (Indeksatory)
Cel

Zrozumienie mechanizmu indeksowania obiektów jako sposobu na imitowanie zachowania tablicy wewnątrz własnej klasy. Student uczy się pisać akcesory get i set dla indeksatorów oraz zarządzać bezpiecznym dostępem do kolekcji wewnętrznej.

Scenariusz

Zarządzasz bazą danych części zamiennych w warsztacie samochodowym. Zamiast operować na surowej tablicy stringów, chcesz stworzyć klasę "Magazyn", która w sposób inteligentny zarządza zapasami. Twoim zadaniem jest dodanie do tej klasy indeksatora, który pozwoli na pobranie lub zmianę nazwy części za pomocą czytelnej składni: magazyn[3] = "Świeca zapłonowa". Indeksator powinien weryfikować, czy podany indeks mieści się w dopuszczalnym zakresie, i informować o błędzie, jeśli użytkownik spróbuje wyjść poza granice zadeklarowanej pamięci. Możesz również spróbować zaimplementować drugi, przeciążony indeksator, który pozwala wyszukać numer półki na podstawie nazwy części (indeksowanie stringiem). Program w konsoli powinien umożliwić użytkownikowi wypełnienie magazynu kilkoma pozycjami, a następnie ich szybką modyfikację poprzez wspomniany indeksator. To podejście znacznie upraszcza czytanie kodu i sprawia, że Twoje obiekty zachowują się jak natywne kolekcje języka C#. Finalnie system powinien wyświetlić listę wszystkich dostępnych części sformatowaną w kolumnach z odpowiadającymi im numerami indeksów.

Sugerowane kroki do wykonania
  1. Zdefiniuj klasę Magazyn zawierającą prywatną tablicę stringów o rozmiarze 10 elementów.
  2. Dodaj publiczny indeksator korzystający ze składni: public string this[int index].
  3. Wewnątrz akcesora get dodaj warunek sprawdzający granice tablicy (0 do 9).
  4. Wewnątrz akcesora set dodaj analogiczną walidację przed przypisaniem wartości do pola value.
  5. Zapewnij, aby próba dostępu do błędnego indeksu zwracała komunikat tekstowy "Błędna pozycja".
  6. W Main utwórz jeden obiekt klasy Magazyn.
  7. Przypisz wartości do trzech dowolnych indeksów przy użyciu prostej składni tablicowej.
  8. Wyświetl te wartości odczytując je bezpośrednio przez indeksator w pętli.
  9. Spróbuj odczytać wartość z indeksu 15 i zobacz, jak Twój program reaguje na ten błąd.
  10. Dodaj prosty licznik zajętych miejsc w magazynie i wyświetl go na końcu.
3.3
Pojazdy i Silniki: Podstawy Dziedziczenia
Cel

Opanowanie mechanizmu dziedziczenia oraz poprawnego wywoływania konstruktorów bazowych za pomocą base. Student uczy się hierarchicznego modelowania danych oraz reużywania kodu z klas nadrzędnych.

Scenariusz

Tworzysz system sterowania dla nowoczesnego garażu wielopoziomowego. Musisz przygotować strukturę klas opisującą różne typy pojazdów, zaczynając od ogólnej klasy "Pojazd", która posiada markę oraz typ napędu. Następnie stwórz klasę pochodną "Samochod", która dziedziczy wszystkie te cechy, ale dodaje od siebie unikalne pole, takie jak liczba drzwi czy pojemność bagażnika. Ważnym elementem jest to, aby konstruktor samochodu nie powielał logiki przypisywania marki, lecz przekazywał te dane „do góry” – do klasy nadrzędnej przy użyciu słowa base. Dzięki temu zapewnisz, że każde auto jest przede wszystkim poprawnym obiektem typu pojazd. Twoim zadaniem jest również nadpisanie podstawowej metody wyświetlającej informacje o obiekcie tak, aby samochód prezentował dodatkowe, specyficzne dla niego parametry. Program w konsoli musi utworzyć egzemplarz ogólnego pojazdu oraz egzemplarz konkretnego samochodu i porównać ich zachowanie. Takie podejście pozwala na łatwe dodawanie w przyszłości nowych typów transportu, np. motocykli czy ciężarówek, bez modyfikacji istniejącego silnika systemu. Finalnie wyświetl zestawienie obu obiektów, zwracając uwagę na różnice w ich opisach.

Sugerowane kroki do wykonania
  1. Utwórz klasę Pojazd z polami Marka i TypPaliwa.
  2. Zdefiniuj konstruktor klasy Pojazd, który inicjalizuje te pola.
  3. Zaimplementuj klasę Samochod dziedziczącą po klasie Pojazd (użyj operatora dwukropka).
  4. Dodaj w klasie Samochod dodatkowe pole LiczbaDrzwi.
  5. Stwórz konstruktor dla klasy Samochod przyjmujący trzy parametry.
  6. W wywołaniu konstruktora użyj : base(marka, paliwo), aby zainicjalizować bazę.
  7. Zdefiniuj w klasie bazowej wirtualną metodę PrzedstawSie().
  8. Nadpisz (override) tę metodę w Samochodzie, dodając informację o liczbie drzwi.
  9. W metodzie Main utwórz oba obiekty podając im przykładowe dane.
  10. Wywołaj PrzedstawSie na obu obiektach i zaobserwuj różnice w wyniku.
3.4
System Premiowy Korporacji (Polimorfizm)
Cel

Praktyczne zastosowanie polimorfizmu do rozwiązywania problemów biznesowych. Student uczy się, jak jedna metoda wirtualna zadeklarowana w klasie bazowej może zachowywać się w różny sposób w zależności od rzeczywistego typu obiektu.

Scenariusz

W dziale księgowości dużej firmy system naliczania premii rocznych działa według różnych reguł dla różnych stanowisk. Każdy pracownik jest reprezentowany przez klasę "Pracownik", która posiada metodę wirtualną o nazwie ObliczPremie. Dla przeciętnego pracownika biurowego premia wynosi stałe 10% wynagrodzenia bazowego. Musisz jednak stworzyć dwie klasy pochodne: "Dyrektor" oraz "Sprzedawca". Dla dyrektora premia powinna być powiększona o dodatkowy bonus stały za wyniki pionu, natomiast dla sprzedawcy jest ona uzależniona od obrotu, jaki wygenerował w ciągu roku. Twoim zadaniem jest nadpisanie metody ObliczPremie w tych klasach w taki sposób, aby każdy obiekt „wiedział”, jak policzyć własną premię. Kluczowym elementem zadania jest utworzenie tablicy typu Pracownik, w której umieścisz obiekty różnych klas (dyrektora i biuralistę obok siebie), a następnie wywołanie metody obliczeniowej w jednej wspólnej pętli. System powinien automatycznie dopasować algorytm do typu osoby bez konieczności sprawdzania tego w kodzie za pomocą instrukcji if. To zadanie demonstruje potęgę polimorfizmu w zarządzaniu złożonymi procesami biznesowymi.

Sugerowane kroki do wykonania
  1. Zdefiniuj klasę bazową Pracownik z polem Wynagrodzenie.
  2. Dodaj w niej metodę public virtual decimal ObliczPremie() zwracającą 10% pensji.
  3. Stwórz klasę Dyrektor dziedziczącą po Pracownik i dodaj tam pole DodatekDyrektorski.
  4. Nadpisz metodę ObliczPremie w Dyrektorze, dodając dodatek do standardowej premii (możesz użyć base.ObliczPremie()).
  5. Stwórz klasę Sprzedawca i nadpisz w niej metodę, aby liczyła premię jako 20% pensji.
  6. W Main utwórz tablicę typu Pracownik[] o rozmiarze 3.
  7. Umieść w tablicy po jednym obiekcie każdej klasy.
  8. Uruchom pętlę foreach przechodzącą przez tablicę pracowników.
  9. Dla każdego elementu wywołaj metodę ObliczPremie() i wyświetl wynik.
  10. Zauważ, że mimo iż zmienna w pętli jest typu Pracownik, C# wywołuje właściwe metody nadpisane.
3.5
Diagnostyka Systemów: New vs Override
Cel

Zrozumienie subtelnej, ale kluczowej różnicy między nadpisywaniem metod (override) a ich przesłanianiem (new). Student uczy się świadomego zarządzania nazewnictwem metod w hierarchii oraz dowiaduje się, jak wybór słowa kluczowego wpływa na wywołania polimorficzne.

Scenariusz

Wyobraź sobie, że piszesz system diagnostyczny dla dwóch typów urządzeń: standardowych sensorów oraz nowoczesnych czujników laserowych. Klasa bazowa "Sensor" posiada metodę Testuj(), która jest wirtualna. Klasa pochodna "SensorKasy" nadpisuje tę metodę (override), zmieniając jej zachowanie na bardziej precyzyjne. Z kolei inna klasa, "SensorSpecjalny", posiada metodę Testuj() oznaczoną słowem kluczowym new, co oznacza, że świadomie przesłania ona wersję z klasy bazowej, nie biorąc udziału w mechanizmie polimorfizmu. Twoim celem jest badanie, co się stanie, gdy przypiszemy oba te obiekty do zmiennych typu bazowego Sensor i wywołamy na nich metodę diagnostyczną. Czy program uruchomi nową wersję testu, czy powróci do starej, bazowej definicji? To zadanie jest bardzo techniczne i ma na celu uniknięcie błędów w dużych frameworkach, gdzie przesłonięcie metody zamiast jej nadpisania może prowadzić do bardzo trudnych do wykrycia błędów logicznych. Program w konsoli powinien wyraźnie pokazać różnice w wywołaniach dla obu przypadków. Dzięki temu zrozumiesz, że słowo kluczowe w sygnaturze metody ma decydujący wpływ na „ścieżkę wywołania” w pamięci komputera. Finalnie opisz w komentarzu zaobserwowane rezultaty eksperymentu.

Sugerowane kroki do wykonania
  1. Utwórz klasę Sensor z metodą public virtual void Testuj() wypisującą "Test bazowy".
  2. Utwórz klasę SensorNowoczesny dziedziczącą po Sensor i nadpisującą metodę przez override.
  3. W SensorNowoczesny wypisz "Test nowoczesny (override)".
  4. Utwórz klasę SensorUkryty dziedziczącą po Sensor i przesłaniającą metodę przez new.
  5. W SensorUkryty wypisz "Test ukryty (new)".
  6. W Main utwórz obiekt Sensor s1 = new SensorNowoczesny().
  7. W Main utwórz obiekt Sensor s2 = new SensorUkryty().
  8. Wywołaj s1.Testuj() oraz s2.Testuj().
  9. Zauważ rzadki przypadek: s2.Testuj() uruchomi wersję bazową, mimo że przypisano SensorUkryty!
  10. Dokonaj konwersji (rzutowania) s2 z powrotem na SensorUkryty i ponownie wywołaj metodę, aby zobaczyć różnicę.
3.6
Inteligentny Agregator Przesyłek (Kombinacja)
Cel

Integracja wszystkich poznanych mechanizmów: dziedziczenia, metod wirtualnych oraz parametrów params. Student buduje kompleksowy system, który wykazuje korzyści płynące z łączenia różnych zaawansowanych technik programistycznych w C#.

Scenariusz

Jako lider zespołu IT w firmie kurierskiej, przygotowujesz mechanizm do wyliczania kosztów transportu różnych typów paczek. Stwórz główną klasę "Przesylka" z wirtualną metodą ObliczKoszt() oraz właściwością Waga. Następnie zaimplementuj klasy "PaczkaStandardowa" i "PrzesylkaEkspresowa", które w różny sposób nadpisują algorytm cenowy (np. ekspres dodaje stałą opłatę za priorytet). Dodatkowo Twoja klasa bazowa powinna posiadać konstruktor, który przyjmuje wagę i przekazuje ją do pola chronionego (protected). Kolejnym poziomem trudności jest stworzenie klasy statycznej "Logistyka", która zawiera metodę WyslijZbiorczo. Ta metoda powinna przyjmować parametr params Przesylka[] i obliczać całkowity koszt wysłania wszystkich paczek przekazanych w jednym wywołaniu. Dzięki polimorfizmowi, metoda ta będzie działać poprawnie niezależnie od tego, czy wyślemy jej trzy paczki zwykłe, czy pięć ekspresowych. System musi wyświetlić szczegółowy raport z podróży zbiorczej, listując każdą paczkę i jej wyliczony indywidualnie koszt. Taki projekt uczy, jak budować elastyczne interfejsy programistyczne odporne na zmiany wymagań biznesowych. Program kończy się wyświetleniem wielkiego podsumowania dla całej floty wysyłkowej.

Sugerowane kroki do wykonania
  1. Zdefiniuj klasę bazową Przesylka z mianem wirtualnym metadzie ObliczKoszt.
  2. Dodaj pole chronione protected double waga i zainicjalizuj je w konstruktorze.
  3. Stwórz klasę PaczkaStandardowa, nadpisując koszt jako waga * 2 zł.
  4. Stwórz klasę PrzesylkaEkspresowa, nadpisując koszt jako waga * 5 zł + 20 zł stałej opłaty.
  5. Dodaj klasę Magazyn Wysylkowy, która będzie zbierać obiekty.
  6. Zaimplementuj metodę public static void PodsumujWszystko(params Przesylka[] lista).
  7. Użyj pętli do przejścia przez tablicę params i sumowania wyników metody ObliczKoszt.
  8. W Main utwórz kilka obiektów paczek różnych typów o różnych wagach.
  9. Wywołaj metodę statyczną przekazując jej wszystkie paczki oddzielone przecinkami.
  10. Wyświetl łączny koszt operacji logistycznej w oknie konsoli.