Теговые типы. Объектно-ориентированное программирование

ООП - подход в программировании, когда более-менее крупные единицы программирования (юниты в играх, кнопки в GUI и т.д.) объединяются в группы по каким-то общим признакам и действиям. На основе этих признаков и действий создается шаблон (т.н. класс) и по этому шаблону "клепаются" объекты. Например, возьмем людей. Для всех людей можно выделить присущие каждому признаки: рост, вес, возраст, цвет глаз и т.д., все люди могут стоять, сидеть, ходить, работать и т.д. Таким образом, создав тип (шаблон) "человек" мы сможем на основе этого типа создать сколько угодно людей с присущими им признаками. Кроме того, для любого из них мы сможем добавить уникальные характеристики и действия. Более подробно про суть ООП можно посмотреть на просторах интернета.

Ну и три кита ООП - наследование, инкапсуляция и полиморфизм. Что это такое и с чем его едят мы и рассмотрим.

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

Большинство компиляторов Ада позволяют создавать несколько пакетов внутри одного файла, однако считается, что это не очень хорошая идея. Лучше придерживаться правила: для каждого пакета свой *.abs и *.adb файл.

Пакет может выглядеть так:

package Pack_Person is
    type Person is --Запись "Персона" (человек)
        Record
            Growth : Float := 55.0; --Рост
            Age : Integer := 1;     --Возраст
            Weight : Float := 3.6;  --Вес
        end Record; 
 
    procedure Show_Person(Per : in Person); --Вывод информации о человеке
end Pack_Person;

Здесь мы имеем тип записи (шаблон)  Person (человек), которому присущи рост, возраст, вес (на самом деле вместо записи можно создать любой другой тип). Кроме того, для этого типа объявлена процедура (действие, которое может совершать переменная этого типа), в данном случае она просто будет выводить информацию на экран.

На данном этапе работа с типом Person будет выглядеть примерно так:

with Pack_Person; use Pack_Person;
 
procedure main is
    --Создание объекта Man и его инициализация
    Man : Pack_Person.Person := (177.0, 17, 67.0);
begin
...
    Show_Person(Man); --Вывод информации об объекте
...
end main;

С помощью этого типа мы уже можем создать много производных типов. Но все они будут обладать одинаковым набором полей (внутренних переменных) и методов (подпрограмм), а это не очень хорошо, т.к. все люди разные. Для того, чтобы на основе какого-либо типа создать новый тип, переопределить операции и добавить какие-то свои характеристики (поля) в Аде введено понятие теговых типов - tagged. Теговые типы уже можно наследовать и дополнять. Кроме того, для переменных (объектов) тегового типа можно использовать точечную нотацию. Таким образом, если тип Person объявить как

type Person is tagged
    Record
        ...
    end Record;

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

with Pack_Person; use Pack_Person;
 
procedure main is
    Man : Person := (Growth => 177.0, Age =>; 17, Weight => 67.0)
begin
     Man.Show_Person;
end main;

Ну а создание на основе этого типа нового типа (наследование) происходит так:

type My_Person is new Person with
Record
    --Переменные Growth, Age и Weight в этом типе создаются автоматически
    --(как у "Родителя"). Также можно добавить свои переменные
    Eye_Color : String(1..10); --Цвет глаз
    Eye_Color_Len : Integer;   --Длина строки (цвета глаз)
    --Здесь же можно создать свои подпрограммы или переопределить подпрограммы
    --родительского типа:
    procedure Show_Person(Per : My_Person);
end Record;

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

Рассмотрим пример наследования, дополнения родительского типа и переопределения (перегрузки) подпрограммы. Программу я для наглядности разобью на несколько пакетов, каждый пакет будет помещён в свой файл:

Пакет Pack_Person, определяем родительский тип.

pack_person.ads:

package Pack_Person is
    type Person is tagged
        Record
            Age : Integer;
        end Record; 
 
     procedure Show_Person(Per : in Person); --Вывод информации о человеке
end Pack_Person;

pack_person.adb:

With Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
 
package body Pack_Person is
 
    --Вывод на экран
    procedure Show_Person(Per : Person) is
    begin
        Put("Возраст = ");
        Put(Item => Per.Age, Width => 0);
        New_Line;
    end Show_Person;
 
begin
    NULL;
end Pack_Person;

Пакет Pack_Child_Person, определяем тип-наследник.

pack_child_person.ads:

with Pack_Person; use Pack_Person;
 
package Pack_Child_Person is
    --Тип-наследник
    type Child_Person is new Person with
        Record
            --Переменная Age наследуется от родителя
            -- + определим дополнительные переменные
            Eye_Color : String(1..10);
            Eye_Color_Len : Integer;
    end Record;
    --Переопределим (перегрузим) процедуру Show_Person для того, чтобы вывести
    --на экран Eye_Color
    procedure Show_Person(Per : Child_Person);
 
end Pack_Child_Person;

pack_child_person.adb:

with Pack_Person; use Pack_Person;
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
 
package body Pack_Child_Person is
    procedure Show_Person(Per : Child_Person) is
    begin
        New_Line;
        Put("Возраст = ");
        Put(Item => Per.Age, Width => 0);
        New_Line;
        Put_Line("Цвет глаз - " & Per.Eye_Color(1..Per.Eye_Color_Len));
    end Show_Person;
 
begin
 NULL;
end Pack_Child_Person;

Основной файл: main.adb

with Pack_Person; use Pack_Person;
with Pack_Child_Person; use Pack_Child_Person;
 
procedure main is
    --Переменная родительского типа
    Man : Person := (Age => 17);
    --Переменная типа-наследник
    ppp : Child_Person := (Age => 20, Eye_Color => "Red       ", Eye_Color_Len => 3);
begin
    --Вызов процедуры Show_Person родительского типа
    Show_Person(Man); --Можно и так: Man.Show_Person;
    Show_Person(ppp); --Вызов процедуры Show_Person типа-наследника (процедура
                     --переопределена для типа My_Person).
                     --Можно вызвать так: ppp.Show_Person;
end main;

Понятие "класс" в Аде отличается от трактовки этого термина в других языках программирования. Пусть у нас есть теговый тип T. Тогда у этого типа существует связанный с ним тип T'Class, который представляет собой объединение всех типов, для которых тип T является родителем. В приведённом примере Person'Class является классом, включающим типы Person и Child_Person. Если создать наследника от типа Child_Person, например Children, то тогда Class'Child_Person будет содержать типы Child_Person и Children.

Класс удобно использовать для создания полиморфных подпрограмм. Полиморфизм - это способность подпрограммы обрабатывать переменные разных типов. Рассмотрим на примере, как это работает. Внесём изменения в пакет Pack_Person:

pack_person.ads:

package Pack_Person is
    type Person is tagged
        Record
            Age : Integer := 1;
        end Record;
 
    procedure Show_Person(Per : in Person); --Вывод информации о человеке
 
    --Полиморфная подпрограмма принимает переменную из иерархии класса Person
    procedure Display(T : in Person'Class);
end Pack_Person;

pack_person.adb:

With Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
 
package body Pack_Person is
 
    --Вывод на экран
    procedure Show_Person(Per : Person) is
    begin
        Put("Возраст = ");
        Put(Item => Per.Age, Width => 0);
        New_Line;
    end Show_Person;
 
    --Полиморфная подпрограмма принимает переменную из иерархии класса Person
    procedure Display(T : in Person'Class) is
    begin
        --Далее происходит т.н. динамическая диспетчеризация. Т.е. компилятор
        --сам определяет, к какому типу относится переменная T, и вызывает
        --процедуру Show_Person из соответствующего пакета
        Show_Person(T);
    end Display;
 
begin
    NULL;
end Pack_Person;

Пакет Pack_Child_Person остаётся без изменений. В main.adb вызов Display будет осуществляться следующим образом:

with Pack_Person; use Pack_Person;
with Pack_Child_Person; use Pack_Child_Person;
 
procedure main is
    --Переменная родительского типа
    Man : Person := (Age => 17);
    --Переменная типа-наследник
    ppp : Child_Person := (Age => 20, Eye_Color => "Red ", Eye_Color_Len => 3);
begin
    Man.Display; --Или Display(Man). Вызов процедуры Show_Person родительского типа
    ppp.Display; --Или Display(ppp). Вызов процедуры Show_Person типа-наследника
                  --(процедура переопределена для типа My_Person)
end main;

Таким образом, в процедуру Display передаются объекты разных типов (Person и Child_Person), компилятор корректно их обрабатывает и вызывает нужную подпрограмму. Добавлю, что переменная тегового типа имеет скрытый тег, который и сообщает подпрограмме конкретный тип переменной.

Сразу замечание: если в пакетах Pack_Person и Pack_Child_Person процедуру Show_Person() объявить с использованием класса: procedure Show_Person(T : Person'Class), и в файле main.adb попробовать вызывать эту подпрограмму: Show_Person(Man) и Show_Person(ppp), то компилятор ругнётся на неоднозначность (ambiguous) и выдаст ошибку. Связано это с тем, что подпрограмма, в параметрах которой есть класс, должна быть определена только один раз - для типа-родителя (т.е. она не может быть быть переопределена (перегружена)).

Рассмотрим, что такое инкапсуляция. Официально этот термин трактуется, как сокрытие, прятание, скрывание. Я бы трактовал его как закрытие доступа.

В рассмотренном выше примере мы можем изменять типы как нам угодно. Однако бывают ситуации, когда какой-то тип (родитель или наследник) создаётся раз и навсегда и автор не предусматривает возможность изменения уже определённых им характеристик. Для этого тип определяется как приватный (private). Например:

--Закрываем родительский тип:
type Person is tagged private;
 
--Закрываем тип-наследник
type My_Person is new Person with private;

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

with Pack_Person; use Pack_Person; --Пакет, содержащий тип-родитель
 
package Pack_Child_Person is
    --Приватный тип-наследник
    type Child_Person is new Person with private;
 
    procedure Show_Person(Per : Child_Person);
 
    --Подпрограмма для инициализации переменной. Нужна, так как тип приватный и,
    --следовательно, прямого доступа к переменным (с помощью точечной нотации
    --или агрегатов, как в предыдущих примерах) нет:
    procedure Set_Person(Per : out Child_Person; Age : Integer; Color : in String; Len : in Integer);
 
private
    --Полное описание приватного типа
    type Child_Person is new Person with
        Record
            --Переменная Aga наследуется от типа Person
            Eye_Color : String(1..10) := "blue ";
            Eye_Color_Len : Integer range 1..10 := 4;
        end Record;
 
end Pack_Child_Person;

Замечание: буржуйский интернет пестрит вопросом: как бороться с ошибкой no selector "Age" for type "Child_Person" defined at pack_child_person.ads:12. Половина отвечающих ссылаются на недоработку компилятора, вторая половина предлагает использовать какие-то свои пакеты. Короче, ничего конкретного я не нашёл. На самом деле тут всё штатно: тип объявлен как приватный => все поля этого типа становятся приватными и доступ к ним ограничен. Что это значит?

В нашем примере, если РОДИТЕЛЬСКИЙ тип Person объявить как ПРИВАТНЫЙ (type Person is tagged private;), то переменная (поле) Age, входящая в состав записи (типа Person), тоже становится приватной. И для доступа к Age нужно предусмотреть соответствующие подпрограммы (например, Set и Get - установить и прочитать). Если попытаться из переменной типа Child_Person (ppp) получить прямой доступ к полю Age (например, ppp.Age := N;), то возникнет указанная выше ошибка.

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

Рассмотрим наш пример с использованием приватных типов:

pack_person.ads:

package Pack_Person is
 
    type Person is tagged private; --Приватный тип-родитель
 
    procedure Show_Person(Per : in Person); --Вывод информации о человеке
    --Первоначальная установка параметров:
    procedure Set_Person(Per : out Person; Age : in Integer);
    --Возвращает приватное поле Age:
    function Get_Person(Per : Person) return Integer;
    --Вывод информации на экран с помощью класса:
    procedure Display(Per : in Person'Class);
 
private
    type Person is tagged
        Record
            --Так как тип приватный, то поле Age тоже становится приватным:
            Age : Integer;
        end Record;
 
end Pack_Person;

pack_person.adb:

With Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
 
package body Pack_Person is
 
    --Первоначальное заполнение объекта
    procedure Set_Person(Per : out Person; Age : in Integer) is
    begin
        Per.Age := Age;
    end;
 
    --Возвращает приватное поле Age
    function Get_Person(Per : Person) return Integer is
    begin
        return Per.Age;
    end Get_Person;
 
    --Вывод на экран
    procedure Show_Person(Per : Person) is
    begin
        Put("Возраст = ");
        Put(Item => Per.Age, Width => 0);
        New_Line;
    end Show_Person;
 
    procedure Display(Per : Person'Class) is
    begin
        Show_Person(Per);
    end Display;
 
begin
    NULL;
end Pack_Person;

pack_child_person.ads:

with Pack_Person; use Pack_Person;
 
package Pack_Child_Person is
    --Приватный тип-наследник
    type Child_Person is new Person with private;
 
    procedure Show_Person(Per : Child_Person);
    procedure Set_Person(Per : out Child_Person; Age : Integer; Color : in String; Len : in Integer);
 
private
 
    type Child_Person is new Person with
        Record
           --Поле Age наследуется от типа Person, объявленном в пакете
           --Pack_Person как приватный и, следовательно, тоже является приватным
           Eye_Color : String(1..10) := "blue ";
           Eye_Color_Len : Integer range 1..10 := 4;
        end Record;
 
end Pack_Child_Person;

pack_child_person.adb

with Pack_Person; use Pack_Person;
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
 
package body Pack_Child_Person is
 
    procedure Set_Person(Per : out Child_Person; Age : Integer; Color : in String; Len : in Integer) is
    begin
        --Для инициализации поля Age (приватного), объявленного в приватном
        --родительском типе, используется процедура Set_Person().
        --Прямого доступа вида Per.Age := Age нет:
        Per.Set_Person(Age);
        Per.Eye_Color := Color;
        Per.Eye_Color_Len := Len;
    end Set_Person;
 
    procedure Show_Person(Per : Child_Person) is
    begin
        New_Line;
        Put("Возраст = ");
        --Для считывания поля Age, объявленного в приватном родительском типе
        --используется функция Get_Person(). Прямого доступа вида Per.Age нет:
        Put(Item => Get_Person(Per), Width => 0);
        New_Line;
        Put_Line("Цвет глаз - " & Per.Eye_Color(1..Per.Eye_Color_Len));
    end Show_Person;
 
begin
    NULL;
end Pack_Child_Person;

main.adb:

with Pack_Person; use Pack_Person;
with Pack_Child_Person; use Pack_Child_Person;
 
procedure main is
    Man : Person;
    ppp : Child_Person;
begin
    --Так как тип объявлен как приватный, то инициализировать переменную этого типа
    --при объявлении не получится. Поэтому я использую для этого специальную
    --подпрограмму:
    Set_Person(Man, 17);
    Man.Display;
 
    --Так как тип объявлен как приватный, то инициализировать переменную этого типа
    --при объявлении не получится. Поэтому я использую для этого специальную
    --подпрограмму:
    Set_Person(ppp, 20, "dark      ", 4);
    ppp.Display;
end main;

Кстати, если в программе для переменной типа наследника нужно вызвать подпрограмму родительского типа, то можно использовать приведение типа. Например:

Show_Person(Person(ppp));

Переменная ppp типа Child_Person приводится к родительскому типу Person и, следовательно, будет вызвана подпрограмма Show_Person, определённая для родительского типа Person.

Иногда нужно создать тип, который не имеет полей, но имеет методы. Для создания такого типа есть два способа:

type Parent is tagged
    Record
        null;
    end Record;

и

type Parent is tagged null Record;

Если тип-наследник такого типа тоже не будет иметь никаких полей, то он объявляется так:

type Child is new Parent with null Record;

Объявление подпрограмм для таких типов штатное.

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

type Empty_Parent is abstract tagged null Record;
 
type Simple_Parent is abstract tagged
    Record
        Simple_Field : Integer ;
    end Record ;

Переменную (объект) абстрактного типа создать нельзя!

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

package Sets is
    type Set is abstract tagged null record;
    function Empty return Set is abstract;
    function Empty(Element : Set) return Boolean is abstract;
    function Union(Left, Right : Set) return Set is abstract;
    function Intersection(Left, Right : Set) return Set is abstract;
    procedure Insert(Element : Natural; Into : Set) is abstract;
end Sets;

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

2 comments on “Теговые типы. Объектно-ориентированное программирование

  1. Спасибо за полезную и интересную информацию!

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

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