Механизм Signal-Slot
Приятная и полезная концепция, приходящая на замену классической концепции подпрограмм обратного вызова. Основные отличия:
- сигналы и слоты есть части типа, наравне с полями записи и примитивными подпрограммами;
- отсуствует понятие "данных пользователя", задаваемых при выполнении соединения;
- неизвестен (теоретически) объект-отправитель;
- может использоваться в для межзадачного взаимодействия в событийно управляемых программах.
Не все перечисленные выше требования легко решить средствами Ada.
Вариант 1
API библиотеки поддержки
Библиотека поддержки состоит из нескольких основных пользовательских пакетов. Корневой пакет определяет базовый тип, способный взаимодействовать с механизмом сигналов/слотов. Все пользовательские типы должны быть порождены от него.
package Objects is type Abstract_Object is abstract new Ada.Finalization.Limited_Controlled with private; private ... end Objects;
Дочерние пакеты предоставляют возможность создания сигналов с определённым набором параметров и типом параметров. Они выполнены в виде настраиваемых пакетов, за исключением одного, определяющего сигналы без параметров:
package Objects.Signals is ---------------------- -- Signal_Connector -- ---------------------- type Signal_Connector (<>) is limited private; generic type T is abstract new Abstract_Object with private; package Generic_Connectors is procedure Connect (Self : Signal_Connector; Object : not null access T'Class; Method : not null access procedure (Self : not null access T)); end Generic_Connectors; ------------ -- Signal -- ------------ type Signal (Object : not null access Abstract_Object'Class) is tagged limited private; not overriding procedure Emit (Self : Signal); not overriding function Connector (Self : in out Signal) return Signal_Connector; private ... end Objects.Signals;
Тип Signal_Connector предназначен для выполнения операций подключения/отключения соединений. Тип Signal предназначен для объявления поля записи, выполяющего обработку сигналов.
Настраиваемый пакет Generic_Connectors предназначен для настройки на конкретный тип подключаемого объекта и содержит подпрограммы управления соединениями.
Пример использования
В качестве примера определим два тэговых типа Emitter и Collector, первый будет возбуждать сигнал, а второй его получать.
Код спецификации и реализации первого приведён ниже.
with Objects.Signals; package Emitters is type Emitter is new Objects.Abstract_Object with private; function Signal (Self : not null access Emitter) return Objects.Signals.Signal_Connector; procedure Test (Self : not null access Emitter); private type Emitter is new Objects.Abstract_Object with record Signal : aliased Objects.Signals.Signal (Emitter'Access); end record; end Emitters;
package body Emitters is ------------ -- Signal -- ------------ function Signal (Self : not null access Emitter) return Objects.Signals.Signal_Connector is begin return Self.Signal.Connector; end Signal; ---------- -- Test -- ---------- procedure Test (Self : not null access Emitter) is begin Self.Signal.Emit; end Test; end Emitters;
Для типа объявлена подпрограмма Signal, возвращающая Signal_Connector и позволяющая выполнять операции подключения/отключения.
Подпрограмма Test предназначена для демонстрации работы механизма и просто выполняет возбуждение сигнала.
Тип Collector, предназначенный для обработки полученного сигнала выглядит следующим образом:
with Objects; package Collectors is type Collector is new Objects.Abstract_Object with private; procedure Process (Self : not null access Collector); private type Collector is new Objects.Abstract_Object with null record; end Collectors;
with Ada.Text_IO; package body Collectors is procedure Process (Self : not null access Collector) is begin Ada.Text_IO.Put_Line ("Processing"); end Process; end Collectors;
Для выполнения подключения необходимо так же выполнить настройку пакета Generic_Connectors:
with Objects.Signals; package Collectors.Connections is new Objects.Signals.Generic_Connectors (Collector);
Главная подпрограмм выглядит следующим образом:
with Collectors.Connections; with Emitters; procedure Main is C : aliased Collectors.Collector; E : aliased Emitters.Emitter; begin Collectors.Connections.Connect (E.Signal, C'Access, Collectors.Process'Access); E.Test; end Main;
Выводы
Выглядит вполне приемлемо. Однако, слишком много необходимо сделать для выполнения подключения. Одна из возможных идей для рассмотрения - настройка специального пакета, принимающего тип и его подпрограмму, а уже потом использование подпрограммы Connect из него.
Вариант 1.1
Этот вариант является модификацией предыдущего в части объявления и подключения слотов. Настраиваемый пакет Generic_Slot в Objects.Signals настраивается на тэговый тип и подпрограмму, играющую роль слота и предоставляет подпрогаммы подключения/отключения:
API поддержки
package Objects.Signals is ---------------------- -- Signal_Connector -- ---------------------- type Signal_Connector (<>) is limited private; generic type T is abstract new Abstract_Object with private; with procedure Method (Self : not null access T) is abstract; package Generic_Slot is procedure Connect (Self : Signal_Connector; Object : not null access T'Class); end Generic_Slot; ------------ -- Signal -- ------------ type Signal (Object : not null access Abstract_Object'Class) is tagged limited private; not overriding procedure Emit (Self : Signal); not overriding function Connector (Self : in out Signal) return Signal_Connector; private ... end Objects.Signals;
Если сигналы/слоты имеют параметры, то используется настраиваемый пакет:
with League.Values; generic type T_1 is private; with function To_Value (Item : T_1) return League.Values.Value; with function From_Value (Item : League.Values.Value) return T_1; package Objects.Generic_Signals_1 is ---------------------- -- Signal_Connector -- ---------------------- type Signal_Connector (<>) is limited private; generic type T is abstract new Abstract_Object with private; with procedure Method (Self : not null access T; Parameter_1 : T_1) is abstract; package Generic_Slot is procedure Connect (Self : Signal_Connector; Object : not null access T'Class); end Generic_Slot; ------------ -- Signal -- ------------ type Signal (Object : not null access Abstract_Object'Class) is tagged limited private; not overriding procedure Emit (Self : Signal; Parameter_1 : T_1); not overriding function Connector (Self : in out Signal) return Signal_Connector; private ... end Objects.Generic_Signals_1;
Пример использования
По сравнения с предыдущим вариантом изменяется способ использования только для типов, содержащих слоты. Для каждого слота объявляется собственный дочерний пакет, содержащий настройку пакета Objects.Signals.Generic_Slot:
with Objects.Signals; package Collectors.Process_Slot is new Objects.Signals.Generic_Slots (Collector, Process);
Главная программа выглядит следующим образом:
with Collectors.Process_Slot; with Emitters; procedure Main is C : aliased Collectors.Collector; E : aliased Emitters.Emitter; begin Collectors.Process_Slot.Connect (E.Signal, C'Access); E.Test; end Main;
Выводы
Больше писанины, поскольку необходимо объявлять отдельный пакет для каждого слота, но (1) меньше усилий на выполнение подключения и лучше понимание происходящего, и (2) жестко контролируется, что слот есть примитивная операция типа.