]>Интерфейсы простыми средствами

Интерфейсы простыми средствами

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

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

Интерфейс — это набор функций, которые следует обязательно реализовать, то есть написать тело функций.

Наследование от интерфейса — это реализация набора функций.

Метод в объектно‐ориентированном программировании — это функция интерфейса.

Класс в объектно‐ориентированном программировании — это набор данных (структура), объединённый вместе с использующими эти данные функциями.

Объект — это переменная определённого класса или указатель на экземпляр таких данных (указатель на экземпляр структуры).

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

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

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

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

Обычно имя интерфейса строится по правилу «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 VirtualTable 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 VirtualTable As IMathVirtualTable Ptr
End Type

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

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

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

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

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

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

&t;Return LeftOperand + RightOperand
End Function

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

&t;Return LeftOperand - RightOperand
End Function

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

&t;Return LeftOperand * RightOperand
End Function

Function Divide( _
&t;&t;ByVal this As IMath 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
Dim Shared GlobalMathVirtualTable As IMathVirtualTable
GlobalMathVirtualTable.Add = @Add
GlobalMathVirtualTable.Substract = @Substract
GlobalMathVirtualTable.Multiply = @Multiply
GlobalMathVirtualTable.Divide = @Divide

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

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

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

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

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

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

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

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

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

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

Объект готов. Вызываем методы интерфейса у объекта:

Код FreeBASIC
Print objMath->VirtualTable->Add(objMath, 3, 5)
Print objMath->VirtualTable->Substract(objMath, 8, 4)
Print objMath->VirtualTable->Multiply(objMath, 16, 5)
Print objMath->VirtualTable->Divide(objMath, 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 VirtualTable 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 VirtualTable 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.

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