CodeGym /Kursy /C# SELF /Metody i właściwości abstrakcyjne

Metody i właściwości abstrakcyjne

C# SELF
Poziom 22 , Lekcja 2
Dostępny

1. Metody abstrakcyjne

W poprzednim wykładzie położyliśmy fundament: poznaliśmy klasy abstrakcyjne i zobaczyliśmy, że mogą one zawierać metody i właściwości abstrakcyjne. Zrozumieliśmy, że to "kontrakt", który zobowiązuje klasy potomne do dostarczenia własnej implementacji.

Teraz zagłębmy się w szczegóły i zobaczmy bardziej złożone i praktyczne scenariusze. W tym wykładzie skupimy się na niuansach składni, na tym, jak abstrakcja działa w wielopoziomowych hierarchiach i jasno rozgraniczymy, kiedy używać abstract, a kiedy — virtual.

Metoda abstrakcyjna jest jak tajemniczy przepis ze starej książki kucharskiej: "dodaj sekretny składnik" — jaki, nie powiedziano, ale wszyscy kolejni kucharze muszą coś wymyślić!

Metoda abstrakcyjna zawsze znajduje się w klasie abstrakcyjnej

Próba zadeklarowania metody abstrakcyjnej w zwykłej (nieabstrakcyjnej) klasie skończy się błędem kompilatora. Dlaczego? Bo zwykłą klasę można tworzyć bezpośrednio, a co dostaniemy przy próbie wywołania nieopisanej metody? Tylko zagadkowe spojrzenie IDE.


// To wywoła błąd kompilacji:
public class WrongClass
{
    public abstract void Oops(); // Tak nie można!
}

Jeśli klasa zawiera choć jedną metodę abstrakcyjną, musi być oznaczona jako abstract!

Implementacja metod abstrakcyjnych w klasach pochodnych

Implementacja odbywa się przez słowo kluczowe override. Klasa, która dziedziczy po klasie abstrakcyjnej i nie implementuje wszystkich metod abstrakcyjnych, też musi być abstrakcyjna (inaczej kompilator się obrazi).

Kontynuujmy pomysł naszej aplikacji:

W poprzednich wykładach stworzyliśmy klasę Shape (figura), w której zadeklarowaliśmy metodę abstrakcyjną do obliczania pola. Teraz stworzymy konkretne figury:


public class Rectangle : Shape
{
    public double Width { get; }
    public double Height { get; }

    public Rectangle(double width, double height)
    {
        Width = width;
        Height = height;
    }

    public override double CalculateArea()
    {
        return Width * Height;
    }
}

public class Circle : Shape
{
    public double Radius { get; }

    public Circle(double radius)
    {
        Radius = radius;
    }

    public override double CalculateArea()
    {
        return Math.PI * Radius * Radius;
    }
}

Dlaczego metody abstrakcyjne są takie ważne?

Metody abstrakcyjne — to twój sposób na powiedzenie: "Słuchajcie, jeśli dziedziczycie po tej klasie, zdecydujcie sami, jak to ma działać!" To sprawia, że architektura programu jest bardziej przejrzysta, chroni przed przypadkowymi pominięciami i, co najważniejsze, pozwala w pełni wykorzystać polimorfizm.

2. Właściwości abstrakcyjne (properties)

Czym jest właściwość abstrakcyjna

Właściwość abstrakcyjna — to taki sam kontrakt jak metoda abstrakcyjna, tylko dla właściwości (property). Dla C# to bardzo naturalne, bo właściwości to jeden z głównych sposobów enkapsulacji danych. Właściwość abstrakcyjna mówi: "Pozwól potomkom samym zdecydować, skąd brać (i ewentualnie jak ustawiać) wartość tej właściwości".

Przykład:
Dodajmy właściwość Name dla figury — powinna być u każdego potomka, ale niech każdy sam zdecyduje, co tam zwrócić.


public abstract class Shape
{
    public abstract string Name { get; }

    public abstract double CalculateArea();
}

Teraz każdy potomek musi zaimplementować tę właściwość:


public class Rectangle : Shape
{
    public double Width { get; }
    public double Height { get; }
    public override string Name => "Prostokąt";

    public Rectangle(double width, double height)
    {
        Width = width;
        Height = height;
    }

    public override double CalculateArea()
    {
        return Width * Height;
    }
}

public class Circle : Shape
{
    public double Radius { get; }
    public override string Name => "Koło";

    public Circle(double radius)
    {
        Radius = radius;
    }

    public override double CalculateArea()
    {
        return Math.PI * Radius * Radius;
    }
}

Specyfika składni

Właściwość abstrakcyjna jest podobna do interfejsowej: deklaruje się ją bez ciała-implementacji, ale z określeniem, czy będzie miała tylko getter, czy także setter.


public abstract class Creature
{
    // właściwość tylko do odczytu
    public abstract string Species { get; }

    // właściwość do odczytu i zapisu
    public abstract int Age { get; set; }
}

W klasie implementującej można określić logikę pobierania (i w razie potrzeby — zmiany) wartości.

Po co są właściwości abstrakcyjne

Właściwość abstrakcyjna — świetne narzędzie, gdy chcesz, żeby każdy potomek sam decydował, jak pobierać lub przechowywać wartość. To często potrzebne w modelowaniu, pracy z modelami biznesowymi, view-modelami i wszędzie tam, gdzie logika właściwości jest bardziej złożona niż tylko zwrot pola.

Na przykład, jeśli jedna figura wylicza nazwę według wzoru, a inna zwraca stałą, wszystko to można ładnie "schować" za właściwością abstrakcyjną.

3. Schemat: jak powiązane są klasa abstrakcyjna, metody i właściwości

Zobaczmy na poniższy schemat, żeby zobaczyć, jak to działa:


┌─────────────┐
│ abstract    │
│   Shape     │
│-------------│
│ +Name: str  │  <-- właściwość abstrakcyjna
│ +Area(): dbl│  <-- metoda abstrakcyjna
└─────┬───────┘
      │
 ┌────▼────┐      ┌───────┐
 │Rectangle│      │ Circle│
 │........ │ .... │ ......│
 │+Name    │      │+Name  │
 │+Area()  │      │+Area()│
 └─────────┘      └───────┘

Tak zapewniamy JEDEN interfejs pracy z dowolnym potomkiem klasy Shape, ale każdy potomek może robić "pod maską" co chce.

UML-diagram dla metody i właściwości abstrakcyjnej:


┌────────────────────────────┐
│        abstract class      │
│           Animal           │
│────────────────────────────│
│+ Name: string {abstract}   │
│+ MakeSound(): void {abstract}│
└─────────────┬──────────────┘
              │
     ┌────────┴──────────┐
     │                   │
┌────────────┐     ┌─────────────┐
│    Cat     │     │    Dog      │
│────────────│     │─────────────│
│+ Name      │     │+ Name       │
│+ MakeSound()│    │+ MakeSound()│
└────────────┘     └─────────────┘

4. Praktyczne scenariusze i korzyści dla prawdziwych projektów

Metody i właściwości abstrakcyjne — to twoje narzędzie, jeśli projektujesz hierarchie klas, które mają się rozwijać. To fundament dla dużych aplikacji biznesowych, wielopoziomowych modeli dziedzinowych, systemów pluginów, frameworków UI, gdzie dla podstaw systemu ustala się "prawo" obowiązkowe dla wszystkich potomków.

W prawdziwych projektach takie "czyste" rozwiązania architektoniczne pozwalają po miesiącach i latach śmiało wprowadzać nowe byty i funkcje, wiedząc, że stary kod już uwzględnia wszystkie warianty zachowania.

Pytanie "po co" i powrót do polimorfizmu

Polimorfizm wydaje się magią, ale tak naprawdę to po prostu kontrakt: "możesz wywołać pewną metodę na dowolnym obiekcie rodziny i zawsze dostaniesz odpowiedni wynik". Metody i właściwości abstrakcyjne — sposób, by cała hierarchia mówiła wspólnym językiem, ale bez sztywnej implementacji "domyślnej".

Takie podejście jest szeroko stosowane w systemach pluginów (IDE, edytory graficzne, systemy CRM), gdy zewnętrzni deweloperzy tworzą własne rozszerzenia, ale muszą zaimplementować minimalny zestaw funkcji wymaganych przez platformę. Na przykład, jeśli piszesz moduł do obsługi plików, klasa bazowa może zawierać właściwość abstrakcyjną FileExtension i metodę abstrakcyjną Open(), żeby każdy plugin mógł poprawnie obsługiwać pliki swojego typu.

Różnice między abstrakcyjnymi, wirtualnymi i zwykłymi członkami klasy

Charakterystyka Zwykła metoda/właściwość virtual abstract
Obecność implementacji Jest Jest Nie ma
Czy override jest obowiązkowe Nie Nie (można nadpisać) Tak (obowiązkowo w potomku)
Czy można wywołać bezpośrednio Tak Tak Nie
Czy może być w zwykłej klasie Tak Tak Nie
Czy może być oznaczony sealed Nie Tak Nie

5. Zastosowanie w naszej aplikacji edukacyjnej

Teraz, uzbrojeni w nową wiedzę, wróćmy do naszej aplikacji z figurami i zobaczmy, jak właściwości i metody abstrakcyjne współpracują, by stworzyć elastyczny i czytelny system.


// Na przykład w głównej metodzie programu
List<Shape> shapes = new List<Shape>
{
    new Rectangle(4, 5),
    new Circle(2.5)
};

foreach (Shape shape in shapes)
{
    // Dzięki abstrakcyjnej właściwości Name i metodzie CalculateArea,
    // nie przejmujemy się konkretnym typem figury
    Console.WriteLine($"{shape.Name}, Pole: {shape.CalculateArea():F2}");
}

Wynik:

Prostokąt, Pole: 20.00
Koło, Pole: 19.63

Piękno: kod nie wie i nie obchodzi go, czym dokładnie jest figura — po prostu wywołuje potrzebne właściwości i metody, a .NET CLR zadba o poprawną implementację!

6. Częste błędy i specyfika implementacji

Błąd nr 1: niezaimplementowana metoda abstrakcyjna rodzica.
Jeśli w klasie pochodnej choć jedna metoda lub właściwość abstrakcyjna pozostanie bez implementacji, a sama klasa nie jest oznaczona jako abstract, kompilator nie pozwoli zbudować projektu. To przydatna ochrona: nie możesz stworzyć obiektu z "dziurawą" logiką — na przykład figury bez metody CalculateArea().

Błąd nr 2: metoda zadeklarowana jako abstract, a klasa — nie.
Taki kod też się nie kompiluje. Jeśli dodałeś do klasy metodę abstract, sama klasa musi być abstrakcyjna:


public abstract class Polygon : Shape
{
    // Nie implementujemy CalculateArea(), klasa zostaje abstract
}

Błąd nr 3: próba implementacji metody abstrakcyjnej przez virtual.
Metoda abstrakcyjna musi być zaimplementowana ze słowem kluczowym override, a nie virtual. Jednak jeśli chcesz, by implementację można było dalej nadpisywać w hierarchii, możesz najpierw nadpisać metodę, a potem oznaczyć ją jako virtual w klasie potomnej:


public override double CalculateArea()
{
    // Implementacja domyślna...
}

W dalszym potomku taką metodę można już nadpisać ponownie. Ale zrobić ją znowu abstract — nie można, bo implementacja już istnieje.

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