Interfejsy w C# – kontrakty określające umiejętności
Nauczysz się definiować „co obiekt potrafi” bez określania „jak to robi”. Interfejsy to klucz do elastycznego, rozszerzalnego kodu i wielokrotnej „dziedziczenia” zachowań!
Problem – tylko jedna klasa bazowa
W poprzednich materiałach poznaliśmy dziedziczenie i klasy abstrakcyjne. Ale jest ograniczenie: w C# klasa może dziedziczyć tylko po jednej klasie. Co jeśli potrzebujemy więcej?
// Mamy różne "umiejętności" jako klasy abstrakcyjne public abstract class Plywajacy { public abstract void Plyn(); } public abstract class Latajacy { public abstract void Lec(); } public abstract class Chodzacy { public abstract void Idz(); } // Kaczka pływa, lata I chodzi... ale to nie zadziała! public class Kaczka : Plywajacy, Latajacy, Chodzacy // ❌ BŁĄD! { // "Class 'Kaczka' cannot have multiple base classes" } // Samolot lata, ale nie pływa ani nie chodzi public class Samolot : Latajacy // OK, ale tylko jedna umiejętność { public override void Lec() { } } // Amfibia pływa I jeździ... też nie zadziała! public class Amfibia : Plywajacy, Chodzacy // ❌ BŁĄD! { }
Problem zwany „diamentem śmierci” (Diamond Problem):
- Co jeśli obie klasy bazowe mają metodę o tej samej nazwie?
- Którą wersję odziedziczyć?
- Języki jak C++ to pozwalają, ale prowadzi to do komplikacji
C# rozwiązuje to interfejsami – możesz implementować wiele interfejsów!
NIEDOZWOLONE w C#!
DOZWOLONE!
Co to jest interfejs?
Interfejs to „kontrakt” – lista metod i właściwości, które klasa obiecuje zaimplementować. Interfejs mówi CO obiekt potrafi, ale nie JAK to robi.
Pomyśl o interfejsie jak o certyfikacie:
- Certyfikat „Kierowca” = osoba umie prowadzić pojazd
- Certyfikat „Ratownik” = osoba umie udzielić pierwszej pomocy
- Certyfikat „Programista” = osoba umie pisać kod
Jedna osoba może mieć wiele certyfikatów! Podobnie klasa może implementować wiele interfejsów.
Inna analogia:
- Interfejs = standard gniazdka (np. europejskie typu E)
- Klasy implementujące = różne urządzenia (laptop, telewizor, toster)
Każde urządzenie „implementuje” interfejs gniazdka – ma pasującą wtyczkę. Gniazdko nie wie, czy podłączony jest laptop czy toster – wie tylko, że „coś co pobiera prąd”.
Kluczowe cechy interfejsu
| Cecha | Opis |
|---|---|
| Tylko deklaracje | Interfejs mówi CO, nie JAK (brak implementacji*) |
| Brak pól | Nie może mieć zmiennych (tylko właściwości) |
| Brak konstruktora | Nie można utworzyć instancji interfejsu |
| Wszystko publiczne | Nie ma modyfikatorów dostępu (domyślnie public) |
| Wielokrotna implementacja | Klasa może implementować wiele interfejsów |
| Nazwa zaczyna się od I | Konwencja: IComparable, IDisposable, IEnumerable |
* Od C# 8.0 interfejsy mogą mieć domyślne implementacje – to zaawansowany temat.
────────────
Tylko deklaracja!
Brak implementacji
Podstawowa składnia
Interfejs definiujemy słowem kluczowym interface. Nazwa interfejsu w C# zaczyna się od wielkiej litery I (konwencja).
// Definicja interfejsu public interface IPlywa { // Tylko DEKLARACJA metody – brak ciała! void Plyn(); // Bez public – domyślnie publiczne // Metoda z parametrem void PlynDoCelu(string cel); // Metoda zwracająca wartość double PobierzPredkosc(); } public interface ILatajacy { void Lec(); void Laduj(); int PobierzWysokosc(); } public interface IJadalny { int PobierzKalorie(); bool CzySwiezy(); }
public interface IBledny
{
// ❌ POLA – niedozwolone!
private int pole;
// ❌ KONSTRUKTOR – niedozwolone!
public IBledny() { }
// ❌ IMPLEMENTACJA (w starszych wersjach C#)
void Metoda() { Console.WriteLine("Coś"); }
}
Interfejs to czysta deklaracja – mówi CO, nie JAK.
Porównanie składni
Klasa abstrakcyjna
public abstract class Plywajacy
{
// Ma pola
protected int predkosc;
// Ma konstruktor
public Plywajacy() { }
// Ma implementację
public void Info() { }
// Metoda abstrakcyjna
public abstract void Plyn();
}
Interfejs
public interface IPlywa
{
// Brak pól!
// Brak konstruktora!
// Brak implementacji!
// Tylko deklaracje
void Plyn();
int PobierzPredkosc();
}
Implementacja interfejsu
Klasa implementuje interfejs używając dwukropka (tak jak przy dziedziczeniu). Musi zaimplementować wszystkie metody zadeklarowane w interfejsie.
public interface IPlywa { void Plyn(); double PobierzPredkosc(); } // Klasa implementuje interfejs public class Kaczka : IPlywa { private double predkosc = 5.0; // MUSI zaimplementować Plyn() public void Plyn() { Console.WriteLine("Kaczka płynie, machając łapkami 🦆"); } // MUSI zaimplementować PobierzPredkosc() public double PobierzPredkosc() { return predkosc; } // Może mieć własne metody public void Kwacz() { Console.WriteLine("Kwa kwa!"); } } public class Lodka : IPlywa { public int MocSilnika { get; set; } public void Plyn() { Console.WriteLine($"Łódka płynie z mocą {MocSilnika} KM ⛵"); } public double PobierzPredkosc() { return MocSilnika * 2.5; // Prędkość zależy od mocy } }
Kaczka kaczka = new Kaczka(); kaczka.Plyn(); // "Kaczka płynie, machając łapkami 🦆" kaczka.Kwacz(); // "Kwa kwa!" Lodka lodka = new Lodka { MocSilnika = 50 }; lodka.Plyn(); // "Łódka płynie z mocą 50 KM ⛵" Console.WriteLine(kaczka.PobierzPredkosc()); // 5 Console.WriteLine(lodka.PobierzPredkosc()); // 125
Zauważ, że przy implementacji interfejsu nie używamy słowa override. Używamy go tylko przy nadpisywaniu metod virtual lub abstract z klas.
// Interfejs – BEZ override:
public void Plyn() { }
// Klasa abstrakcyjna – Z override:
public override void Plyn() { }
Wiele interfejsów naraz – główna siła!
To jest główna zaleta interfejsów: klasa może implementować wiele interfejsów jednocześnie. To rozwiązuje problem z sekcji 1!
// Trzy różne interfejsy = trzy różne umiejętności public interface IPlywa { void Plyn(); } public interface ILata { void Lec(); } public interface IChodzi { void Idz(); } // Kaczka implementuje WSZYSTKIE TRZY! 🎉 public class Kaczka : IPlywa, ILata, IChodzi { public string Imie { get; set; } public void Plyn() { Console.WriteLine($"{Imie} płynie po stawie 🦆"); } public void Lec() { Console.WriteLine($"{Imie} leci na południe 🦆✈️"); } public void Idz() { Console.WriteLine($"{Imie} kaczo spaceruje 🦆🚶"); } } // Ryba – tylko pływa public class Ryba : IPlywa { public void Plyn() { Console.WriteLine("Ryba płynie pod wodą 🐟"); } } // Samolot – tylko lata public class Samolot : ILata { public void Lec() { Console.WriteLine("Samolot leci na wysokości 10km ✈️"); } } // Hydroplan – pływa I lata! public class Hydroplan : IPlywa, ILata { public void Plyn() { Console.WriteLine("Hydroplan kołuje po wodzie 🛩️"); } public void Lec() { Console.WriteLine("Hydroplan startuje z wody i leci! 🛩️"); } }
Klasa może jednocześnie dziedziczyć po jednej klasie i implementować wiele interfejsów:
// Klasa bazowa + interfejsy
public class Kaczka : Ptak, IPlywa, ILata, IChodzi
{
// Dziedziczy z Ptak + implementuje 3 interfejsy
}
Kolejność: najpierw klasa bazowa, potem interfejsy (oddzielone przecinkami).
Interfejs jako typ zmiennej (polimorfizm)
Interfejs może być typem zmiennej, parametru lub kolekcji. Pozwala to traktować różne obiekty jednakowo, jeśli implementują ten sam interfejs.
// Zmienna typu interfejsowego IPlywa cos = new Kaczka(); // Kaczka implementuje IPlywa cos.Plyn(); // ✅ OK – metoda z interfejsu // cos.Kwacz(); // ❌ BŁĄD – Kwacz() nie jest w IPlywa // Możemy przypisać INNY obiekt implementujący IPlywa cos = new Lodka(); // Łódka też implementuje IPlywa cos.Plyn(); // ✅ OK – inna implementacja cos = new Ryba(); // Ryba też! cos.Plyn(); // ✅ OK – jeszcze inna implementacja
Tablica/lista obiektów implementujących interfejs
// Lista WSZYSTKIEGO co pływa! List<IPlywa> plywajace = new List<IPlywa>(); plywajace.Add(new Kaczka { Imie = "Donald" }); plywajace.Add(new Ryba()); plywajace.Add(new Lodka { MocSilnika = 100 }); plywajace.Add(new Hydroplan()); // Każdy pływa PO SWOJEMU! foreach (IPlywa obiekt in plywajace) { obiekt.Plyn(); } // Wyświetli: // Donald płynie po stawie 🦆 // Ryba płynie pod wodą 🐟 // Łódka płynie z mocą 100 KM ⛵ // Hydroplan kołuje po wodzie 🛩️
Interfejs jako parametr metody
// Metoda przyjmuje COKOLWIEK co pływa public void WypuscNaWode(IPlywa obiekt) { Console.WriteLine("Wypuszczam na wodę..."); obiekt.Plyn(); } // Użycie – działa z każdym obiektem implementującym IPlywa WypuscNaWode(new Kaczka()); // ✅ WypuscNaWode(new Lodka()); // ✅ WypuscNaWode(new Hydroplan()); // ✅ // WypuscNaWode(new Samolot()); // ❌ Samolot nie implementuje IPlywa!
Jedna metoda WypuscNaWode() działa z kaczką, łódką, rybą, hydroplanem – z czymkolwiek co pływa. Nie musisz pisać osobnej metody dla każdego typu!
Ewolucja kodu – od problemu do rozwiązania
Zobaczmy pełną transformację – jak interfejsy rozwiązują problem z sekcji 1.
❌ PROBLEM: Bez interfejsów
// Musimy wybierać JEDNĄ klasę bazową public abstract class Zwierze { public string Nazwa { get; set; } } // Kaczka dziedziczy po Zwierze – OK public class Kaczka : Zwierze { // Ale jak dodać pływanie, latanie, chodzenie? // Musimy wszystko wpisać ręcznie, bez żadnego kontraktu public void Plyn() { } // Pisane "z pamięci" public void Lec() { } // Może się pomylić nazwa public void Idz() { } // Brak gwarancji spójności } // Chcemy metodę dla wszystkiego co pływa... ale jak? public void WypuscNaWode(/* jaki typ? */) { // Kaczka pływa, Łódka pływa, Ryba pływa... // Ale nie mają wspólnego typu! }
✅ ROZWIĄZANIE: Z interfejsami
// Interfejsy definiują "umiejętności" public interface IPlywa { void Plyn(); } public interface ILata { void Lec(); } public interface IChodzi { void Idz(); } // Klasa bazowa dla wspólnych cech public abstract class Zwierze { public string Nazwa { get; set; } public abstract void WydajDzwiek(); } // Kaczka: dziedziczy po Zwierze + implementuje 3 interfejsy! public class Kaczka : Zwierze, IPlywa, ILata, IChodzi { public override void WydajDzwiek() => Console.WriteLine("Kwa!"); // Kompilator WYMUSZA implementację wszystkich metod z interfejsów: public void Plyn() => Console.WriteLine($"{Nazwa} płynie"); public void Lec() => Console.WriteLine($"{Nazwa} leci"); public void Idz() => Console.WriteLine($"{Nazwa} idzie"); } // Łódka: NIE jest Zwierzęciem, ale PŁYWA public class Lodka : IPlywa { public void Plyn() => Console.WriteLine("Łódka płynie"); } // Teraz możemy napisać uniwersalną metodę! public void WypuscNaWode(IPlywa obiekt) { obiekt.Plyn(); // Działa z Kaczką, Łódką, Rybą... }
❌ Bez interfejsów
- Tylko jedna klasa bazowa
- Brak wspólnego typu dla „pływających”
- Brak wymuszenia implementacji
- Metody pisane „z pamięci”
- Trudno o polimorfizm
✅ Z interfejsami
- Klasa bazowa + wiele interfejsów
- IPlywa = wspólny typ
- Kompilator wymusza implementację
- Spójne nazwy metod
- Pełny polimorfizm!
Popularne interfejsy w .NET
.NET Framework zawiera wiele gotowych interfejsów, które warto znać. Implementując je, Twoje klasy zyskują nowe możliwości!
| Interfejs | Do czego służy? | Metoda do implementacji |
|---|---|---|
IComparable<T> | Porównywanie, sortowanie | CompareTo(T other) |
IEquatable<T> | Sprawdzanie równości | Equals(T other) |
IDisposable | Zwalnianie zasobów | Dispose() |
IEnumerable<T> | Iterowanie foreach | GetEnumerator() |
ICloneable | Kopiowanie obiektów | Clone() |
Przykład: IComparable – sortowanie
public class Produkt : IComparable<Produkt> { public string Nazwa { get; set; } public decimal Cena { get; set; } // Implementacja IComparable – sortowanie po cenie public int CompareTo(Produkt other) { if (other == null) return 1; return Cena.CompareTo(other.Cena); } public override string ToString() => $"{Nazwa}: {Cena:C}"; } // Użycie: List<Produkt> produkty = new List<Produkt> { new Produkt { Nazwa = "Laptop", Cena = 3500 }, new Produkt { Nazwa = "Mysz", Cena = 50 }, new Produkt { Nazwa = "Monitor", Cena = 1200 } }; produkty.Sort(); // Działa dzięki IComparable! foreach (var p in produkty) Console.WriteLine(p); // Wyświetli (posortowane po cenie): // Mysz: 50,00 zł // Monitor: 1 200,00 zł // Laptop: 3 500,00 zł
Przykład: IDisposable – zwalnianie zasobów
public class PolaczenieBazy : IDisposable { private bool polaczony = false; public void Polacz() { polaczony = true; Console.WriteLine("Połączono z bazą danych"); } public void WykonajZapytanie(string sql) { if (!polaczony) throw new Exception("Brak połączenia!"); Console.WriteLine($"Wykonuję: {sql}"); } // Implementacja IDisposable – sprząta po sobie public void Dispose() { if (polaczony) { polaczony = false; Console.WriteLine("Rozłączono z bazą danych"); } } } // Użycie z using – automatyczne wywołanie Dispose()! using (PolaczenieBazy db = new PolaczenieBazy()) { db.Polacz(); db.WykonajZapytanie("SELECT * FROM Users"); } // ← Tutaj automatycznie wywołane Dispose()! // Wyświetli: // Połączono z bazą danych // Wykonuję: SELECT * FROM Users // Rozłączono z bazą danych ← automatycznie!
Gdy używasz using z obiektem implementującym IDisposable, C# automatycznie wywoła Dispose() na końcu bloku – nawet jeśli wystąpi wyjątek!
Właściwości w interfejsach
Interfejsy mogą deklarować właściwości (properties), nie tylko metody. Klasa implementująca musi je dostarczyć.
public interface IIdentyfikowalny { // Właściwość tylko do odczytu int Id { get; } // Właściwość do odczytu i zapisu string Nazwa { get; set; } } public interface ICenowany { decimal Cena { get; set; } decimal ObliczCeneZVAT(decimal stawkaVAT); } // Klasa implementuje oba interfejsy public class Produkt : IIdentyfikowalny, ICenowany { // Implementacja IIdentyfikowalny public int Id { get; } // Tylko get – bo interfejs tak wymaga public string Nazwa { get; set; } // Implementacja ICenowany public decimal Cena { get; set; } public decimal ObliczCeneZVAT(decimal stawkaVAT) { return Cena * (1 + stawkaVAT); } // Konstruktor ustawia Id (bo jest tylko get) public Produkt(int id) { Id = id; } } // Użycie: Produkt p = new Produkt(1) { Nazwa = "Laptop", Cena = 3000 }; Console.WriteLine($"{p.Nazwa} z VAT: {p.ObliczCeneZVAT(0.23m):C}"); // Laptop z VAT: 3 690,00 zł
Interfejs może mieć właściwości (z get/set), ale nie może mieć pól (zmiennych):
public interface IBledny
{
int Pole; // ❌ BŁĄD – pola niedozwolone!
int Wlasciwosc { get; set; } // ✅ OK – właściwość
}
interface vs abstract class – pełne porównanie
| Cecha | interface | abstract class |
|---|---|---|
| Wielokrotne dziedziczenie | ✅ Wiele interfejsów | ❌ Tylko jedna klasa |
| Implementacja metod | ❌ Tylko deklaracje* | ✅ Może mieć implementację |
| Pola | ❌ Nie | ✅ Tak |
| Konstruktor | ❌ Nie | ✅ Tak |
| Modyfikatory dostępu | ❌ Wszystko public | ✅ Dowolne |
| Relacja | „Umie” (can-do) | „Jest rodzajem” (is-a) |
| Kiedy używać? | Definiujesz umiejętności | Masz wspólny kod |
* Od C# 8.0 interfejsy mogą mieć domyślne implementacje.
Schemat decyzyjny
do współdzielenia?
po wielu źródłach?
Praktyczny przykład – łączenie obu
// Interfejsy definiują UMIEJĘTNOŚCI public interface IPlywa { void Plyn(); } public interface ILata { void Lec(); } public interface IJadalny { int Kalorie(); } // Klasa abstrakcyjna definiuje WSPÓLNY KOD public abstract class Zwierze { public string Nazwa { get; set; } protected int wiek; public void Spij() { Console.WriteLine($"{Nazwa} śpi... Zzz"); } public abstract void WydajDzwiek(); } // Kaczka: JEST zwierzęciem + UMIE pływać, latać, jest jadalna public class Kaczka : Zwierze, IPlywa, ILata, IJadalny { public override void WydajDzwiek() => Console.WriteLine("Kwa!"); public void Plyn() => Console.WriteLine($"{Nazwa} płynie"); public void Lec() => Console.WriteLine($"{Nazwa} leci"); public int Kalorie() => 250; } // Pies: JEST zwierzęciem + UMIE pływać (ale nie lata, nie jest jadalny) public class Pies : Zwierze, IPlywa { public override void WydajDzwiek() => Console.WriteLine("Hau!"); public void Plyn() => Console.WriteLine($"{Nazwa} pływa pieskiem"); }
Często najlepsze rozwiązanie to kombinacja:
- abstract class – dla wspólnego kodu i relacji „jest rodzajem”
- interface – dla umiejętności i „wielokrotnego dziedziczenia”
Kaczka JEST zwierzęciem (dziedziczy) i UMIE pływać, latać (implementuje interfejsy).
Kiedy używać interfejsów?
✅ Używaj interface gdy:
| Sytuacja | Przykład |
|---|---|
| Definiujesz „umiejętność”, nie „rodzaj” | IPlywa – kaczka i łódka pływają, ale to różne rzeczy |
| Potrzebujesz wielokrotnej implementacji | Klasa : IA, IB, IC |
| Nie masz wspólnego kodu | Tylko deklaracje metod |
| Chcesz luźne powiązanie (loose coupling) | Dependency Injection |
| Różne klasy mają wspólne zachowanie | IComparable – sortowanie różnych typów |
❌ NIE używaj interface gdy:
| Sytuacja | Co zamiast? |
|---|---|
| Masz wspólny kod do współdzielenia | abstract class |
| Potrzebujesz pól | abstract class |
| Relacja „jest rodzajem” | Dziedziczenie klas |
| Tylko jedna klasa będzie implementować | Może niepotrzebny? |
✅ Dobre interfejsy
- IPlywa, ILata, IChodzi
- IComparable, IDisposable
- ISerializable, ICloneable
- ILogger, IEmailSender
- IRepository, IService
❌ Złe interfejsy
- IPies (to klasa, nie umiejętność)
- ISamochod (to rzecz, nie zachowanie)
- IData (za ogólne)
- Interfejs z jedną metodą dla jednej klasy
Częste błędy
❌ Błąd 1: Próba utworzenia instancji interfejsu
❌ Źle
IPlywa p = new IPlywa(); // BŁĄD: Cannot create an instance // of the abstract type or // interface 'IPlywa'
✅ Dobrze
IPlywa p = new Kaczka(); // OK! Zmienna typu interfejsowego, // obiekt klasy implementującej
❌ Błąd 2: Użycie override przy interfejsie
❌ Źle
public class Kaczka : IPlywa
{
public override void Plyn()
{ }
}
// BŁĄD: 'Kaczka.Plyn()' is marked
// as override but no suitable
// method found
✅ Dobrze
public class Kaczka : IPlywa
{
public void Plyn()
{ }
}
// OK! Bez override dla interfejsu
❌ Błąd 3: Pola w interfejsie
❌ Źle
public interface IPlywa
{
int predkosc; // POLE
}
// BŁĄD: Interfaces cannot
// contain instance fields
✅ Dobrze
public interface IPlywa
{
int Predkosc { get; } // WŁAŚCIWOŚĆ
}
// OK!
❌ Błąd 4: Modyfikatory dostępu w interfejsie
❌ Źle
public interface IPlywa
{
public void Plyn();
private void PomocniczaMetoda();
}
// Ostrzeżenie/Błąd – modyfikatory
// nie są potrzebne/dozwolone
✅ Dobrze
public interface IPlywa
{
void Plyn(); // Bez modyfikatora
void PomocniczaMetoda();
}
// OK! Wszystko domyślnie public
❌ Błąd 5: Brakująca implementacja
❌ Źle
public interface IPlywa
{
void Plyn();
double PobierzPredkosc();
}
public class Kaczka : IPlywa
{
public void Plyn() { }
// Brak PobierzPredkosc()!
}
// BŁĄD: 'Kaczka' does not
// implement 'IPlywa.PobierzPredkosc'
✅ Dobrze
public class Kaczka : IPlywa
{
public void Plyn() { }
public double PobierzPredkosc()
{
return 5.0;
}
}
// OK! Wszystko zaimplementowane
Podsumowanie
- interface = kontrakt definiujący „co obiekt umie”
- Implementacja = klasa dostarcza kod dla metod interfejsu
- Wielokrotna implementacja = klasa może implementować wiele interfejsów
- Brak instancji = nie można zrobić
new IInterface() - Brak override = przy interfejsach nie używamy override
- Konwencja nazewnictwa = nazwa zaczyna się od I (IComparable)
Porównanie trzech podejść
| Zwykła klasa | abstract class | interface | |
|---|---|---|---|
| Instancja | ✅ new Klasa() | ❌ Nie | ❌ Nie |
| Implementacja | ✅ Tak | ✅ Częściowa | ❌ Nie* |
| Pola | ✅ Tak | ✅ Tak | ❌ Nie |
| Wielokrotne | ❌ 1 bazowa | ❌ 1 bazowa | ✅ Wiele |
| Relacja | is-a | is-a | can-do |
Schemat składni
// Definicja interfejsu public interface INazwaInterfejsu { // Metody (bez ciała) void Metoda(); int MetodaZwracajaca(); // Właściwości string Wlasciwosc { get; set; } } // Implementacja – sama klasa public class Klasa : INazwaInterfejsu { public void Metoda() { /* implementacja */ } public int MetodaZwracajaca() { return 0; } public string Wlasciwosc { get; set; } } // Implementacja – klasa bazowa + interfejsy public class Klasa : KlasaBazowa, IInterfejs1, IInterfejs2 { // Implementacja wszystkich metod ze wszystkich interfejsów }
Zadania praktyczne
📝 Zadanie 1: System płatności
Utwórz interfejs i implementacje:
- IMetodaPlatnosci: bool Zaplac(decimal kwota), string PobierzNazwe()
- KartaKredytowa: NumerKarty, Limit
- PayPal: Email, Saldo
- Blik: NumerTelefonu
- Gotowka: KwotaWPortfelu
Napisz metodę void RealizujZamowienie(IMetodaPlatnosci platnosc, decimal kwota) działającą z dowolną metodą płatności.
💡 Podpowiedź: Zaplac() zwraca true jeśli płatność się powiodła
📝 Zadanie 2: System eksportu danych
Utwórz:
- IEksporter: void Eksportuj(string dane), string PobierzRozszerzenie()
- EksportCSV: eksportuje do .csv
- EksportJSON: eksportuje do .json
- EksportXML: eksportuje do .xml
- EksportPDF: eksportuje do .pdf
Utwórz listę List<IEksporter> i wyeksportuj dane do wszystkich formatów.
💡 Podpowiedź: Każdy eksporter może mieć własny sposób formatowania
📝 Zadanie 3: Urządzenia wielofunkcyjne
Utwórz interfejsy i klasy:
- IDrukuje: void Drukuj(string dokument)
- ISkanuje: string Skanuj()
- IKopiuje: void Kopiuj(int ileKopii)
- IFaksuje: void WyslijFaks(string numer, string dokument)
- Drukarka: tylko drukuje
- Skaner: tylko skanuje
- Ksero: skanuje i drukuje (więc też kopiuje!)
- UrządzenieWielofunkcyjne: wszystko!
💡 Podpowiedź: IKopiuje może wykorzystywać ISkanuje i IDrukuje wewnętrznie
⭐ Zadanie 4: System powiadomień
Utwórz:
- IPowiadomienie: void Wyslij(string odbiorca, string tresc), bool CzyWyslano { get; }
- Email: AdresNadawcy, Temat
- SMS: NumerNadawcy (max 160 znaków!)
- Push: Priorytet (Low/Normal/High)
- Slack: Kanal, MozeBycEmoji
Utwórz klasę MenadzerPowiadomien z metodą void WyslijDoWszystkich(List<IPowiadomienie>, string odbiorca, string tresc).
💡 Podpowiedź: SMS może skracać treść automatycznie
⭐⭐ Zadanie 5: Gra RPG – pełny system
Połącz dziedziczenie, klasy abstrakcyjne i interfejsy:
- abstract Postac: Imie, HP, abstract void Atakuj(Postac cel)
- IBroniSieWalce: int Blokuj(), int PobierzPancerz()
- IUzywaMagii: int Mana { get; }, void RzucCzar(Postac cel, string zaklecie)
- ILeczy: void Lecz(Postac cel, int ilosc)
- IKradnie: bool Okradnij(Postac cel)
- Wojownik : Postac, IBroniSieWalce
- Mag : Postac, IUzywaMagii
- Paladyn : Postac, IBroniSieWalce, IUzywaMagii, ILeczy
- Lotrzyk : Postac, IKradnie
- Bard : Postac, IUzywaMagii, ILeczy
Utwórz symulację walki, gdzie różne postacie używają swoich unikalnych umiejętności.
💡 Podpowiedź: Użyj sprawdzania if (postac is ILeczy leczacy) do dynamicznego wywoływania umiejętności