JavaRush /Курси /C# SELF /Абстракції у побудові ієрархій

Абстракції у побудові ієрархій

C# SELF
Рівень 22 , Лекція 3
Відкрита

1. Вступ

Погляньмо на абстракцію з висоти пташиного польоту: уявіть великий офіс, де різні співробітники виконують різні завдання. У них може бути спільна посада — «Співробітник», а в кожного — своє спеціальне завдання. Якщо ви, як керівник, хочете, щоб кожен співробітник міг «ВиконатиРоботу», то не так важливо, як саме це робиться: програміст пише код, бухгалтер готує звіти. Ця ідея — «спільний контракт для всіх через абстракцію, а деталі — для спеціалістів» — лежить в основі грамотного проєктування ієрархій класів.

Як це виглядає в коді?

Усе починається з базового абстрактного класу. Він описує найголовніше, що мають уміти усі його нащадки.


public abstract class Employee
{
    public string Name { get; }
    public Employee(string name)
    {
        Name = name;
    }

    // Абстрактний метод — контракт для всіх співробітників
    public abstract void DoWork();
}

Далі — похідні класи (нащадки): програміст і бухгалтер.


public class Programmer : Employee
{
    public Programmer(string name) : base(name) { }
    public override void DoWork()
    {
        Console.WriteLine($"{Name} пише код");
    }
}

public class Accountant : Employee
{
    public Accountant(string name) : base(name) { }
    public override void DoWork()
    {
        Console.WriteLine($"{Name} рахує гроші");
    }
}

Тепер зверніть увагу: ми можемо створити список співробітників — бухгалтерів, програмістів, кого завгодно (абстракція саме для цього й існує!).


Employee[] office = new Employee[]
{
    new Programmer("Вася"),
    new Accountant("Таня")
};

foreach (Employee emp in office)
{
    emp.DoWork(); // Кожен робить свою роботу
}

Вася пише код
Таня рахує гроші

Зверніть увагу, наскільки все стало охайним, універсальним і, головне, масштабованим. Можна додати новий клас, наприклад, Manager, і не змінювати вже наявний код.

2. Як будувати ієрархії на основі абстракцій

Оскільки майже кожен підручник з ООП не обходиться без прикладу з тваринами, підтримаймо і ми цю традицію.

Абстрактний клас — у ролі «Великого Диктатора»


public abstract class Animal
{
    public string Name { get; set; }
    public Animal(string name)
    {
        Name = name;
    }

    // Усі тварини мають уміти видавати звук
    public abstract void MakeSound();

    // Але не кожна зобов’язана уміти літати, тому:
    public virtual void Fly()
    {
        Console.WriteLine("Я не можу літати.");
    }
}

Похідні класи: зобов’язуються відтворювати звук; можуть перевизначити й інші методи


public class Cat : Animal
{
    public Cat(string name) : base(name) { }
    public override void MakeSound()
    {
        Console.WriteLine($"{Name}: Мяу!");
    }
}

public class Dog : Animal
{
    public Dog(string name) : base(name) { }
    public override void MakeSound()
    {
        Console.WriteLine($"{Name}: Гав!");
    }
}

public class Eagle : Animal
{
    public Eagle(string name) : base(name) { }
    public override void MakeSound()
    {
        Console.WriteLine($"{Name}: Крик!");
    }

    public override void Fly()
    {
        Console.WriteLine($"{Name} літає в небі!");
    }
}

А тепер спробуймо зібрати колекцію з різних тварин:


Animal[] zoo = new Animal[]
{
    new Cat("Барсик"),
    new Dog("Шарик"),
    new Eagle("Орел")
};

foreach (Animal animal in zoo)
{
    animal.MakeSound();
    animal.Fly();
}

Барсик: Мяу!
Я не можу літати.
Шарик: Гав!
Я не можу літати.
Орел: Крик!
Орел літає в небі!

Зверніть увагу, наскільки просто тепер використовувати будь-які класи-нащадки: ми не замислюємося, який саме об’єкт у колекції. Абстракція забезпечує «контракт», а деталі реалізації визначають конкретну поведінку.

3. Абстракція на кількох рівнях

Абстракція — це не лише поділ на базовий клас і його прямих нащадків. У складних системах вона часто реалізується у кілька рівнів, коли один абстрактний клас будується на основі іншого. Це схоже на багатошаровий пиріг — або на цибулю, як сказав би Шрек: кожен шар ховає зайві деталі, залишаючи тільки потрібне.

Приклад: ієрархія транспортних засобів


.              Vehicle (abstract)
            /           |           \
       Car           Airplane      Boat
  ElectricCar    JetAirplane    SailBoat

Тут Vehicle визначає загальні принципи (наприклад, метод Move()), але не знає, як саме рухатиметься автомобіль чи літак. Це вирішують похідні класи. Більш конкретні класи, такі як ElectricCar чи JetAirplane, можуть додавати власні деталі, розширюючи можливості.

Блок-схема ієрархії:


.       +------------------+
        |  Vehicle         |  <--- абстрактний клас
        +------------------+
        /        \
   +-------+   +-------+
   |  Car  |   | Boat  |  <--- проміжні абстракції або конкретні класи
   +-------+   +-------+

Код: базовий абстрактний клас


public abstract class Vehicle
{
    public string Model { get; }
    public Vehicle(string model)
    {
        Model = model;
    }

    // Абстрактний метод
    public abstract void Move();
}

Проміжний абстрактний клас

Іноді на проміжному рівні теж потрібні власні абстракції.


public abstract class Car : Vehicle
{
    public Car(string model) : base(model) { }

    public override void Move()
    {
        Console.WriteLine($"{Model} їде по дорозі.");
    }

    // Абстрактний метод — не кожен автомобіль електричний:
    public abstract void RefuelOrCharge();
}

Конкретні реалізації


public class ElectricCar : Car
{
    public ElectricCar(string model) : base(model) { }

    public override void RefuelOrCharge()
    {
        Console.WriteLine($"{Model} заряджається електрикою.");
    }
}

public class GasolineCar : Car
{
    public GasolineCar(string model) : base(model) { }

    public override void RefuelOrCharge()
    {
        Console.WriteLine($"{Model} заправляється бензином.");
    }
}

Результат: абстракція на кількох рівнях спрощує додавання нових класів і функцій.

Візуалізація: приклад ієрархії класів

Клас Тип Абстрактний Батьківський Особлива поведінка
Vehicle Базовий Так - Абстрактний метод Move()
Car Проміжний Так Vehicle Абстрактний RefuelOrCharge()
ElectricCar Кінцевий Ні Car Реалізація RefuelOrCharge()
GasolineCar Кінцевий Ні Car Реалізація RefuelOrCharge()
Boat Кінцевий Ні Vehicle Реалізація Move()

4. Застосування абстракції для масштабованих застосунків

Практична користь:

  1. Гнучкість і розширюваність коду: Ви можете додати новий клас (наприклад, новий тип тварини, транспортного засобу чи співробітника), не змінюючи вже написаний робочий код. Ваші цикли та методи працюватимуть із новим об’єктом автоматично — головне, щоб він реалізував методи, визначені в абстракції.
  2. Мінімізація дублювання коду: Спільні властивості та методи (наприклад, імʼя, базова логіка) визначаються один раз в абстрактному класі, і нащадки їх автоматично успадковують.
  3. Поширений підхід у великих фреймворках і системах: Наприклад, в ASP.NET MVC є абстрактні базові контролери (ControllerBase), а у WinForms — абстрактний клас Control для всіх UI-елементів. Це дозволяє розширювати фреймворк, не порушуючи стабільність уже написаних частин.

Де стане у пригоді?

  • Будь-які великі системи: банківські застосунки (співробітники, операції та транзакції), ігри (ігрові сутності, персонажі), бізнес-застосунки (каталоги, товари, користувачі), будь-які ієрархічні та структуровані дані.
  • Технічні співбесіди: уміння будувати класи на основі абстракцій і пояснювати ієрархії — часте запитання на співбесіді.
  • Архітектура програм: абстракція дозволяє будувати «скелет» архітектури ще до написання логіки — зручно для командної розробки, TDD (розробка через тестування) і просто для підтримуваності.

5. Типові помилки й підводні камені під час побудови ієрархій

Помилка № 1: забута реалізація абстрактного методу.
Компілятор не дозволить створити екземпляр похідного класу, якщо в ньому не реалізовані всі абстрактні члени базового класу. Це обов’язкове правило, як неписаний закон: хочете створити конкретний клас — реалізуйте все.

Помилка № 2: спроба множинного успадкування від абстрактних класів.
У C# не можна успадковувати від кількох класів одночасно, навіть якщо всі вони абстрактні. Це обмеження мови. Якщо вам потрібно «успадкувати» поведінку з різних джерел — використовуйте інтерфейси. Вони подібні до абстрактних класів, але легші й без реалізацій.

Помилка № 3: нерозуміння призначення проміжних абстрактних класів.
Такі класи часто використовують для групування спільної логіки і захисту від створення «недовизначених» об’єктів. Наприклад, клас Car може бути абстрактним, щоб ніхто не створював просто «машину», не уточнивши, яка вона: бензинова, дизельна чи електрична.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ