Абстрактные классы и интерфейсы: 7‑я часть гайда по ООП
Узнайте истинную мощь наследования и полиморфизма! Раскрываем секреты абстрактных классов и интерфейсов.


vlada_maestro / shutterstock
В предыдущей статье мы увидели, насколько удобнее становится ООП благодаря наследованию. Но оно может стать ещё лучше, если использовать абстрактные классы и интерфейсы.
Все статьи про ООП
- Что такое классы и объекты.
- Особенности работы с объектами.
- Модификаторы доступа, инкапсуляция.
- Полиморфизм и перегрузка методов.
- Полиморфизм.
- Наследование и ещё немного полиморфизма.
- Абстрактные классы и интерфейсы.
- Практикум.
Что такое абстракция в ООП
В широком смысле абстракция — это когда мы фокусируемся на тех свойствах системы, которые важны в рамках текущей задачи, а менее существенные отбрасываем.
Абстракции часто встречаются в повседневной жизни. Например, когда мы набираем и отправляем сообщения в мессенджере, то работаем лишь с клавиатурой и кнопкой «Отправить». Мы не задумываемся о версии приложения, о том, какую кодировку использует операционная система, сколько весит наше сообщение и т.д.
Другой пример — вождение автомобиля. Водитель думает лишь о том, куда поворачивать руль и когда нажимать педали. Он не задумывается о том, какого цвета его автомобиль, из какого материала сделана обивка салона, сколько присадок в моторном масле, и прочих вещах, которые не влияют на процесс вождения.
Похожая ситуация в объектно-ориентированном программировании, только там мы имеем дело с абстракцией данных и методов. Например, чтобы рассчитать зависимость количества потребляемого топлива от веса автомобиля, нам достаточно использовать атрибуты «вес» и «номер» (чтобы различать автомобили) и метод «ехать» (для моделирования пробега).
class Car
{
private string id_number;
private int weight;
//константа пути (100 км)
private int const distance = 100;
public Car(string id_number, int weight){
this.id_number = id_number;
this.weight = weight;
}
int Go(){
//формула для расчета количества потребляемого топлива
}
};
Конечно, реальные автомобили отличаются цветом, габаритами, размером колес, типом шин, материалом обивки салона и т.д. А еще они умеют не только ехать, но и заводиться, тормозить и глохнуть. Но все эти подробности не имеют отношения к поставленной задаче, а значит, мы можем их смело опустить.
Абстрактные классы
Абстрактные классы в объектно-ориентированном программировании — это базовые классы, которые можно наследовать, но нельзя реализовывать. То есть на их основе нельзя создать объект.
Пример такого класса из жизни — животное. Рыбы, пауки и насекомые являются животными, но каждое из них также принадлежит своему подклассу с набором специфических свойств. При этом, просто «животных» в природе не существует.
В программировании абстрактные классы могут понадобиться, чтобы объединить реализацию нескольких схожих классов. Например, в вашей игре должны быть персонаж игрока и NPC (неигровые персонажи). У них могут быть общие свойства (имя, координаты) и методы (перемещение, изменение анимации).
Чтобы не повторять код несколько раз, можно вынести реализацию этих свойств и методов в абстрактный класс Character. Для его объявления используют ключевое слово abstract:
abstract class Character
{
private string name;
private int x;
public int y;
public Character(string name, int x, int y)
{
this.name = name;
this.x = x;
this.y = y;
}
public int X {
get
{
return this.x;
}
}
public void ShowName()
{
Console.WriteLine(this.name);
}
public abstract int Y { get; }
public abstract void ShowPosition();
}
Тут всё как у обычных классов, но в конце можно заметить объявление свойства и метода без реализации. Реализация этих абстрактных свойств должна находиться в дочернем классе:
class Player : Character
{
public Player(string name, int x, int y)
:base(name, x, y)
{
}
public override int Y
{
get
{
return this.y;
}
}
public override void ShowPosition()
{
Console.WriteLine($"[{X}, {Y}]");
}
}
Когда объявляется реализация такого члена класса, необходимо указать ключевое слово override. Абстрактными могут быть следующие члены класса:
- методы;
- свойства;
- индексаторы;
- события.
Дочерний класс должен реализовывать все члены родительского абстрактного класса, кроме тех случаев, когда дочерний класс тоже абстрактный.
В остальном всё очень похоже на обычные классы. Например, поле Y класса Character публичное, чтобы можно было использовать его в свойстве Y дочерних классов.
Важно!
Абстрактный класс должен быть публичным.
Интерфейсы
Интерфейс похож на абстрактный класс: вы так же не можете объявить его экземпляр, дочерний класс должен реализовывать все члены интерфейса. Реализация членов может находиться как в интерфейсе, так и в дочернем классе.
Сходств очень много, но у интерфейсов есть особенность: один класс может наследовать несколько интерфейсов сразу.
Объявляется интерфейс следующим образом:
//Используется ключевое слово interface
interface IInteractive //Названия интерфейсов принято начинать с заглавной буквы I
{
void Interact(Player p); //Метод без реализации
//public bool Accessor { get; set; }
//Доступно начиная с C# 8.0
bool IsInteractive() //Метод с реализацией
{
return true;
}
//const bool Interactive = true; //Константы
//static bool Enabled = true; //Статические поля
//Поля объявлять нельзя
}
Также можно использовать индексаторы и события (это тема для отдельной статьи). Теперь рассмотрим применение этого интерфейса.
class NPC : Character, IInteractive
{
public NPC(string name, int x, int y)
:base(name, x, y)
{
}
public override int Y
{
get
{
return this.y;
}
}
public override void ShowPosition()
{
Console.WriteLine($"[{X}, {Y}]");
}
public void Interact(Player p)
{
Console.WriteLine($"{p.Name} interacting with {this.Name}");
}
}
В отличие от абстрактных методов, методы интерфейса не нужно реализовывать с ключевым словом override.
Также есть одна особенность: метод, реализация которого находится внутри интерфейса, не может использовать этот метод — класс нужно привести к интерфейсу. Для примера добавим в класс Player следующий метод:
public void Interact(IInteractive obj)
{
if(obj.IsInteractive())
{
obj.Interact(this);
}
}
В качестве параметра в этот метод можно передавать любой класс, который использует интерфейс IInteractive.
Player p = new Player("Gamer", 5, 10);
NPC npc = new NPC("Cube", 10, 10);
p.Interact(npc);
Это очень удобно в разработке игр, в которых взаимодействовать можно с самыми разными объектами — от NPC до предметов.
Более подробно об отличиях интерфейсов и абстрактных классов на примерах из Java можно прочитать в другой нашей статье.
Домашние задание
Создайте игру, в которой будут использоваться абстрактные классы Character и Item, а также интерфейсы IInteractive, ITalkable, IMovable. Методы и свойства придумайте, исходя из названий.
Заключение
Вот мы и рассмотрели основные части объектно-ориентированного программирования. Дальше вас ждёт практикум, в котором мы поработаем над полноценным проектом, чтобы закрепить полученные знания и узнать ещё немного полезностей.
Больше интересного про код в нашем телеграм-канале. Подписывайтесь!