Что такое абстрактный класс
Перейти к содержимому

Что такое абстрактный класс

Что такое абстрактный класс

Кроме обычных классов в C# есть абстрактные классы . Зачем они нужны? Классы обычно представляют некий план определенного рода объектов или сущностей. Например, мы можем определить класс Car для преставления машин или класс Person для представления людей, вложив в эти классы соответствующие свойства, поля, методы, которые будут описывать данные объекты. Однако некоторые сущности, которые мы хотим выразить с помощью языка программирования, могут не иметь конкретного воплощения. Например, в реальности не существует геометрической фигуры как таковой. Есть круг, прямоугольник, квадрат, но просто фигуры нет. Однако же и круг, и прямоугольник имеют что-то общее и являются фигурами. И для описания подобных сущностей, которые не имеют конкретного воплощения, предназначены абстрактные классы.

Абстрактный класс похож на обычный класс. Он также может иметь переменные, методы, конструкторы, свойства. Единственное, что при определении абстрактных классов используется ключевое слово abstract . Например, определим абстрактный класс, который представляет некое транспортное средство:

abstract class Transport < public void Move() < Console.WriteLine("Транспортно средство движется"); >>

Транспортное средство представляет некоторую абстракцию, которая не имеет конкретного воплощения. То есть есть легковые и грузовые машины, самолеты, морские судна, кто-то на космическом корабле любит покататься, но как такового транспортного средства нет. Тем не менее все транспортные средства имеют нечто общее — они могут перемещаться. И для этого в классе определен метод Move, который эмулирует перемещение.

Но главное отличие абстрактных классов от обычных состоит в том, что мы НЕ можем использовать конструктор абстрактного класса для создания экземпляра класса. Например, следующим образом:

Transport tesla = new Transport();

Тем не менее абстрактные классы полезны для описания некоторого общего функционала, который могут наследовать и использовать производные классы:

Transport car = new Car(); Transport ship = new Ship(); Transport aircraft = new Aircraft(); car.Move(); ship.Move(); aircraft.Move(); abstract class Transport < public void Move() < Console.WriteLine("Транспортное средство движется"); >> // класс корабля class Ship : Transport < >// класс самолета class Aircraft : Transport < >// класс машины class Car : Transport

В данном случае от класса Transport наследуются три класса, которые представляют различные типы транспортных средств. Тем не менее они имеют общую черту — они могут перемещаться с помощью метода Move() .

Выше писалось, что мы не можем использовать конструктор абстрактного класса для создания экземпляра этого класса. Тем не менее такой класс также может определять конструкторы:

Transport car = new Car("машина"); Transport ship = new Ship("корабль"); Transport aircraft = new Aircraft("самолет"); car.Move(); // машина движется ship.Move(); // корабль движется aircraft.Move(); // самолет движется abstract class Transport < public string Name < get; >// конструктор абстрактного класса Transport public Transport(string name) < Name = name; >public void Move() =>Console.WriteLine($" движется"); > // класс корабля class Ship : Transport < // вызываем конструктор базового класса public Ship(string name) : base(name) < >> // класс самолета class Aircraft : Transport < public Aircraft(string name) : base(name) < >> // класс машины class Car : Transport < public Car(string name) : base(name) < >>

В данном случае в абстрактном классе Transport определен конструктор — с помощью параметра он устанавливает значение свойства Name, которое хранит название транспортного средства. И в этом случае производные классы должны в своих конструкторах вызвать этот конструктор.

Абстрактные члены классов

Кроме обычных свойств и методов абстрактный класс может иметь абстрактные члены классов, которые определяются с помощью ключевого слова abstract и не имеют никакого функционала. В частности, абстрактными могут быть:

  • Методы
  • Свойства
  • Индексаторы
  • События

Абстрактные члены классов не должны иметь модификатор private. При этом производный класс обязан переопределить и реализовать все абстрактные методы и свойства, которые имеются в базовом абстрактном классе. При переопределении в производном классе такой метод или свойство также объявляются с модификатором override (как и при обычном переопределении виртуальных методов и свойств). Также следует учесть, что если класс имеет хотя бы один абстрактный метод (или абстрактные свойство, индексатор, событие), то этот класс должен быть определен как абстрактный .

Абстрактные члены также, как и виртуальные, являются частью полиморфного интерфейса. Но если в случае с виртуальными методами мы говорим, что класс-наследник наследует реализацию, то в случае с абстрактными методами наследуется интерфейс, представленный этими абстрактными методами.

Абстрактные методы

Например, выше в примере с транспортными средствами метод Move описывает передвижение транспортного средства. Однако различные типы транспорта перемещаются по разному — ездят по земле, летят по воздуху, плывут на воде и т.д. В этом случае мы можем сделать метод Move абстрактным, а его реализацию переложить на производные классы:

abstract class Transport < public abstract void Move(); >// класс корабля class Ship : Transport < // мы должны реализовать все абстрактные методы и свойства базового класса public override void Move() < Console.WriteLine("Корабль плывет"); >> // класс самолета class Aircraft : Transport < public override void Move() < Console.WriteLine("Самолет летит"); >> // класс машины class Car : Transport < public override void Move() < Console.WriteLine("Машина едет"); >>
Transport car = new Car(); Transport ship = new Ship(); Transport aircraft = new Aircraft(); car.Move(); // машина едет ship.Move(); // корабль плывет aircraft.Move(); // самолет летит
Абстрактные свойства

Следует отметить использование абстрактных свойств. Их определение похоже на определение автосвойств. Например:

abstract class Transport < // абстрактное свойство для хранения скорости public abstract int Speed < get; set; >> // класс корабля class Ship: Transport < int speed; public override int Speed < get =>speed; set => speed = value; > > class Aircraft : Transport < public override int Speed < get; set; >>

В классе Transport определено абстрактное свойство Speed, которое должно хранить скорость транспортного средства. Оно похоже на автосвойство, но это не автосвойство. Так как данное свойство не должно иметь реализацию, то оно имеет только пустые блоки get и set. В производных классах мы можем переопределить это свойство, сделав его полноценным свойством (как в классе Ship), либо же сделав его автоматическим (как в классе Aircraft).

Отказ от реализации абстрактных членов

Производный класс обязан реализовать все абстрактные члены базового класса. Однако мы можем отказаться от реализации, но в этом случае производный класс также должен быть определен как абстрактный:

Transport tesla = new Auto(); tesla.Move(); // легковая машина едет abstract class Transport < public abstract void Move(); >// класс машины abstract class Car : Transport<> class Auto: Car < public override void Move() < Console.WriteLine("легковая машина едет"); >>

В данном случае класс Car не реализует абстрактный метод Move базового класса Transport и поэтому также определен как абстрактный. Однако любые неабстрактные классы, производные от Car, все равно должны реализовать все унаследованные абстрактные методы и свойства.

Пример абстрактного класса

Xрестоматийным примером является система геометрических фигур. В реальности не существует геометрической фигуры как таковой. Есть круг, прямоугольник, квадрат, но просто фигуры нет. Однако же и круг, и прямоугольник имеют что-то общее и являются фигурами:

// абстрактный класс фигуры abstract class Shape < // абстрактный метод для получения периметра public abstract double GetPerimeter(); // абстрактный метод для получения площади public abstract double GetArea(); >// производный класс прямоугольника class Rectangle : Shape < public float Width < get; set; >public float Height < get; set; >// переопределение получения периметра public override double GetPerimeter() => Width * 2 + Height * 2; // переопрелеление получения площади public override double GetArea() => Width * Height; > // производный класс окружности class Circle : Shape < public double Radius < get; set; >// переопределение получения периметра public override double GetPerimeter() => Radius * 2 * 3.14; // переопрелеление получения площади public override double GetArea() => Radius * Radius * 3.14; >
var rectanle = new Rectangle < Width = 20, Height = 20 >; var circle = new Circle < Radius = 200 >; PrintShape(rectanle); // Perimeter: 80 Area: 400 PrintShape(circle); // Perimeter: 1256 Area: 125600 void PrintShape(Shape shape) < Console.WriteLine($"Perimeter: Area: "); >

Абстрактные классы в ООП на PHP

Пусть у вас есть класс User , а от него наследуют классы Employee и Student .

При этом предполагается, что вы будете создавать объекты классов Employee и Student , но объекты класса User — не будете, так как этот класс используется только для группировки общих свойств и методов своих наследников.

В этом случае можно принудительно запретить создавать объекты класса User , чтобы вы или другой программист где-нибудь их случайно не создали.

Для этого существуют так называемые классы. Абстрактные классы представляют собой классы, предназначенные для наследования от них. При этом объекты таких классов нельзя создать.

Для того, чтобы объявить класс абстрактным, нужно при его объявлении написать ключевое слово abstract :

Итак, давайте напишем реализацию абстрактного класса User . Пусть у него будет приватное свойство name , а также геттеры и сеттеры для него:

name; > public function setName($name) < $this->name = $name; > > ?>

Попытка создать объект класса User вызовет ошибку:

А вот унаследовать от нашего класса будет можно. Сделаем класс Employee , который будет наследовать от нашего абстрактного класса User :

salary; > public function setSalary($salary) < $this->salary = $salary; > > ?>

Создадим объект класса Employee — все будет работать:

setName(‘john’); // метод родителя, т.е. класса User $employee->setSalary(1000); // свой метод, т.е. класса Employee echo $employee->getName(); // выведет ‘john’ echo $employee->getSalary(); // выведет 1000 ?>

Аналогично можно создать объект класса Student , наследующий от User :

scholarship; > public function setScholarship($scholarship) < $this->scholarship = $scholarship; > > ?>

Самостоятельно, не подсматривая в мой код, реализуйте такой же абстрактный класс User , а также классы Employee и Student , наследующие от него.

Абстрактные методы

Абстрактные классы также могут содержать абстрактные методы. Такие методы не должны иметь реализации, а нужны для того, чтобы указать, что такие методы должны быть у потомков. А собственно реализация таких методов — уже задача потомков.

Для того, чтобы объявить метод абстрактным, при его объявлении следует написать ключевое слово abstract .

Давайте попробуем на практике. Пусть предполагается, что все потомки класса User должны иметь метод increaseRevenue ( увеличить доход ).

Этот метод должен брать текущий доход пользователя и увеличивать его на некоторую величину, переданную параметром.

Сам класс User не знает, какой именно доход будет получать наследник — ведь у работника это зарплата, а у студента — стипендия. Поэтому каждый потомок будет реализовывать этот метод по-своему.

Фишка тут в том, что абстрактный метод класса User заставляет программиста реализовывать этот метод в потомках, иначе PHP выдаст ошибку. Таким образом вы, или другой программист, работающий с вашим кодом, никак не сможете забыть реализовать нужный метод в потомке.

Итак, давайте попробуем на практике. Добавим абстрактный метод increaseRevenue в класс User :

name; > public function setName($name) < $this->name = $name; > // Абстрактный метод без тела: abstract public function increaseRevenue($value); > ?>

Пусть наш класс Employee пока останется без изменений. В этом случае, даже если не создавать объект класса Employee , а просто запустить код, в котором определяются наши классы, — PHP выдаст ошибку.

Давайте теперь напишем реализацию метода increaseRevenue в классе Employee :

salary; > public function setSalary($salary) < $this->salary = $salary; > // Напишем реализацию метода: public function increaseRevenue($value) < $this->salary = $this->salary + $value; > > ?>

Проверим работу нашего класса:

setName(‘john’); // установим имя $employee->setSalary(1000); // установим зарплату $employee->increaseRevenue(100); // увеличим зарплату echo $employee->getSalary(); // выведет 1100 ?>

Реализуем метод increaseRevenue и в классе Student . Только теперь наш метод будет увеличивать уже стипендию:

scholarship; > public function setScholarship($scholarship) < $this->scholarship = $scholarship; > // Метод увеличивает стипендию: public function increaseRevenue($value) < $this->scholarship = $this->scholarship + $value; > > ?>

Добавьте в ваш класс User такой же абстрактный метод increaseRevenue . Напишите реализацию этого метода в классах Employee и Student .

Добавьте также в ваш класс User абстрактный метод decreaseRevenue ( уменьшить зарплату ). Напишите реализацию этого метода в классах Employee и Student .

Некоторые замечания

При наследовании от абстрактного класса, все методы, помеченные абстрактными в родительском классе, должны быть определены в дочернем классе.

При этом область видимости этих методов должна совпадать или быть менее строгой. Что значит менее строгой: например, если абстрактный метод объявлен как protected , то реализация этого метода должна быть protected или public , но не private .

Объявления методов также должны совпадать: количество обязательных параметром должно быть одинаковым. Однако класс-потомок может добавлять необязательные параметры, которые не были указаны при объявлении метода в родителе.

Практика

Пусть нам необходимо работать с геометрическими фигурами, например, с квадратами, прямоугольниками, треугольниками и так далее. Пусть каждая фигура будет описываться своим классом, при этом мы хотим сделать так, чтобы каждый класс имел метод для вычисления площади и метод для вычисления периметра фигуры.

Давайте сделаем для этого абстрактный класс Figure с двумя абстрактными методами getSquare и getPerimeter .

Почему класс Figure абстрактный: потому что он не описывает реально существующую геометрическую фигуру и, соответственно, объект этого класса мы не будем создавать.

Почему методы getSquare и getPerimeter абстрактные: потому что каждая фигура имеет свой алгоритм вычисления площади и периметра и, соответственно, класс Figure не может написать реализацию этих методов.

Зачем нам вообще нужен класс Figure : чтобы наследовать от него и таким образом заставить всех наследников реализовать указанные методы.

Итак, напишем реализацию класса Figure :

Пусть теперь мы хотим создать класс Quadrate для описания геометрической фигуры квадрат. Как известно, у квадрата все стороны равны, поэтому для описания квадрата нам нужно задать только его ширину.

Давайте для этого сделаем приватное свойство $a , значение которого будет задаваться в конструкторе класса:

Давайте теперь унаследуем наш класс Quadrate от класса Figure :

a = $a; > > /* Код класса не рабочий и будет выдавать ошибку, так как мы не написали реализацию методов родителя. */ ?>

Сейчас наша реализация класса Quadrate не рабочая, так как мы не написали реализацию абстрактных методов родителя.

Давайте сделаем это:

a = $a; > public function getSquare() < return $this->a * $this->a; > public function getPerimeter() < return 4 * $this->a; > > ?>

Давайте создадим квадрат со стороной 2 и найдем его площадь и периметр:

getSquare(); // выведет 4 echo $quadrate->getPerimeter(); // выведет 8 ?>

Сделайте аналогичный класс Rectangle ( прямоугольник ), у которого будет два приватных свойства: $a для ширины и $b для длины. Данный класс также должен наследовать от класса Figure и реализовывать его методы.

Усложним

Сейчас все методы класса Figure — абстрактные. Это, конечно же, не обязательно. Пусть наш класс имеет еще и метод getRatio , который будет находить отношение площади к периметру (то есть одно делить на второе).

Этот метод уже будет не абстрактный, а иметь реализацию, и все потомки смогут воспользоваться этим методом.

Почему мы можем написать реализацию этого метода прямо в классе Figure : потому что этот метод будет одинаковым для всех потомков.

Итак, добавим наш метод:

getSquare() / $this->getPerimeter(); > > ?>

Обратите внимание на следующее: хотя методы getSquare и getPerimeter абстрактные и не имеют реализации, мы их все равно можем использовать в своем методе getRatio , хотя реализация этих методов появится только в потомках.

Применим наш метод:

getSquare(); // выведет 4 echo $quadrate->getPerimeter(); // выведет 8 echo $quadrate->getRatio(); // выведет 0.5 ?>

Добавьте в класс Figure метод getSquarePerimeterSum , который будет находить сумму площади и периметра.

Абстрактные классы — PHP: Погружаясь в классы

Может ли нам понадобиться когда-нибудь создавать объекты класса HTMLElement? Наверняка нет. Вся работа строится на базе конкретных элементов, а значит, на практике, всегда используются его наследники.

В PHP классы, объекты которых не имеют смысла, принято помечать специальным ключевым словом abstract . Тогда никто не сможет создать объект этого класса напрямую. Только через наследников. В остальном, абстрактный класс такой же класс, со своими методами и свойствами.

 abstract class HTMLElement  public $attributes = []; public function __construct($attributes = [])  $this->attributes = $attributes; > public function getAttribute(string $key)  return $this->attributes[$key]; > > 

Абстрактные классы имеют смысл только в связке с наследованием. В большинстве случаев не нужно создавать объект от базового класса, поэтому его логично пометить как абстрактный.

Ещё одна особенность абстрактных классов связана с использованием интерфейсов. Абстрактный класс, в отличие от конкретного, не должен реализовывать интерфейсы полностью. Всё, что не реализовывает абстрактный класс, должны реализовать его наследники. Рассмотрим конкретный пример. Каждый объект, описывающий конкретный DOM-элемент, имеет текстовое представление в виде HTML-тега. Это значит, что каждый класс, наследующий HTMLElement , должен реализовывать определённый метод, возвращающий кусок HTML. В такой ситуации можно ввести специальный интерфейс Showable (обычно так называют интерфейсы, которые отвечают за строковое представление), содержащий один метод __toString() .

 interface Showable  public function __toString(); > 
 abstract class HTMLElement implements Showable // __toString  // здесь нет реализации __toString > 

С одной стороны здесь написано что абстрактный класс реализует интерфейс, с другой, так как класс абстрактный, мы можем не реализовывать Showable напрямую. Но это обязательно придётся сделать в наследниках, что нам и нужно.

Абстрактные классы могут иметь абстрактные методы, фактически это сигнатуры методов, а не сами методы. В этом смысле они становятся похожи на интерфейсы.

 abstract class HTMLElement  abstract public function __toString(); > 

С появлением абстрактных классов возникает множество новых вопросов. Когда они нужны, а когда нет? Можно ли использовать абстрактные классы вместо интерфейсов? Как совмещать их с интерфейсами?

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов

Наши выпускники работают в компаниях:

Абстрактные классы и интерфейсы: 7‑я часть гайда по ООП

Узнайте истинную мощь наследования и полиморфизма! Раскрываем секреты абстрактных классов и интерфейсов.

Евгений Кучерявый

Евгений Кучерявый

Пишет о программировании, в свободное время создаёт игры. Мечтает открыть свою студию и выпускать ламповые RPG.

В предыдущей статье мы увидели, насколько удобнее становится ООП благодаря наследованию. Но оно может стать ещё лучше, если использовать абстрактные классы и интерфейсы.

Все статьи про ООП
  • Что такое классы и объекты.
  • Особенности работы с объектами.
  • Модификаторы доступа, инкапсуляция.
  • Полиморфизм и перегрузка методов.
  • Полиморфизм.
  • Наследование и ещё немного полиморфизма.
  • Абстрактные классы и интерфейсы.
  • Практикум.

Что такое абстракция в ООП

В широком смысле абстракция — это когда мы фокусируемся на тех свойствах системы, которые важны в рамках текущей задачи, а менее существенные отбрасываем.

Абстракции часто встречаются в повседневной жизни. Например, когда мы набираем и отправляем сообщения в мессенджере, то работаем лишь с клавиатурой и кнопкой «Отправить». Мы не задумываемся о версии приложения, о том, какую кодировку использует операционная система, сколько весит наше сообщение и т.д.

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

Похожая ситуация в объектно-ориентированном программировании, только там мы имеем дело с абстракцией данных и методов. Например, чтобы рассчитать зависимость количества потребляемого топлива от веса автомобиля, нам достаточно использовать атрибуты «вес» и «номер» (чтобы различать автомобили) и метод «ехать» (для моделирования пробега).

Также можно использовать индексаторы и события (это тема для отдельной статьи). Теперь рассмотрим применение этого интерфейса.

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($"[, ]"); > public void Interact(Player p) < Console.WriteLine($" interacting with "); > >

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

Заключение

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

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

Читайте также:

  • Не Windows единой: как писать кроссплатформенные приложения с GUI на C#
  • Лицензии BSD и MIT: чем они различаются и в каких проектах их используют
  • Перечисления в C#: как правильно использовать enum

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *