]>Интерфейсы и полиморфизм

Интерфейсы и полиморфизм

Аватар пользователя mabu

Для начала немного неточной терминологии.

Интерфейс
Единый набор функций, которые следует обязательно реализовать. Абстрактный тип данных, определяющий поведение некоей сущности.
Класс
Единый набор функций и данных. Абстрактный тип данных, определяющий и реализующий поведение и устройство сущности.
Объект
Переменная определённого класса или указатель на экземпляр таких данных. Конкретный тип.
Наследование
Концепция, согласно которой существующий тип данных (родитель, базовый класс, прототип) и его поведение используются как образцы для построения другого типа данных (наследника) и поведения.
Полиморфизм
Способность функции обрабатывать данные разных типов.
Метод
Функция интерфейса или класса.

Вот общие принципы организации интерфейсов:

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

  1. Этапы построения интерфейса
    1. Правила именования
    2. Интерфейс как тип данных
    3. Контекст вызова
    4. Таблица виртуальных методов
    5. Разрешение циклических ссылок
  2. Наследование класса от интерфейса
    1. Класс, наследующий интерфейс
    2. Реализация функций
    3. Таблица виртуальных методов
    4. Создание объектов
  3. Пример интерфейса ICloneable для клонирования объектов
    1. Интерфейс ICloneable
    2. Класс Monster
    3. Соединяем всё вместе
  4. Множественное наследование интерфейсов

Этапы построения интерфейса

Правила именования

Обычно имя интерфейса строится по правилу «I<Имя>», то есть состоит из написанного с заглавной буквы осмысленного имени, которому предшествует заглавная латинская буква I. Примеры: IUnknown, IDispatch, IStringList и тому подобное.

Интерфейс как тип данных

Определим набор функций для операций над двумя числами: сложение, вычитание, умножение и деление. Так как нам впоследствии необходимо будет использовать реальные функции, это значит, что такой набор должен состоять из указателей на наши будущие функции. Завернём их в структуру:

Код FreeBASIC
Type IMath
&t;Dim Add As Function( _
&t;&t;ByVal LeftOperand As Integer, _
&t;&t;ByVal RightOperand As Integer _
&t;)As Integer

&t;Dim Substract As Function( _
&t;&t;ByVal LeftOperand As Integer, _
&t;&t;ByVal RightOperand As Integer _
&t;)As Integer

&t;Dim Multiply As Function( _
&t;&t;ByVal LeftOperand As Integer, _
&t;&t;ByVal RightOperand As Integer _
&t;)As Integer

&t;Dim Divide As Function( _
&t;&t;ByVal LeftOperand As Integer, _
&t;&t;ByVal RightOperand As Integer _
&t;)As Integer
End Type

Контекст вызова

Для полноценной работы методы должны иметь доступ к данным объекта. Чтобы функции интерфейса знали, какой объект их вызывает, добавим в них указатель на объект, реализующий наш интерфейс, первым параметром:

Код FreeBASIC
Type IMath
&t;Dim Add As Function( _
&t;&t;ByVal this As IMath Ptr, _
&t;&t;ByVal LeftOperand As Integer, _
&t;&t;ByVal RightOperand As Integer _
&t;)As Integer

&t;Dim Substract As Function( _
&t;&t;ByVal this As IMath Ptr, _
&t;&t;ByVal LeftOperand As Integer, _
&t;&t;ByVal RightOperand As Integer _
&t;)As Integer

&t;Dim Multiply As Function( _
&t;&t;ByVal this As IMath Ptr, _
&t;&t;ByVal LeftOperand As Integer, _
&t;&t;ByVal RightOperand As Integer _
&t;)As Integer

&t;Dim Divide As Function( _
&t;&t;ByVal this As IMath Ptr, _
&t;&t;ByVal LeftOperand As Integer, _
&t;&t;ByVal RightOperand As Integer _
&t;)As Integer
End Type

Таблица виртуальных методов

На практике набор функций интерфейса выделяют в отдельную структуру, которую теперь называеют таблицей виртуальных методов, а в самом интерфейсе оставляют ссылку (указатель) на неё. Это позволяет использовать одну и ту же таблицу для разных объектов одного класса, реализующих один и тот же интерфейс, экономя память. Название VirtualTable часто сокращают до Vtable или даже Vtbl, но в данном случае мы будет использовать полный вариант.

Код FreeBASIC
Type IMathVirtualTable
&t;Dim Add As Function( _
&t;&t;ByVal this As IMath Ptr, _
&t;&t;ByVal LeftOperand As Integer, _
&t;&t;ByVal RightOperand As Integer _
&t;)As Integer

&t;Dim Substract As Function( _
&t;&t;ByVal this As IMath Ptr, _
&t;&t;ByVal LeftOperand As Integer, _
&t;&t;ByVal RightOperand As Integer _
&t;)As Integer

&t;Dim Multiply As Function( _
&t;&t;ByVal this As IMath Ptr, _
&t;&t;ByVal LeftOperand As Integer, _
&t;&t;ByVal RightOperand As Integer _
&t;)As Integer

&t;Dim Divide As Function( _
&t;&t;ByVal this As IMath Ptr, _
&t;&t;ByVal LeftOperand As Integer, _
&t;&t;ByVal RightOperand As Integer _
&t;)As Integer
End Type

Type IMath
&t;Dim pVirtualTable As IMathVirtualTable Ptr
End Type

Разрешение циклических ссылок

Пытаемся всё скомпилировать, но компилятор почему‐то сопротивляется такому коду. Дело в том, что виртуальная таблица IMathVirtualTable ссылается на интерфейс IMath, объявленный позднее, а интерфейс IMath ссылается на виртуальную таблицу IMathVirtualTable, и как их не меняй местами, ссылаться друг на друга от этого они не перестают. Выйти из ситуации поможет дополнительное имя для нашего интерфейса, введённое оператором Type, а к названию оригинального интерфейса добавим подчёркивание.

Код FreeBASIC
Type IMath As IMath_

Type IMathVirtualTable
&t;Dim Add As Function( _
&t;&t;ByVal this As IMath Ptr, _
&t;&t;ByVal LeftOperand As Integer, _
&t;&t;ByVal RightOperand As Integer _
&t;)As Integer

&t;Dim Substract As Function( _
&t;&t;ByVal this As IMath Ptr, _
&t;&t;ByVal LeftOperand As Integer, _
&t;&t;ByVal RightOperand As Integer _
&t;)As Integer

&t;Dim Multiply As Function( _
&t;&t;ByVal this As IMath Ptr, _
&t;&t;ByVal LeftOperand As Integer, _
&t;&t;ByVal RightOperand As Integer _
&t;)As Integer

&t;Dim Divide As Function( _
&t;&t;ByVal this As IMath Ptr, _
&t;&t;ByVal LeftOperand As Integer, _
&t;&t;ByVal RightOperand As Integer _
&t;)As Integer
End Type

Type IMath_
&t;Dim pVirtualTable As IMathVirtualTable Ptr
End Type

Ну вот теперь‐то описание нашего интерфейса готово.

Наследование класса от интерфейса

Наследование от интерфейса есть его реализация в производном классе. Здесь нам нужно объявить класс‐наследник, написать функции и создать таблицу.

Класс, наследующий интерфейс

Теперь объявим класс, наследующий наш интерфейс:

Код FreeBASIC
Type Math
&t;Dim pVirtualTable As IMathVirtualTable Ptr
End Type

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

Реализация функций

Во всей этой мешанине виртуальных таблиц ты ещё не забыл, что интерфейс — это набор функций? Как раз пора приняться за них вплотную.

Код FreeBASIC
Function MathAdd( _
&t;&t;ByVal this As Math Ptr, _
&t;&t;ByVal LeftOperand As Integer, _
&t;&t;ByVal RightOperand As Integer _
&t;)As Integer

&t;Return LeftOperand + RightOperand
End Function

Function MathSubstract( _
&t;&t;ByVal this As Math Ptr, _
&t;&t;ByVal LeftOperand As Integer, _
&t;&t;ByVal RightOperand As Integer _
&t;)As Integer

&t;Return LeftOperand - RightOperand
End Function

Function MathMultiply( _
&t;&t;ByVal this As Math Ptr, _
&t;&t;ByVal LeftOperand As Integer, _
&t;&t;ByVal RightOperand As Integer _
&t;)As Integer

&t;Return LeftOperand * RightOperand
End Function

Function MathDivide( _
&t;&t;ByVal this As Math Ptr, _
&t;&t;ByVal LeftOperand As Integer, _
&t;&t;ByVal RightOperand As Integer _
&t;)As Integer

&t;Return LeftOperand \ RightOperand
End Function

Параметр this представляет собой указатель на объект, который вызвал функцию интерфейса.

Таблица виртуальных методов

Создадим таблицу виртуальных функций класса Math. Это требуется сделать всего один раз, она будет общая для всех производных объектов.

Код FreeBASIC
Common Shared GlobalMathVirtualTable As IMathVirtualTable
GlobalMathVirtualTable.Add = @MathAdd
GlobalMathVirtualTable.Substract = @MathSubstract
GlobalMathVirtualTable.Multiply = @MathMultiply
GlobalMathVirtualTable.Divide = @MathDivide

Создание объектов

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

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

Код FreeBASIC
' Выделяем память под объект, унаследованный от IMath
Dim objMath As Math Ptr = Allocate(SizeOf(Math))

' Обязательно проверяем на успешность выделения памяти
If objMath = 0 Then
&t;Print "Не удалось выделить память под объект"
&t;End(1)
End If

' Назначаем объекту таблицу виртуальных фукнций
objMath->VirtualTable = @GlobalMathVirtualTable

Объект готов. Теперь получим указатель на интерфейс и вызовем методы интерфейса у объекта:

Код FreeBASIC
Dim pIMath As IMath Ptr = @objMath

Print pIMath->pVirtualTable->Add(pIMath, 3, 5)
Print pIMath->pVirtualTable->Substract(pIMath, 8, 4)
Print pIMath->pVirtualTable->Multiply(pIMath, 16, 5)
Print pIMath->pVirtualTable->Divide(pIMath, 9, 3)

' Удаляем ненужные объекты
Deallocate(objMath)

Так как мы определили таблицу виртуальных функций явно, то код получился немного громоздким. В высокоуровневых языках программирования типа C++ эта таблица и контекст вызова существуют неявно и скрыто от программиста.

Пример интерфейса ICloneable для клонирования объектов

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

Интерфейс ICloneable

Интерфейс ICloneable предоставляет возможность объекту самому создавать собственную копию. Он должен содержать один метод Clone.

Пример заголовочного файла ICloneable.bi:

Код FreeBASIC
#ifndef ICLONEABLE_BI
#define ICLONEABLE_BI

Type ICloneable As ICloneable_

Type ICloneableVirtualTable
&t;Dim Clone As Function( _
&t;&t;ByVal this As ICloneable Ptr _
&t;)As ICloneable Ptr
End Type

Type ICloneable_
&t;Dim pVirtualTable As ICloneableVirtualTable Ptr
End Type

#endif

Класс Monster

Объявление

Теперь объявим класс Monster и унаследуем его от интерфейса ICloneable в заголовочном файле Monster.bi:

Код FreeBASIC
#ifndef MONSTER_BI
#define MONSTER_BI

#include "ICloneable.bi"

Type Monster
&t;' Виртуальная таблица
&t;Dim pVirtualTable As ICloneableVirtualTable Ptr

&t;' Параметры монстра

&t;' Координаты на игровом поле
&t;Dim X As Integer
&t;Dim Y As Integer

&t;' Сила удара монстра
&t;Dim Power As Integer

&t;' Здоровье монстра
&t;Dim Health As Integer

&t;' Отображаемое имя
&t;Dim CharacterName As WString Ptr
End Type

Declare Function Clone( _
&t;ByVal this As Monster Ptr _
)As Monster Ptr

#endif

Реализация

Посмотрим на реализацию клонирования монстра в файле Monster.bas:

Код FreeBASIC
#include "Monster.bi"

Function Clone( _
&t;&t;ByVal this As Monster Ptr _
&t;)As Monster Ptr

&t;' Выделяем память под монстра
&t;Dim pMonster As Monster Ptr = Allocate(SizeOf(Monster))
&t;If pMonster = 0 Then
&t;&t;Return 0
&t;End If

&t;' Клонирование имени монстра
&t;Dim CharacterNameLength As Integer = Len(*this->CharacterName)
&t;Dim pCharacterName As WString Ptr = Allocate((CharacterNameLength + 1) * SizeOf(WString))
&t;If pCharacterName = 0 Then
&t;&t;Deallocate(pMonster)
&t;&t;Return 0
&t;End If
&t;*pCharacterName = *this->CharacterName

&t;' Клонирование самих себя
&t;pMonster->VirtualTable = this->VirtualTable
&t;pMonster->X = this->X
&t;pMonster->Y = this->Y
&t;pMonster->Power = this->Power
&t;pMonster->Health = this->Health
&t;pMonster->CharacterName = pCharacterName

&t;Return pMonster
End Function

Соединяем всё вместе

В этом примере создадим десять клонированных монстров на основе уже существующего.

Код FreeBASIC
#include "ICloneable.bi"
#include "Monster.bi"

' Виртуальная таблица функций монстра
Dim Shared GlobalMonsterVirtualTable As ICloneableVirtualTable
GlobalMonsterVirtualTable.Clone = CPtr(Any Ptr, @Clone)

' Шаблон монстра для клонирования
Dim objMainMonster As Monster Ptr = Allocate(SizeOf(Monster))

' Обязательно проверяем на успешность выделения памяти
If objMainMonster = 0 Then
&t;Print "Не удалось выделить память под объект"
&t;End(1)
End If

' Назначаем объекту таблицу виртуальных фукнций
objMainMonster->VirtualTable = @GlobalMonsterVirtualTable

' Параметры монстра
objMainMonster->X = 25
objMainMonster->Y = 40
objMainMonster->Power = 67
objMainMonster->Health = 100
objMainMonster->CharacterName = @"Ужасный маленький монстр"

' Десять дополнительных клонов
Dim Clones(9) As ICloneable Ptr
For i As Integer = 0 To 9
&t;Clones(i) = CPtr(ICloneable Ptr, objMainMonster->VirtualTable->Clone(CPtr(ICloneable Ptr, objMainMonster)))
Next

' Проверяем работоспособность клонов
For i As Integer = 0 To 9
&t;Dim pMonster As Monster Ptr = CPtr(Monster Ptr, Clones(i))
&t;Print pMonster->X,
&t;Print pMonster->Y,
&t;Print pMonster->Power,
&t;Print pMonster->Health,
&t;Print *pMonster->CharacterName
Next

' Удаляем клонов
For i As Integer = 0 To 9
&t;Dim pMonster As Monster Ptr = CPtr(Monster Ptr, Clones(i))
&t;Deallocate(pMonster->CharacterName)
&t;Deallocate(pMonster)
Next

' Удаляем шаблонный объект
Deallocate(objMainMonster)

Можно видеть, что клонирование не зависит от типа объекта. Это могут быть разные монстры, предметы, оружие. Клонировать можно что угодно, если оно реализует интерфейс ICloneable.

Поделись ссылочкой в социальных сетях