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 может быть абстрактным, чтобы никто не создавал просто "машину", не уточнив, какая она: бензиновая, дизельная или электрическая.

2
Задача
C# SELF, 22 уровень, 3 лекция
Недоступна
Создание иерархии классов с абстрактным классом
Создание иерархии классов с абстрактным классом
2
Задача
C# SELF, 22 уровень, 3 лекция
Недоступна
Многоуровневая абстракция: транспортные средства
Многоуровневая абстракция: транспортные средства
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ