Q. Почему в SObjectizer для именования сообщений, событий, агентов и состояний агентов используются строковые литералы? Не приводит ли это к ошибкам при программировании и проблемам при сопровождении?
A. Чтобы ответить на этот вопрос, можно сначала рассмотреть, откуда пошло использование текстовых имен, в чем проблемы привязки к именам типов для статического контроля, некоторые достоинства текстовых имен.
Первоначально (еще в до-SObjectizer-овские времена) текстовые имена (в основном сообщений и состояний) нужны были для того, чтобы SObjectizer-приложением можно было управлять снаружи. Предполагалось, что с помощью подобного инструмента будут создаваться черные ящики, не имеющие средств диалога с пользователем. Но как-то же этими черными ящиками нужно было управлять. Причем хотелось бы, чтобы диалоговые программы для управления можно было писать не только на C++, но и на, к примеру, Visual Basic-е, Java или Python-е. Собственно, в прародителе SObjectizer-а (SCADA Objectizer) это было проделано. Был тривиальный тектовый протокол, названный SOP, который позволял отсылать в черный ящик сообщения, команды на смену состояния агента или запросы на получение состояния агента. Java-клиент с GUI интерфейсом подключался к черному ящику и мог с помощью SOP управлять черным ящиком.
Это показалось настолько удобно, что в SObjectizer было решено оставить использование текстовых имен для всех основных понятий (классов агентов, имен агентов, имен сообщений, состояний, событий). С одной стороны, это нужно для того, чтобы SObjectizer приложения могли обмениваться данными между собой (через SOP). С другой стороны, это нужно для того, чтобы SObjectizer RunTime мог отслеживать такие вещи, как наследование классов агентов и подписку и выдавать внятные сообщения об ошибках.
Данная возможность широко используется в настоящее время. Например, для воздействия на SObjectizer-приложения во время отладки. Есть простенький инструментик, который позволяет отсылать по SOP команды в SObjectizer-приложения со стандартного ввода. Например, в отладке одного из проектов на основе SObjectizer используется такой SOP-файл (текстовый файл, содержащий SOP-команды) для выдачи команды на перезагрузку конфигурации компонента:
{send-msg {agent aag_3::smsc_map::default::a_router } {msg msg_configure } } send exit
Для исполнения этого SOP файла запускается утилита so_send_stdin:
so_send_stdin "какой-то IP" < reconfigure.sop
С помощью следующего Ruby-скрипта при отладке имитируется поступление 5000 нужных разработчику сообщений:
1.upto( 5000 ) do |i| puts <<-EOF {send-msg {agent aag_3::smpp_smsc::bserver.trx::a_channel } {msg msg_imit_delivery_receipt } {field m_short_message {string-stream "id:#{i} sub:001 dlvrd:001 submit date:0508251625 done date:0508251625 stat:DELIVRD err:000 text:AAG3 test"} } } EOF puts "send" if 0 == i % 10 end puts "exit"
Запускается этот скрипт вот таким образом:
ruby imit_delivery_receipt.rb | so_send_stdin "какой-то IP"
Так что было желание сохранить идентификацию основых понятий с помощью строк. Задача была в том, чтобы сократить количество строковых литералов в C++ коде и по максимому задействовать статическую типизацию. Где-то это удалось сделать. Например, при описании класса агента для SObjectizer строковых литералов нет (за исключением всего, что связано с описанием состояний). Имена классов, типов сообщений и имена событий должны быть корректными C++ идентификаторами. Например, попытка указать в SOL4_EVENT имя не существующего метода приведет к ошибке еще во время компиляции.
Но вот с именами сообщений и с моментом подписки все не так просто. Дело в том, что в C++ нет никакой рефлексии, поэтому возникает вопрос о том, как, например, связать текстовое имя сообщения (которое необходимо для SOP) с C++ идентификатором типа сообщения. Можно было бы попробовать воспользоваться RTTI для получения из имени типа текстовое сообщение. Но, в разных компиляторах RTTI строит разные текстовые имена для одного и того же типа. А в SObjectizer требовалось, чтобы по SOP можно было связать несколько процессов, скомпилированных разными компиляторам. Поэтому RTTI не подошел. Далее можно было бы рассмотреть способ вроде такого:
struct msg_hello_time { static const std::string so_msg_name = "msg_hello_time"; ... };
Но заставлять пользователя подобные конструкции не очень хорошо, т.к. слишком много писанины. Ну и не все компиляторы в свое время поддерживали описание static const в определении класса/структуры. Получалось, что попытка как-то связать имя типа с именем сообщения в C++ приведет к использованию макросов. Макросов же в SObjectizer и так не мало. Лишние без необходимости вводить не хотелось.
И еще один важный фактор. Один и тот же тип мог использоваться сообщений с разными именами. Это не так уж и редко делается на практике. Вот и не нашлось удобного способа использовать в коде программы C++ идентификаторы, который каким-то образом преобразовывал идентификатор (имя типа) в строку.
С идентификацией имен событий вообще плохо, т.к. событие -- это метод. А с идентификатором имени метода в C++ вообще не много чего можно сделать.
Так что получалось, что код мог выглядеть как-то так:
so_subscribe( SO4_EVENT_NAME( a_hello_t, evt_hello_time ), SO4_MSG_NAME( msg_hello_time ) );
но при разработке SObjectizer было решено подобный вид записи не использовать.
Но так ли плохи строковые идентификаторы? Вообще-то говоря, они дают возможность управлять подпиской в run-time. Если бы в C++ коде пришлось писать что-то вроде:
subscribe< msg_hello_time >( &a_hello_t::evt_hello_time );
то это намертво бы закрепило связь между сообщением msg_hello_time и evt_hello_time. Но как связать evt_hello_time с другим сообщением в run-time? Подобная статическая связанность была в последней версии SCADA Objectizer и являлась чуть ли не самым большим недостатком, т.к. на практике требовалось динамически менять подписку, причем имя сообщения (а иногда и события) могло стать известно только в run-time.
Использование же тестовых идентификаторов подобную свободу предоставляет. Например, после своего старта агент может прочитать какой-нибудь конфиг и узнать, на сообщения каких агентов ему следует подписать свои события. Либо агент может среагировать на какое-то событие, узнать о появлении нового агента и подписаться на его сообщения (так, например, делает агент-коммуникатор при появлении нового глобального агента). Более того, подобную подписку может делать не только сам агент, но кто-либо другой, какой-нибудь конфигуратор, даже не являющийся агентом.
Теперь о страхах и вероятности все запортить связав не то не с тем. Очевидно, что SObjectizer позволяет наломать дров. Но в SObjectizer предпринимаются некоторые шаги для повышения безопасности. Во-первых, использование макроса SO4_EVENT_STC(evt_name, msg_name) указывает SObjectizer, что evt_name будет получать только сообщения типа msg_name. И эта информация позволяет в so_subscribe проверить типа сообщения на которое подписывается сообщение. И, если у сообщения другой тип, то подписка не выполняется. Однако, в момент отсылки сообщения SObjectizer не в состоянии проверить тип указателя, передаваемый в send_msg(). Поэтому, если пользователь отослал агенту сообщение не того типа, то SObjectizer помочь здесь не сможет. Ну это обычная ситуация в C++ приложениях при передаче информации через указатель на void. Если же попробовать отказаться от указателя на void, то тогда агент лишается возможности подписать свое событие на несколько разнотипых сообщений. А иногда это нужно. Например, агенту-коммуникатору. Или прикладному агенту, которому не столь важны данные в сообщении, сколько сам факт наличия сообщения.
Во-вторых, при подписке события SObjectizer проверяет наличие в словаре имен событий и сообщений. Поэтому попытка подписать несуществующее сообщение или несуществующее событие завершается возникновением ошибки. А поскольку в большинстве случаев подписка выполняется в одном централизованном методе so_on_subscription(), то ошибки подписки сразу становятся заметны.
Ну и что получается на практике. Самой распространенной проблемой с подпиской события является отсутствие подписки события. Т.е. элементарно забывают пописать событие. Так что проблемы с тем, что что-то не на то подписали, на практике практически не встречалась.
Так же нет проблем с сопровождением строковых литералов. Этому способствует использование нотации: префикс evt_ для имен событий и msg_ для имен сообщений. Поэтому такие распространенные имена, как status в качестве имен событий/сообщений практически не встречаются. Если же возникает необходимость сменить имя события, например, с evt_hello_time на evt_yet_another_hello, то в коде все равно следует искать подстроку evt_hello_time, а затем уже на месте смотреть, нужно ли это вхождение или следует искать дальше. И здесь уже без разницы, является ли найденная подстрока частью идентификатора или строкового литерала. То, что компилятор не сможет проверить корректность имен событий/сообщений -- это не приятно. Но выручают проверки, выполняемые самим SObjectizer. Так что на практике проблем с сопровождением строковых литералов не было.