Недавно мне довелось внести небольшое изменение в работу одного из компонентов большой агентной системы. В которой существуют две категории компонентов: самостоятельные (имеющие собственные адреса) и заплатки (не имеющие собственных адресов). Заплатки играют роль фильтров -- они располагаются между самостоятельными компонентами и либо модифицируют, либо отбраковывают, либо задерживают, циркулирующие между самостоятельными компонентами сообщения.
Итак, были самостоятельные компоненты S и M, между которыми располагалась заплатка B. Мне потребовалось, чтобы при своем рестарте заплатка B отсылала компоненту S сообщение query_state, на которое компонент S должен был ответить сообщением current_state. Но, любое сообщение должно иметь как адрес получателя (в данном случае адрес компонента S), так и адрес отправителя. Когда сообщениями между собой обмениваются компоненты S и M проблем нет -- у каждого из них есть собственный адрес. Поэтому, когда M отправляет S сообщение query_state, то S знает, кому нужно отослать ответ. А у заплатки B нет собственного адреса. Заплатка знает только адрес компонента S. И перехватывает сообщения, которые либо отсылаются на адрес S, либо отправлены с адреса S. Поэтому B легко может отослать query_state на адрес S. Но что подставлять в качестве адреса отправителя?
Первым решением напрашивалось добавление в конфигурацию B адреса M. Тогда бы B мог отослать query_state от имени M. Но это решение было не самым лучшим. Во-первых, нарушилась бы совместимость между версиями B -- добавился бы обязательный параметр при конфигурировании. Во-вторых, снизилась бы гибкость использования B: ранее можно было менять взаимосвязи между S и M, не затрагивая B. Теперь, если бы пришлось заменить M на N, то потребовалось перенастроить не только S, но и B.
К счастью выяснилось, что S при получении query_state всегда отсылает current_state не отправителю query_state, а компоненту M (точнее, тому компоненту, адрес которого прописан в конфигурации S, обычно это компонент M). Поэтому вопрос о том, с какого адреса B должен отсылать query_state отпал сам собой -- это оказалось совершенно неважно.
Я уже не помню, по каким причинам S всегда отсылает current_state на определенный в конфигурации адрес. Очень похоже, что это явилось деталью реализации S: сообщение current_state отсылается в нескольких случаях, и везде используется один и тот же фрагмент кода, формирующий и отсылающий current_state беря адрес получателя сообщения из конфигурации. Забавно, что подобным образом поступают и еще два компонента, E и G, играющие подобную S роль, и разработанных после S. Вероятно, при создании E и G логика работы S использовалась в качестве образца.
Таким образом, в модифицированной версии B оказалось заложена зависимость от текущей логики работы компонента S (а так же E и G, с которыми B так же может использоваться). Если в дальнейшем логика S будет изменена так, что S будет отсылать current_state именно тому, кто отослал query_state, то B окажется неработоспособным.
Что может помочь выявить такое нарушающее совместимость изменение логики работы S? Пока только тесты. Но с тестами не все просто, поскольку именно такое взаимодействие S и B вряд ли возможно проверить с помощью unit-тестов. Для такой проверки необходима организация тестового стенда (с различными способами размещения S и B) и проведение тестирования на этом стенде. Понятно, что в некоторых случаях такое тестирование просто не будет организовано. Например, когда в S будет обнаружен какой-то критический баг, который придется править в очень сжатые сроки и сразу же запускать исправленную версию S в эксплуатацию. Хорошо бы, чтобы такого никогда не происходило, но раз в пару лет такие случаи все-таки возникают.
Гораздо лучше было бы, если бы изменение логики S привело к тому, что измененная версия S не могла бы даже быть скомпилирована. И вот вопрос, на который я пока не имею ответа: можно ли сделать так, чтобы компилятор контролировал изменение логики работы компонента, и не позволял бы скомпилировать измененную версию компонента, если она нарушает ожидания остальных компонентов?
Тут сразу же напрашивается аналогия с контрактами. Отсылка current_state в ответ на query_state на фиксированный адрес -- это текущий контракт S. Однако, как этот контракт выразить в программном коде (например, на C++)? Скажем, я знаю, как выглядят контракты в Eiffel. Но, в Eiffel контракты привязываются к прототипам синхронных методов (а мне нужен прием и отсылка сообщений), да и выполнение контрактов проверяется во время исполнения программы (а мне хотелось бы во время компиляции). Поэтому вопрос о том, как можно сделать систему контрактов для агентных систем, в которых агенты обмениваются асинхронными сообщениями, для меня пока остается открытым...