Перейти к содержимому

Зачем нужны абстрактные классы

  • автор:

ООП. Абстрактный класс. Декомпозиция программы.

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

В python существует стандартная библиотека abc, добавляющая в язык абстрактные базовые классы (АБК). АБК позволяют определить класс, указав при этом, какие методы или свойства обязательно переопределить в классах-наследниках.

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

from abc import ABC, abstractmethod class ChessPiece(ABC): # общий метод, который будут использовать все наследники этого класса def draw(self): print("Drew a chess piece") # абстрактный метод, который будет необходимо переопределять для каждого подкласса @abstractmethod def move(self): pass 
a = ChessPiece() # Если мы попытаемся инстанциировать данный класс, логично получим ошибку. 
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) in () ----> 1 a = ChessPiece() # Если мы попытаемся инстанциировать данный класс, логично получим ошибку. TypeError: Can't instantiate abstract class ChessPiece with abstract methods move

Как видите, система не дает нам создать экземпляр данного класса. Теперь нам необходимо создать конкретный класс, например, класс ферзя, в котором мы реализуем метод move.

class Queen(ChessPiece): def move(self): print("Moved Queen to e2e4") # Мы можем создать экземпляр класса q = Queen() # И нам доступны все методы класса q.draw() q.move() 
Drew a chess piece Moved Queen to e2e4

Обратите внимание, абстрактный метод может быть реализован сразу в абстрактном классе, однако, декоратор abstractmethod, обяжет программистов, реализующих подкласс либо реализовать собственную версию абстрактного метода, либо дополнить существующую. В таком случае, мы можем переопределять метод как в обычном наследовании, а вызывать родительский метод при помощи super().

from abc import ABC, abstractmethod class Basic(ABC): @abstractmethod def hello(self): print("Hello from Basic class") class Advanced(Basic): def hello(self): super().hello() print("Enriched functionality") a = Advanced() a.hello() 
Hello from Basic class Enriched functionality

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

Декомпозиция программы на модули

Модули и пакеты в Python – это прекрасные инструменты для управления сложностью в программном проекте.

Создадим модуль с именем simplemath.py, который будет содержать функции для выполнения простых арифметических действий.

Создадим ещё один модуль worker.py, который будет использовать функции из simplemath.py. Если мы хотим импортировать все функции, то оператор import для нас отлично подойдет. Это будет выглядеть так.

# представим, что эта ячейка - текстовый редактор, который мы сохраним под именем simplemath.py def add(a, b): return a + b def sub(a, b): return a - b def mul(a, b): return a * b def div(a, b): return a / b 
# представим, что эта ячейка - текстовый редактор, который мы сохраним под именем simplemath.py #import simplemath #from simplemath inpord add,sub,mul,div #print(simplemath.add(1, 2)) # = 3 #print(simplemath.sub(1, 2)) # = -1 #print(simplemath.mul(1, 2)) # = 2 #print(simplemath.div(1, 2)) # = 0.5 

Задачи:

Задача 1:

В файле вам даны 3 класса A , B , C , имеющие сходный (но не одинаковый) интерфейс. Вам необходимо создать абстрактный базовый класс Base и построить корректную схему наследования. При выполнении следует избегать дублирования кода, и стараться следовать SOLID принципам ООП.

Задача 2:

В файле вам дана программа. Необходимо провести её рефакторинг.

Для работы программы необходима библиотека PyGame. В открывшемся окне программы доступны следующие команды управления:

  • — показать справку по командам
  • — рестарт
  • — пауза, снять/поставить
  • — увеличить количество точек «сглаживания»
  • — уменьшить количество точек «сглаживания»
  • — добавить «опорную» точку

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

  1. Изучить документацию к библиотеке pygame и код программы. Понять механизм работы программы (как происходит отрисовка кривой, перерасчет точек сглаживания и другие нюансы реализации программы)
  2. Провести рефакторниг кода, переписать программу в ООП стиле с использованием классов и наследования. Реализовать класс 2-мерных векторов Vec2d . В классе следует определить методы для основных математических операций, необходимых для работы с вектором. Добавить возможность вычислять длину вектора с использованием функции len(a) и метод int_pair , который возвращает кортеж из двух целых чисел (текущие координаты вектора).

Реализовать класс замкнутых ломаных Polyline с методами отвечающими за добавление в ломаную точки ( Vec2d ) c её скоростью, пересчёт координат точек ( set_points ) и отрисовку ломаной ( draw_points ). Арифметические действия с векторами должны быть реализованы с помощью операторов, а не через вызовы соответствующих методов.

Реализовать класс Knot (наследник класса Polyline ), в котором добавление и пересчёт координат инициируют вызов функции get_knot для расчёта точек кривой по добавляемым «опорным» точкам.

Все классы должны быть самостоятельными и не использовать внешние функции.

Задача 3* ДНК

Реализуйте классы для ДНК (двойная цепочк) и РНК (одинарная цепочка). Данные структуры данных должны поддерживать следующие возможности:

Умножение РНК

1. Создавать структуру из строк. Обратите внимание, что в ДНК встречаются только азотистые основания ATGC, а в РНК (AUGC) поэтому если во входной строке содержались другие символы, необходимо поднимать ошибку (Exception). 2. Поддерживают индексацию. РНК по индексу возвращает i-ое азотистое основание, ДНК — пару азотистых оснований (соответствующие первой и второй цепочке) 3. РНК может возвращать комплиментарную ДНК (каждому азотистому основанию из РНК соответсвует соответсвующее основание для первой цепочки ДНК: AT , UA , GC , CG . Вторая цепочка ДНК строится комплиментарной первой строчке ДНК: AT , TA , GC , CG ) 4. РНК, как и ДНК, могут складываться путем склеивания («AUUGAACUA» + «CGGAAA» = «AUUGAACUACGGAAA»). У ДНК склеиваются соответствующие цепочки ([«ACG», «TGC»] + [«TTTAAT», «AAATTA»] = [«ACGTTTAAT», «TGCAAATTA»]) 5. РНК могут перемножаться друг с другом: каждое азотистое основание результирующей РНК получается случайным выбором одного из двух соответсвующих родительских азотистых оснований. Если одна из цепочек длиннее другой, то перемножение происходит с начала, когда одна из цепочек закончится оставшийся хвост другой переносится без изменений. 6. ДНК могут перемножаться друг с другом: ПЕРВЫЕ цепочки каждой из ДНК перемножаются по такому же приницпу, как перемножаются РНК выше. Вторая цепочка результирующей ДНК строится как комплиментарная первой 7. Цепочки РНК и первую и вторую у ДНК можно проверять на равенство 8. Оба класса должны давать осмысленный вывод как при print, так и просто при вызове в ячейке | Обдумайте и создайте необходимые и, возможно, вспомогательные классы, настройте наследование, если требуется. Полученная структура должна быть адекватной и удобной, готовой к простому расширению функционала, если потребуется

Сайт построен с использованием Pelican. За основу оформления взята тема от Smashing Magazine. Исходные тексты программ, приведённые на этом сайте, распространяются под лицензией GPLv3, все остальные материалы сайта распространяются под лицензией CC-BY.

Абстрактные классы (C++)

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

Вы создаете абстрактный класс, объявляя по крайней мере одну чистую виртуальную функцию-член. Это виртуальная функция, объявленная с помощью синтаксиса чистого описателя ( = 0 ). Классы, производные от абстрактного класса, должны реализовывать чисто виртуальную функцию; в противном случае они также будут абстрактными.

Рассмотрим пример, представленный в функциях Virtual. Класс Account создан для того, чтобы предоставлять общие функции, но объекты типа Account имеют слишком общий характер для практического применения. Это означает Account , что это хороший кандидат для абстрактного класса:

// deriv_AbstractClasses.cpp // compile with: /LD class Account < public: Account( double d ); // Constructor. virtual double GetBalance(); // Obtain balance. virtual void PrintBalance() = 0; // Pure virtual function. private: double _balance; >; 

Единственное различие между этим и предыдущим объявлениями состоит в том, что функция PrintBalance объявлена со спецификатором чисто виртуальной функции pure ( = 0 ).

Ограничения на использование абстрактных классов

Абстрактные классы нельзя использовать для:

  • переменных и данных членов;
  • типов аргументов;
  • типов возвращаемых функциями значений;
  • типов явных преобразований.

Если конструктор абстрактного класса вызывает чистую виртуальную функцию напрямую или косвенно, результат не определен. Однако конструкторы и деструкторы абстрактных классов могут вызывать другие функции-члены.

Определенные чистые виртуальные функции

Чистые виртуальные функции в абстрактных классах можно определить или реализовать. Такие функции можно вызывать только с помощью полного синтаксиса:

Определенные чистые виртуальные функции полезны при разработке иерархий классов, базовые классы которых включают чистые виртуальные деструкторы. Это связано с тем, что деструкторы базового класса всегда вызываются во время уничтожения объектов. Рассмотрим следующий пример:

// deriv_RestrictionsOnUsingAbstractClasses.cpp // Declare an abstract base class with a pure virtual destructor. // It's the simplest possible abstract class. class base < public: base() <>// To define the virtual destructor outside the class: virtual ~base() = 0; // Microsoft-specific extension to define it inline: // virtual ~base() = 0 <>; >; base::~base() <> // required if not using Microsoft extension class derived : public base < public: derived() <>~derived() <> >; int main() < derived aDerived; // destructor called when it goes out of scope >

В примере показано, как расширение компилятора Майкрософт позволяет добавлять встроенное определение в чистую виртуальную ~base() . Его можно также определить за пределами класса с помощью base::~base() <> .

Когда объект aDerived выходит из область, вызывается деструктор класса derived . Компилятор создает код для неявного вызова деструктора класса base после derived деструктора. Пустая реализация для чистой виртуальной функции ~base гарантирует, что для функции существует хотя бы какая-то реализация. Без него компоновщик создает неразрешенную ошибку внешнего символа для неявного вызова.

В предыдущем примере чистая виртуальная функция base::~base вызывается неявно из derived::~derived . Кроме того, можно явно вызывать чистые виртуальные функции с помощью полного имени функции-члена. Такие функции должны иметь реализацию, или вызов приводит к ошибке во время связи.

Зачем нужны абстрактные классы

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

При определении абстрактных классов используется ключевое слово abstract :

public abstract class Human < private String name; public String getName() < return name; >>

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

Human h = new Human();

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

public abstract void display();

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

Зачем нужны абстрактные классы? Допустим, мы делаем программу для обслуживания банковских операций и определяем в ней три класса: Person, который описывает человека, Employee, который описывает банковского служащего, и класс Client, который представляет клиента банка. Очевидно, что классы Employee и Client будут производными от класса Person, так как оба класса имеют некоторые общие поля и методы. И так как все объекты будут представлять либо сотрудника, либо клиента банка, то напрямую мы от класса Person создавать объекты не будем. Поэтому имеет смысл сделать его абстрактным.

public class Program < public static void main(String[] args) < Employee sam = new Employee("Sam", "Leman Brothers"); sam.display(); Client bob = new Client("Bob", "Leman Brothers"); bob.display(); >> abstract class Person < private String name; public String getName() < return name; >public Person(String name) < this.name=name; >public abstract void display(); > class Employee extends Person < private String bank; public Employee(String name, String company) < super(name); this.bank = company; >public void display() < System.out.printf("Employee Name: %s \t Bank: %s \n", super.getName(), bank); >> class Client extends Person < private String bank; public Client(String name, String company) < super(name); this.bank = company; >public void display() < System.out.printf("Client Name: %s \t Bank: %s \n", super.getName(), bank); >>

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

// абстрактный класс фигуры abstract class Figure < float x; // x-координата точки float y; // y-координата точки Figure(float x, float y)< this.x=x; this.y=y; >// абстрактный метод для получения периметра public abstract float getPerimeter(); // абстрактный метод для получения площади public abstract float getArea(); > // производный класс прямоугольника class Rectangle extends Figure < private float width; private float height; // конструктор с обращением к конструктору класса Figure Rectangle(float x, float y, float width, float height)< super(x,y); this.width = width; this.height = height; >public float getPerimeter() < return width * 2 + height * 2; >public float getArea() < return width * height; >>

Абстрактные классы в ООП на 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 , который будет находить сумму площади и периметра.

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

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