Код
#Руководства

Абстрактные классы и интерфейсы: 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. Методы и свойства придумайте, исходя из названий.

Заключение

Вот мы и рассмотрели основные части объектно-ориентированного программирования. Дальше вас ждёт практикум, в котором мы поработаем над полноценным проектом, чтобы закрепить полученные знания и узнать ещё немного полезностей.

Больше интересного про код в нашем телеграм-канале.  Подписывайтесь!

Жизнь можно сделать лучше!
Освойте востребованную профессию, зарабатывайте больше и получайте от работы удовольствие.
Каталог возможностей
Понравилась статья?
Да

Пользуясь нашим сайтом, вы соглашаетесь с тем, что мы используем cookies 🍪

Ссылка скопирована