CodeGym /Kursy /C# SELF /Porównanie interfejsów i klas abstrakcyjnych

Porównanie interfejsów i klas abstrakcyjnych

C# SELF
Poziom 24 , Lekcja 2
Dostępny

1. Krótko o klasycznych różnicach

Jeśli ktoś mówi: „Interfejs to po prostu zbiór sygnatur”, zapytaj: „A na jakiej wersji C# piszesz?” Od C# 8 i nowszych interfejsy stały się dużo potężniejsze. Czas porównać je z klasami abstrakcyjnymi — nie tylko pod kątem klasycznych właściwości, ale też wszystkich nowych bajerów platformy .NET.

Jeśli cofnąć się kilka lat — do C# 7 — wszystko było proste. Klasa abstrakcyjna może definiować pola i częściowo zaimplementowane metody, interfejs — tylko sygnatury (metod, właściwości, zdarzeń, indeksatorów).
Dziedziczenie klasy abstrakcyjnej to relacja "jest" (Is-a), interfejsy realizują wielokrotne dziedziczenie zachowania ("potrafi", can-do).

Charakterystyka Klasa abstrakcyjna Interfejs (do C# 8)
Relacja is-a can-do
Dziedziczenie Tylko jedno Wielokrotne
Pola Może zawierać Nie może
Implementacja metod Może Nie może
Konstruktory Może Nie może
Modyfikatory dostępu Różne (public, protected, ...) Tylko domyślnie public

Jak widać, kiedyś klasy abstrakcyjne były prawdziwymi "starszymi braćmi" interfejsów — mocniejsze i bardziej elastyczne. Ale wszystko się zmienia!

2. Interfejsy z domyślną implementacją

Od C# 8 (a tym bardziej w C# 14 i .NET 9) interfejsy dostały nową supermoc — metody z domyślną implementacją, nazywane "Default Interface Methods" (DIM).

Jak to wygląda?


public interface IAnimal
{
    void SayHello();

    // Metoda z domyślną implementacją!
    void Walk()
    {
        Console.WriteLine("Idę...");
    }
}

Wow! Nagle okazało się, że interfejs może zawierać implementację metody. I to nie jedną, a ile dusza zapragnie. Ale jest haczyk: takie metody muszą być jawnie zadeklarowane z ciałem, a cała reszta (pola, prywatne metody, konstruktory) — nadal nie można.

3. Nowoczesne możliwości interfejsów

Nowe bajery, o których powinien wiedzieć każdy .NET-deweloper:

  • Metody z domyślną implementacją.
  • Prywatne metody w interfejsie (tylko do pomocniczych celów, dostępne tylko dla innych metod tego samego interfejsu).
  • Metody statyczne (od C# 8).
  • Właściwości z domyślną implementacją.
  • Pola statyczne (od C# 14 — "static interface members").
  • Abstrakcyjne statyczne członki ("abstract static members" — tak, teraz interfejs może wymagać od implementacji określonych metod statycznych!).

Przykład pełnego nowoczesnego interfejsu:


public interface ILogger
{
    static int LoggerCount { get; set; } // C# 14

    void Log(string message); // Sygnatura (kontrakt)

    // Domyślna implementacja
    void LogWarning(string warning)
    {
        Log("[WARNING]: " + warning);
    }
    
    // Prywatna metoda pomocnicza w interfejsie (C# 8+)
    private void FormatAndLog(string level, string msg)
    {
        Log($"{level}: {msg}");
    }

    // Metoda statyczna w interfejsie (C# 8+)
    static void PrintLoggerInfo()
    {
        Console.WriteLine("Interfejs ILogger — twój najlepszy pomocnik!");
    }
}

Wyobraź sobie — kiedyś to było po prostu niemożliwe, to jakby ktoś pozwolił kotu pracować jako ochroniarz serwera.

4. Klasy abstrakcyjne: co nowego?

Klasy abstrakcyjne... jakby to powiedzieć... nie bardzo ewoluowały przez ostatnią dekadę. Nadal mogą zawierać:

  • Pola (w tym prywatne, chronione i statyczne).
  • Metody zaimplementowane i abstrakcyjne.
  • Konstruktory (tak, można tworzyć klasy abstrakcyjne z logiką inicjalizacji).
  • Właściwości, zdarzenia, indeksatory.
  • Członkowie statyczni i instancyjni.

Przykład klasy abstrakcyjnej:


public abstract class Animal
{
    public string Name { get; set; }

    public abstract void Speak();

    public virtual void Walk()
    {
        Console.WriteLine($"{Name} idzie na łapkach!");
    }

    protected void Eat()
    {
        Console.WriteLine($"{Name} je karmę.");
    }
}

Klasa abstrakcyjna nadal jest świetnym miejscem na wspólną logikę, stan i zachowanie dla hierarchii klas.

5. Nowoczesne porównanie: tabela z nowymi możliwościami

Charakterystyka Klasa abstrakcyjna Interfejs (C# 14+, .NET 9)
Relacja is-a (jest) can-do (potrafi)
Dziedziczenie Tylko jedno Wielokrotne
Pola Tak, dowolne Tylko statyczne* (C# 14+)
Konstruktory Tak Nie
Implementacja metod Tak (virtual/abstract) Tak (default, static, abstract static)
Właściwości z implementacją Tak Tak (default implementation)
Prywatni członkowie Tak Tak (tylko metody, C# 8+)
Członkowie statyczni Tak Tak (C# 8+, z ograniczeniami)
Pola statyczne Tak Tak * (C# 14+)
Modyfikatory dostępu Dowolne Domyślnie public lub private

* — W interfejsach pola statyczne używane są raczej w wyjątkowych przypadkach i to bardzo świeża funkcja języka.

6. Gdzie użyć interfejsu, a gdzie klasy abstrakcyjnej: nowoczesne rekomendacje

Interfejsy (a teraz także z domyślną implementacją) — narzędzie do tworzenia kontraktów między komponentami. Ich kluczowa cecha: wielokrotność. Twoja klasa może implementować nawet dziesięć różnych interfejsów, co czyni ją uniwersalnym żołnierzem.

Klasa abstrakcyjna to nadal twój wybór, jeśli:

  • Potrzebny jest wspólny stan (pola), logika i zachowanie, które dziedziczą inne klasy.
  • Potrzebna jest standardowa, ale nadpisywalna logika (użyj virtual).
  • Chcesz scentralizować inicjalizację przez konstruktor.

W prawdziwych projektach często spotyka się taki schemat: "czyste kontrakty" formułuje się w interfejsach, a jeśli potrzebny jest wspólny kod lub infrastruktura dla podklas — tworzy się abstrakcyjną klasę bazową.


.                     ┌────────────────────────┐
                      │      Interfejs         │
                      │  (kontrakt: co potrafi)│
                      └─────────┬──────────────┘
                                │
                 ┌──────────────┼──────────────┐
                 │              │              │
           Implementacja 1  Implementacja 2 ... Implementacja N
             MyLogger     CloudLogger     FileLogger
    
        (można łączyć z dziedziczeniem klasy abstrakcyjnej)
Schemat: gdzie i co stosować

7. Scenariusze — kiedy co wygrywa

Wielokrotna implementacja:
Załóżmy, że masz interfejs IDrivable i klasę abstrakcyjną Vehicle. Teraz klasa Car może dziedziczyć bazę — Vehicle — i jednocześnie implementować kilka interfejsów (IDrivable, IRepairable, IInsurable). Gdybyś miał klasę abstrakcyjną Repairable, musiałbyś wybierać — albo Vehicle, albo Repairable! Interfejsy tutaj wygrywają.

Wspólna logika i stan:
Załóżmy, że cały "autotransport" ma pole "numer". To powinno być pole klasy abstrakcyjnej. W interfejsie pola (poza statycznymi) nie można.

Ewolucja API:
Jedna z rewolucyjnych historii z Default Interface Methods — teraz interfejsy można rozwijać bez ryzyka popsucia istniejących użytkowników.
Na przykład, dodano do interfejsu nową metodę z domyślną implementacją — wszystko działa, wszystkie stare implementacje interfejsu nie padły! Kiedyś to bolało (albo, jeśli zapomnieć o bólu, i tak było niemożliwe).

8. Przykłady w praktyce

W naszej aplikacji edukacyjnej stopniowo pojawia się logowanie. Stwórzmy własny interfejs ILogger z domyślną implementacją:


public interface ILogger
{
    void Log(string message);

    // Domyślna implementacja dostępna dla wszystkich implementacji interfejsu!
    void LogInfo(string info)
    {
        Log("[INFO] " + info);
    }

    // Statyczna metoda interfejsu
    static void PrintHelp()
    {
        Console.WriteLine("Użyj ILogger do logowania zdarzeń");
    }
}

public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine(message);
    }
}

// Gdzieś w kodzie:
ILogger logger = new ConsoleLogger();
logger.LogInfo("System uruchomiony!"); // działa dzięki domyślnej implementacji

// Wywołanie statycznej metody interfejsu
ILogger.PrintHelp();

Gdybyśmy dodali nową metodę z domyślną implementacją do interfejsu, wszystkie istniejące implementacje (np. ConsoleLogger) automatycznie dostałyby tę nową metodę — żadnej paniki i psucia kodu.

9. Błędy i niuanse: praktyka i pułapki

Warto wiedzieć, że nie wszystko jest tak kolorowe, jak się wydaje. Na przykład, jeśli twój interfejs zawiera domyślną implementację, ale użytkownik odwołuje się do obiektu przez typ klasy, a nie przez typ interfejsu, domyślna implementacja jest dostępna tylko przez interfejs.


ConsoleLogger log = new ConsoleLogger();
log.LogInfo("Hello"); // Nie skompiluje się: LogInfo nie jest zdefiniowane w klasie!

ILogger log2 = log;
log2.LogInfo("Hello"); // Wszystko OK!

To trochę jak szczególny przypadek jawnej implementacji interfejsu. Czasem takie zachowanie jest wygodne do ukrycia "zbędnego" API, czasem — zaskakujące dla początkujących.

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