Сигналы и слоты — подход, используемый в некоторых языках программирования и библиотеках (например, Boost и Qt) который позволяет реализовать шаблон «наблюдатель», минимизируя написание повторяющегося кода. Концепция заключается в том, что компонент (часто виджет) может посылать сигналы, содержащие информацию о событии (например: был выделен текст «слово», была открыта вторая вкладка). В свою очередь другие компоненты могут принимать эти сигналы посредством специальных функций — слотов. Система сигналов и слотов хорошо подходит для описания Графического интерфейса пользователя. Также механизм сигналов/слотов может быть применён для асинхронного ввода-вывода (включая сокеты, pipe, устройства с последовательным интерфейсом, др.) или уведомления о событиях. В библиотеке Qt благодаря Метаобъектному компилятору (англ.)русск. отпадает необходимость писать код регистрации/дерегистрации/вызова, так как эти шаблонные участки кода генерируются автоматически.Говорит нам Википедия.
В php приложениях нет никакого event loop. Получили запрос, отдали ответ. И все тут. Однако между «получили запрос» и «отдали ответ» есть какой-то жизненный цикл приложения, соответственно не все потеряно и можно попробовать применить этот механизм в веб приложениях. В студенчестве я программировал на C++/Qt и его реализация сигналов и слотов очень тогда нравилась. Отличная ведь идея. Ближайший родственник — шаблон «Наблюдатель». Цель одна, однако реализации совершенно разные. Пришла мне в голову мысль реализовать данный механизм для php в виде небольшой библиотеки.
Сигналы и слоты
Сигнал — это нечто, что может сообщить внешнему Миру о внутреннем состоянии объекта. Телефон может звонить, кот может «мяукать». Звонки и «мяу» являются сигналами телефона и кота соответственно. Они сообщают нам об изменении внутреннего состояния объектов: телефон из состояния покоя перешел в состояние «Вам звонят», кот из состояния «сытый кот» перешел в состояние «голодный кот».
Вы можете реагировать на эти сигналы каким либо способом. Например: снять трубку или накормить наконец-таки своего кота. Ваши реакции на сигналы — это слоты.
В программировании возникают точно такие же ситуации, когда нам нужно реагировать на изменение состояния какого-либо объекта. Для этого и используется механизм сигналов и слотов — для обеспечения коммуникации между объектами. На мой взгляд, в отличии от шаблона «Наблюдатель» он проще и намного прозрачнее в использовании (хотя «Наблюдатель» сам по себе тоже очень простой шаблон).
Немного поразмыслив, я написал библиотеку connector для php, реализующую механизм сигналов и слотов. Посмотрим на нее?
Установка
Connector доступен как composer пакет, соответственно для установки необходимо выполнить
composer require fluffy/connector
или добавить зависимость на библиотеку в Ваш composer.json файл
"require": {
...
"fluffy/connector": "^1.0"
}
и выполнить
composer update
Использование
1. Сигналы
Если мы хотим, чтобы объект мог испускать сигналы, то нам нужно имплементировать интерфейс
SignalInterface
и использовать соответствующий трейт SignalTrait
. Например, у нас есть логер класс и мы хотим, чтобы он высылал сигнал somethingIsLogged
всякий раз, когда он заканчивает логирование:<?php
/**
* @file
* Contains definition of Logger class.
*/
use Fluffy\Connector\Signal\SignalInterface;
use Fluffy\Connector\Signal\SignalTrait;
/**
* Class Logger.
*/
class Logger implements SignalInterface {
use SignalTrait;
public function log()
{
// Do logging stuff.
...
// Emit signal about successfull logging.
$this->emit('somethingIsLogged', 'Some useful data');
}
}
Для того, чтобы выслать сигнал, необходимо вызвать метод
emit
и передать два параметра: имя сигнала и данные, которые будут переданы этим сигналом. Можно передавать любой тип данных: строку, число, массив или объект. Это все. Теперь объект логер умеет высылать сигналы во внешний мир. Но пока никто не подключен к этому сигналу, никто не будет знать о том, что этот объект сообщает о чем-то полезном. Давайте это исправим.2. Слоты
Слот — это обычный метод класса. Давайте создадим класс со слотом.
<?php
/**
* @file
* Contains definition of Receiver class.
*/
/**
* Class Receiver.
*/
class Receiver
{
public function slotReactOnSignal($dataFromSignal) {
echo "Received data: $dataFromSignal";
}
}
3. Сигнально слотовые соединения
Итак, у нас есть
Logger
класс с сигналом и Receiver
класс со слотом. Для того, чтобы реагировать на сигнал, необходимо подключить к нему слот.use Fluffy\Connector\ConnectionManager;
$logger = new Logger();
$receiver = new Receiver();
ConnectionManager::connect($logger, 'somethingIsLogged', $receiver, 'slotReactOnSignal');
$logger->log();
Вызвав
ConnectionManager::connect($sender, $signalName, $receiver, $slotName)
мы тем самым подключили сигнал объекта логера к слоту объекта получателя. Это значит, что всякий раз при вызове $logger->log()
будет высылаться сигнал логера somethingIsLogged
, на который будет реагировать слот slotReactOnSignal
объекта получателя. Мы можем определить столько сигнально слотовых соединений, сколько нам нужно. Работают все возможные виды соединений:- Один сигнал к одному слоту
- Один сигнал к нескольким слотам
- Несколько сигналов к нескольким слотам
- Несколько сигналов к одному слоту
При чем неважно, будь то сигналы от одного объекта или от разных, будь то слоты одного объекта или разных.
4. Типы соединений
По умолчанию метод
ConnectionManager::connect()
создает перманентное соединение. Это значит, что соединение не будет разорвано после первой высылки сигнала. Однако есть возможность создать одноразовое соединение, передав пятым параметром константу — тип соединения. Например:use Fluffy\Connector\ConnectionManager;
$logger = new Logger();
$receiver = new Receiver();
ConnectionManager::connect($logger, 'somethingIsLogged', $receiver, 'slotReactOnSignal', ConnectionManager::CONNECTION_ONE_TIME);
$logger->log();
// Log once again.
$logger->log();
После второго вызова
Logger::log()
ничего не произойдет так как слот будет отключен от сигнала после первой его высылки.5. Disconnect
Если мы не хотим больше слушать определенный сигнал, то просто отключаемся от него
ConnectionManager::disconnect($logger, 'somethingIsLogged', $receiver, 'slotReactOnSignal');
Если нам необходимо сбросить все существующие соединения, то нужно вызвать
ConnectionManager::resetAllConnections()
Для чего это нужно?
- Мне просто нравится механизм сигналов и слотов и его реализация в Qt. Возникло желание реализовать нечто подобное для php.
- Это удобнее чем «Наблюдатель».
Репозиторий проекта: fluffy/connector
Надеюсь, было интересно дойти до этой строки. Критика и мысли в слух приветствуются.
UPD
- Как заметил symbix использование наследования для реализации функционала сигнала не удобно. Учтено. Теперь реализовано на трейте (см. пункт 1)
- Как заметил oxidmod не имеет особого смысла ограничивать именование слотов (ключевое слово «slot»). Теперь слотом может быть метод с любым именем (см. пункт 2)
Комментарии (35)
symbix
07.07.2016 03:52+3Мне видится несколько неудобным abstract class Signal. Я бы еще понял, если MeowSignal extends Signal, а с предлагаемым способом использования это выглядит странно: логгер в примере вынужденно «является» сингалом (is-a relation), хотя по сути то это не так; да и что делать, если у меня уже есть иерархия наследования? Может, в Qt оно так и сделано (хотя не знаю, не видел), но в С++ есть множественное наследование ведь.
Думаю, было бы уместнее либо трейт, либо Signal::emit() (либо оба варианта).FluffyMan
07.07.2016 03:56+1Действительно. Если иерархия уже имеется, то абстрактный класс не очень удобен. С трейтом идея нравится.
Blumfontein
07.07.2016 07:20Тоже сразу при прочтении статьи обратил внимание, что трейт был бы уместнее.
FluffyMan
07.07.2016 04:00по сути Signal класс должен называться Signalable, тогда проблема с восприятием отойдет. Но так только интерфейсы принято именовать. Поэтому оставил Signal. Наверное, самый верный вариант в моем случае — это трейт.
И в Qt сделано не так, не надо плохо о нем думать)
Scrobot
07.07.2016 06:58+1ИМХО, механизм сигналов и слотов сам по себе хорош, но что касаемо данного примера… я не могу сказать, что меня это вдохновило, извините. Сигнатура
ConnectionManager::connect($logger, 'somethingIsLogged', $receiver, 'slotReactOnSignal');
Меня немного смущает… Довольно простой пример, и в нем уже такая простыня… а если задачи усложнятся по тем вариантам, которые вы привели?
- Один сигнал к нескольким слотам
- Несколько сигналов к нескольким слотам
- Несколько сигналов к одному слоту
И как мне установить в одном соединении несколько ресиверов? К примеру я создал 3 класса Receiver1. Receiver2, Receiver3 и заимлиментился от Receiveable который реализовал slotReactOnSignal(). Далее, я хочу сделать так:
ConnectionManager::connect($logger, 'somethingIsLogged', [$receiver1, $receiver2, $receiver3], 'slotReactOnSignal');
А если добавить в эту логику внутреннее общение между слотами…
Как мне видится, я полностью согласен с webmasterx все придет к тому, что нужен Rx.
Не хочу сказать, что она бесполезная, я просто не вижу область применений) А не можете дополнить статью еще какими-либо примерами? )) Чтобы ощутить полезность вашей библиотеки) Хотелось бы ее использовать, но где ее юзануть так, чтобы она была прям полезной…webmasterx
07.07.2016 07:28Эта сигнатура пришла исключительно из Qt и мне тоже не нравится. Но зато она знакома Qt кодерам. А для нескольких ресиверов нужно просто повторить
ConnectionManager::connect($logger, 'somethingIsLogged', $receiver, 'slotReactOnSignal');
И пременения я тоже не нашел. В Yii2 можно подключиться к событиям модели, думаю и к другим классам тоже, и в других фреймворках тоже найдется аналог.
Но в итоге если нужна событийная реализация — приходим к тому же Rx.
ИМХО: сигналы и слоты в Qt либо пережиток прошлого (не догадались до абстракции Rx), либо умышленное упрощение в сторону скорости.
FluffyMan
07.07.2016 08:31По поводу сигнатуры. Что в ней такого страшного? Вроде бы все логично и интуитивно понятно: сигнал от отправителя соединяется со слотом получателя.
По поводу соединений — посмотрите тесты. Как указал webmasterx будет несколько последовательных вызовов ConnectionManager::connect().
По поводу применения. Вначале статьи я писал, что сигналы и слоты отлично подходят для gui приложений с event loop. Там они смотрятся на своем месте. Что же касается реального применения в скриптовом php: сигалы и слоты можно использовать там, где подходит наблюдатель. Например выполнить действие перед сохранением сущности. Или же (о боже мой максимализм) переписать старый процедурный подход в приложении с хуков на сигналы.
Вообще, конечно же, just for fun. Узнал как пишутся composer пакеты.
oxidmod
07.07.2016 09:21+3а чем вам ивенты не угодили?
FluffyMan
08.07.2016 12:53Угодили. Спортивный интерес. Ваш вопрос можно задать и относительно к evenement/evenement
Borro
07.07.2016 09:38Есть отличная библиотека evenement/evenement с похожим функционалом и на трейтах.
oxidmod
07.07.2016 10:12или соотвествующая компонента symfony
http://symfony.com/doc/current/components/event_dispatcher/introduction.htmlFluffyMan
07.07.2016 11:36Это ведь реализация шаблона «Медиатор». Я вот только что осознал, что сигналы и слоты — больше «Медиатор», чем «Наблюдатель». Так как возможны соединения не только один к одному, но и многие ко многим.
FluffyMan
07.07.2016 11:29Спасибо, не видел. Подход почти такой же. С одним различием, что там классический «Наблюдатель», где наблюдаемый объект содержит в себе список всех наблюдателей. В сигналах и слотах маппинг объект.сигнал -> объект.слот не хранится в наблюдаемом объекте.
Очень похожа) И судя по всему очень популярна. Ну, хотя бы мой спортивный интерес удовлетворен.oxidmod
07.07.2016 11:36не уверен что правильно вас понял. маппинг объект.сигнал -> объект.слот хранится в диспетчере.
вы можете иметь столько диспетчеров сколько вам нужно. Допустим каждая подсистема может иметь свой диспетчер который связывает компоненты подсистемы + общий диспетчер всег оприложения, который связывает между собой подсистемы. При ленивой инициализации лишние диспетчеры/листенеры не будут созданы что улучшит общий перфоманс приложения.
зы. еще можно иметь немутабельный диспетчер, если хотите быть уверенным что никто туда не всунет лишнего и не отпишется от важных событий. Это может быть очень полезным при работе со сторонним кодомFluffyMan
07.07.2016 11:40Мой комментарий был по поводу evenement/evenement, которая является классической реализацией шаблона «Наблюдатель». В этой библиотеке все наблюдатели хранятся внутри наблюдаемого объекта, а не в диспетчере. Посмотрите реализацию.
Про диспетчер Вы, наверное, имели в виду symfony event dispatcher. Там согласен, маппинг объект.сигнал -> объект.слот хранится в диспетчере.FluffyMan
07.07.2016 11:45evenement/evenement все же не классическая реализация наблюдателя. все же наблюдатели привязываются к событию. но суть та же.
oxidmod
07.07.2016 11:30и еще впрос, почему метод слот должен начинаться с «ключевого» слова slot
в свете
ConnectionManager::connect($logger, 'somethingIsLogged', $receiver, 'slotReactOnSignal');
смысла в этом нет.
есть смысл это сделать, если название метода slotReactOnSignal генерируется автоматически из somethingIsLogged
типа slotOnSomethingIsLogged и автоматически вызывается или бросает исключение, если метод не определн
тогда
ConnectionManager::connect($logger, 'somethingIsLogged', $receiver, 'slotReactOnSignal');
можно заменить на
ConnectionManager::connect($logger, 'somethingIsLogged', $receiver);
или по крайней мере сделать аргумент опциональным, если по какойто причине нужно использовать не стандартно названный методFluffyMan
07.07.2016 11:33ответ простой — для наглядности. У Вас в классе может быть множество методов и лишь для того, чтобы среди этого множества выделить те методы, которые могут использоваться в качестве слотов, используется «ключевое» слово «slot». Не более. Конечно, технически в этом никакой необходимости нету.
IvanPanfilov
07.07.2016 12:35штука чисто для фанатов сигналов и слотов — для людей которые писали на Qt и хотят похожего поведения.
ну а для нормальных людей есть привычные «события» удобно использовать, читабельный код — просто и понятно
Mistx
07.07.2016 16:44Для PHP механизм signal-slot не требуется, т.к. отсутствует потоковая модель (все функции и методы в PHP блокирующие) и необходимость между этими потоками общаться. А если вы при помощи данного механизма желаете навешивать некоторые стандарные поведения на группы объектов, то лучше использовать одноименный шаблон Behavior (поведение) — значительно красивее и понятнее получится.
webmasterx
Мне кажется Rx библиотека более гибкая в использовании чем сигналы и слоты. Как реализовывать на сигналах ситуацию: doSomething() только после того, как получены два сигнала? Придется пилить свой велосипед, который опять таки станет Rx
FluffyMan
Никак и не реализовать такое поведение на сигналах. Хм. Можете привести реальный пример, когда такое поведение необходимо?
webmasterx
Например после двух запросов к бд (за новостями и комментариями к ним). по отдельности они нас не интересуют, а вот если вместе — то записать в лог/внести подозрительный ip в бд
webmasterx
И релизовать можно. Через посредника, который подпишется на оба сигнала, и при их получении выдаст третий, на который уже подпишется целевой слот. но опять таки, уж лучше Rx делать. Тем более что он поддерживает асинхронную работу.
FluffyMan
Верно. Поспешил я с ответом.