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