Концепты: 7. Описание протокола с состояниями

Apr 01, 2011 06:51

Задача: написать интерфейс взаимодействия между двумя процессам/потокам, которые работают по очереди и передают друг другу данные.
Начнем с того, что у двух потоков есть только один способ сообщать данные друг другу: записывать значение в переменную и дергать семафор; после этого другой поток из этой переменной читает (немного непривычно после ООП, где объекты посылают друг другу сообщения). Как записать такой интерфейс взаимодействия, если данные бывают разных типов - непонятно. Например, как описать интерфейс между процессами (не сами процессы), когда передаем сначала два boolean'а, а потом последовательность строк?

Решение:

Во-первых, как вообще пишется типизированное API для шагов? Можно сделать по интерфейсу на каждое состояние и возвращать новый объект после очередного шага.

interface ChooseLanguageScreen { EnterPinCodeScreen doStep1(Lang lang); } interface EnterPinCodeScreen { MainMenuScreen doStep2(byte b1, byte b2, byte b3, byte b4); ChooseLanguageScreen cancel(); } и т. д.
Здесь совершенно четко записано, что пока не сделаешь первый шаг, нельзя делать второй (у вас просто нет объекта в руках). На втором шаге в примере есть два пути, один из которых (cancel) возвращает нас в начало. Примерно так можно строго описать всю последовательность взаимодейтсвия с виртуальным банкоматом со стороны клиента в виде Java'ского API. Тут впрочем есть упрощение. Реально метод doStep2 возвращает не сразу MainMenuScreen, а какое-то подобие алгебраического типа, который может быть либо на самом деле MainMenuScreen, либо какой-нибудь InvalidPinScreen, т. о. совершив действие можно попасть в одно из двух состояний (и тут бы очень помогла поддержка алгебраических типов, да).

Так мы описали интерфейс взаимодействия (строго-типизированный!) с одной стороны. Но и сам банкомат может быть реализован своим потоком или процессом. В таком случае для него можно написать аналогичный интерфейс, только вывернутый наизнанку:

interface InitialState { LanguageChosenState waitForLanguage(); } interface LanguageChosenState { Lang getLang(); // Пришедшие данные. PinCodeEnteredState waitForPinCode(); // Переход к следующему шагу. } и т. д.
Теперь оба процесса могу работать через типизированное API. То есть задача решена - мы написали интерфейсы для нетривиального диалога потоков.

Само собой осталось реализовать эти интерфейсы - обеспечить реальную передачу данных. Это может быть сделано через сеть (с сериализацией данных) или внутри одного процесса - просто передавая указатели. Кстати, реализовать последнее без type-cast'ов - довольно интересная головоломка.

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

Как может выглядеть исходное описание протокола? Как state-машина, где все состояния делятся на «клиентские» и «серверные», а все переходы происходят от одной группы к другой.

Например так:

client ChooseLanguageScreen <= waitForLanguage() <= server InitialState; client ChooseLanguageScreen => doStep1(Lang lang) => server LanguageChosenState; client EnterPinCodeScreen <= waitForPinCode() <= server LanguageChooseState; client EnterPinCodeScreen => doStep2(byte b1, byte b2, byte b3, byte b4) => server PinCodeEnteredState; client EnterPinCodeScreen => cancel() => server InitialState; client InvalidPinScreen <= invalidPin(int numberOfTriesLeft) <= server PinCodeEnteredState; client MainMenuScreen <= pinOk() <= server PinCodeEnteredState;
и так далее, все переходы со всеми развилками. Построить из этого описания интерфейсы - тоже хорошее упражнение на абстрактное мышление.

concepts

Previous post Next post
Up