CodeGym /Kursy /C# SELF /Pojęcie interfejsu i jego składnia

Pojęcie interfejsu i jego składnia

C# SELF
Poziom 23 , Lekcja 0
Dostępny

1. Czym jest interfejs?

Jeśli klasa abstrakcyjna to taki szablonowy "półprodukt" z częściową implementacją, to interfejs to po prostu lista wymagań: co musi umieć dana istota (klasa), żeby można było jej użyć w jakimś abstrakcyjnym kontekście.

W programowaniu interfejs to taki "kontrakt" albo "lista wymagań" dotyczących zachowania obiektu. Opisuje zestaw publicznych metod, właściwości, indeksatorów i zdarzeń, które klasa implementująca ten interfejs musi udostępnić. Interfejs mówi: "Jeśli chcesz się nazywać np. IDriveable (czyli być zdolnym do jazdy), to bądź łaskaw udostępnić metody Drive() i Stop()".

Możesz wyobrazić sobie interfejs jako Listę Wymagań dla Pracownika: na przykład, jeśli chcesz zatrudnić "Kucharzy", to w interfejsie będzie napisane: "Musi umieć gotować, podawać dania, myć ręce". Jak konkretny kucharz to zrobi — to już jego sprawa. Najważniejsze, żeby na zewnątrz działał zgodnie z kontraktem.

Ważne: interfejs opisuje tylko co klasa powinna udostępniać, a nie jak to robi.

Krótko w terminach OOP

  • Interfejs opisuje zewnętrzny "wygląd" (API) obiektu: jakie akcje pozwala wykonać.
  • Nie zawiera stanu (żadnych pól). W klasycznym rozumieniu interfejsy też nie zawierają implementacji, ale w nowoczesnych wersjach C# są wyjątki (np. metody domyślne), o których pogadamy później.
  • Jedna klasa może implementować dowolną liczbę interfejsów (w przeciwieństwie do dziedziczenia klas!).

Analogicznie z życia

Port USB — każdy wie, że można podłączyć myszkę, klawiaturę, pendrive, słuchawki, a nawet ekspres do kawy (serio, są takie!). Co jest w środku "myszki" — nieważne, byle był port USB i urządzenie obsługiwało odpowiedni protokół. To właśnie jest interfejs!

Po co są interfejsy?

  • Zmniejszają powiązania. Kod działa z interfejsem, a nie z konkretną klasą. Programujemy "na poziomie interfejsu".
  • Pozwalają na wielokrotne dziedziczenie zachowania: klasa może implementować kilka interfejsów.
  • Standaryzacja: można zrobić uniwersalne mechanizmy: np. wszystkie obiekty, które można porównywać, implementują interfejs IComparable.
  • Testowalność: łatwo podmienić konkretną implementację na "mocka" podczas testów.
  • Modułowość: można dodawać nowe "moduły" bez zmian w istniejącym kodzie.

2. Składnia deklaracji interfejsu

A teraz — do kodu! W C# interfejs deklaruje się za pomocą słowa kluczowego interface, a nazwy interfejsów zwykle zaczynają się od I :


// Deklaracja interfejsu
public interface IPrintable
{
    // Abstrakcyjna metoda — kontrakt
    void Print();

    // Można też deklarować właściwości
    string Name { get; set; }
}

Zapamiętaj: metody i właściwości interfejsu NIE mają implementacji (nie ma ciała metody, tylko sygnatura — dokładnie jak w metodach abstrakcyjnych).

Kluczowe cechy interfejsu

  • Nie można deklarować pól (zmiennych członkowskich) w interfejsie.
  • Wszystkie metody, właściwości, zdarzenia i indeksatory domyślnie są public (i takie muszą być w implementacji).
  • Interfejs nie może mieć konstruktorów (bo nie ma stanu).
  • Interfejs nie zależy od tego, czy klasa go implementująca jest abstrakcyjna czy konkretna.

Interfejs w akcji

Zintegrujmy interfejs w naszej aplikacji edukacyjnej. Załóżmy, że mamy interfejs "drukowalne" (IPrintable), który implementuje klasa Report i np. nowa klasa Invoice.

public interface IPrintable
{
    void Print();
    string Name { get; set; }
}

Teraz zdefiniujmy klasę implementującą ten interfejs:

public class Report : IPrintable
{
    public string Name { get; set; }

    public Report(string name)
    {
        Name = name;
    }

    // Implementacja metody z interfejsu
    public void Print()
    {
        Console.WriteLine($"Drukowanie raportu: {Name}");
    }
}

A teraz — zupełnie inna klasa, ale z tym samym interfejsem:

public class Invoice : IPrintable
{
    public string Name { get; set; }

    public Invoice(string name)
    {
        Name = name;
    }

    public void Print()
    {
        Console.WriteLine($"Drukowanie faktury: {Name}");
    }
}

Teraz możemy napisać metodę, która działa z dowolnym drukowalnym obiektem:

public static void PrintAnything(IPrintable printable)
{
    printable.Print(); // I tyle! Nieważne, czy to raport czy faktura — ważne, że umie drukować.
}

A oto przykład użycia:

var report = new Report("Raport miesięczny");
var invoice = new Invoice("Faktura #12345");

PrintAnything(report);  // Drukowanie raportu: Raport miesięczny
PrintAnything(invoice); // Drukowanie faktury: Faktura #12345

Tak właśnie interfejsy pozwalają pisać uniwersalny, rozszerzalny i ładny kod.

3. Implementacja interfejsów

Podłączanie interfejsu do klasy

Interfejs implementuje się w klasie za pomocą dwukropka (tak, jak przy dziedziczeniu):

public class Ticket : IPrintable
{
    public string Name { get; set; }
    public void Print()
    {
        Console.WriteLine($"Drukowanie biletu: {Name}");
    }
}

Ważne: klasa musi zaimplementować wszystkie człony interfejsu. Implementowane człony muszą być public.

Jeśli nie zaimplementujesz choćby jednego członu:

public class BrokenTicket : IPrintable
{
    // Brak implementacji Print()
    public string Name { get; set; }
}
// Błąd kompilacji: 'BrokenTicket' nie implementuje członu interfejsu 'IPrintable.Print()'

Kilka interfejsów

Klasa może implementować kilka interfejsów, oddzielając je przecinkiem:

public interface IStorable
{
    void Store();
}

public class MultiPurposeDoc : IPrintable, IStorable
{
    public string Name { get; set; }
    public void Print()
    {
        Console.WriteLine("Drukowanie dokumentu");
    }

    public void Store()
    {
        Console.WriteLine("Zapisywanie dokumentu");
    }
}

4. Po co są interfejsy?

Może teraz masz w głowie myśl: "No dobra, składnia jasna. Ale po co mi to wszystko w prawdziwym życiu, poza tym, że utrudnia mi życie na tym kursie?" Spieszę wyprowadzić Cię z błędu! Interfejsy to jedno z najczęściej używanych narzędzi w profesjonalnym programowaniu.

Podział odpowiedzialności i luźne powiązania (Decoupling / Loose Coupling):

  • Wyobraź sobie, że tworzysz odtwarzacz muzyki. Nie obchodzi go, skąd bierze muzykę — z pliku lokalnego, z internetu czy z płyty CD. Ważne, żeby źródło muzyki mogło dostarczyć strumień audio.
  • Możesz zadeklarować interfejs IAudioSource z metodą GetAudioStream().
  • Wtedy masz klasy FileAudioSource, InternetAudioSource, CDAudioSource, które implementują ten interfejs.
  • Twój odtwarzacz będzie działał z IAudioSource, nie znając konkretnego typu. Jeśli jutro pojawi się nowe źródło, np. BluetoothAudioSource, nie musisz zmieniać kodu odtwarzacza! Po prostu stwórz nową klasę implementującą IAudioSource. To sprawia, że Twój system jest dużo bardziej elastyczny i łatwy do rozbudowy. To właśnie luźne powiązania – komponenty zależą od abstrakcji (interfejsów), a nie od konkretnych implementacji.

Polimorfizm i jednolite przetwarzanie:

Jak widzieliśmy w przykładzie z PrintAnything, możesz mieć zestaw obiektów różnych typów, ale połączonych wspólnym zachowaniem opisanym w interfejsie. Możesz wywołać tę samą metodę (Print()) na wszystkich takich obiektach, nie wiedząc, co to dokładnie jest — raport, faktura czy bilet. To pozwala pisać bardzo zwięzły i uniwersalny kod.

Testowanie (Unit Testing):

To chyba jedno z najważniejszych zastosowań interfejsów. Gdy testujesz jakiś komponent swojego systemu, często potrzebuje on innych komponentów do działania (np. klasa, która zapisuje dane, może zależeć od klasy obsługującej bazę danych).

Zamiast przekazywać prawdziwą klasę DatabaseSaver (która wymaga prawdziwej bazy danych do testów!), możesz przekazać "podrobiony" (albo "mockowany") obiekt, który po prostu implementuje interfejs IDataSaver. Ten "mock" będzie tylko udawał zapis, nie łącząc się z prawdziwą bazą. Dzięki temu możesz testować komponenty w izolacji, szybko i bez zewnętrznych zależności.

Tworzenie API i frameworków:

Gdy tworzysz bibliotekę lub framework, chcesz dać programistom "punkty rozszerzenia". Interfejsy nadają się do tego idealnie. Możesz powiedzieć: "Jeśli chcesz, żeby Twój komponent działał z moim systemem, zaimplementuj ten interfejs". Standardowe biblioteki .NET są pełne interfejsów (np. IEnumerable<T>, IDisposable, IComparable<T>) – one definiują kontrakty dla najczęstszych scenariuszy.

Programowanie bezpośrednio na poziomie interfejsów (Programming to an Interface):

Doświadczeni programiści często mówią: "Programuj na poziomie interfejsów, nie implementacji". To znaczy, że gdy deklarujesz typ zmiennej lub parametr metody, zamiast konkretnej klasy (Car) lepiej użyć interfejsu (IDriveable). To sprawia, że Twój kod jest bardziej elastyczny i mniej zależny od szczegółów implementacji, pozwalając łatwo podmienić jedną implementację na inną.

5. Typowe błędy przy pracy z interfejsami

Błąd nr 1: próba utworzenia instancji interfejsu.
Możesz napisać Cat murzik = new Cat("Murzik", 3);, bo Cat to konkretna klasa. Ale nie możesz napisać ITalkable talker = new ITalkable();. Interfejs to tylko kontrakt, szablon. Nie zawiera implementacji i nie można go utworzyć bezpośrednio. To jak projekt, a nie gotowy dom.

Błąd nr 2: zapomniana implementacja wszystkich członów interfejsu.
Jeśli zadeklarowałeś, że Twoja klasa implementuje interfejs, np. IMyInterface, to musi zaimplementować wszystkie jego metody. Nawet jeden pominięty wywoła błąd kompilacji: MyClass nie implementuje IMyInterface.TheMissingMethod().

Błąd nr 3: złe modyfikatory dostępu przy implementacji.
Metody interfejsu są domyślnie public i w implementacji też muszą być public. Jeśli spróbujesz zrobić metodę private lub protected, kompilator wywali błąd. Obiecałeś — implementuj publicznie.

Błąd nr 4: próba dodania pól lub konstruktorów do interfejsu.
Interfejsy opisują zachowanie, nie stan. Dlatego nie można dodawać do nich pól ani konstruktorów. Jeśli spróbujesz — dostaniesz błąd kompilacji. Dozwolone są tylko właściwości, i to tylko jako opis getterów/setterów.

Błąd nr 5: mylenie override z implementacją interfejsu.
Słowo kluczowe override służy do nadpisywania metod klasy bazowej. Ale przy implementacji interfejsu nie jest potrzebne — po prostu piszesz public-metodę z odpowiednią sygnaturą. To ważny niuans, który łatwo przeoczyć.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION