Pull to refresh

Comments 29

Аргументы в пользу того, что в конкретно вашем случае код становится проще, звучат неубедительно. Кажется, что это усложнение ради усложнения.
КА имеет смысл применять там, где много состояний, много переходов, могут быть циклические цепочки переходов. Остальные случаи можно отнести к тривиальным и пользоваться соответствующими более простыми средствами.
Разумеется, что пример упрощён и этот подход там не оправдан. В рабочих проектах код несколько сложнее.

Кроме того, разбиение на блоки всё-таки помогает лучше воспринимать код, явно видеть законченные фрагменты алгоритма и понимать, в каких случаях ожидается обработка данных от второй стороны, в каких — нет. Кроме того, я отступаю от строгости КА и конечное состояние зависит не только от исходного и входящих данных, но и от других параметров. К примеру, при передаче удалённому клиенту больших порций данных с докачкой, я храню счётчик переданного (байтов, фрагментов...) в объекте ClientConnection и в состоянии, например, «передатьБлокБольшихДанных» зацикливаюсь, пока счётчик не укажет на последний фрагмент «больших данных».

Также в некоторых случаях приходится вычитывать буфер данных с удалённого датчика, работая в двух состояниях «ожидаемНовыйФрагментБуфера» и «обрабатываемНовыйФрагментБуфера». И только получив в первом состоянии код об опустошении буфера переключаться дальше.
А, позвольте уточнить, почему не использовать генераторы (function* foo() {}) или копроцедуры (async function foo() {})? Они позволяют описывать конечные автоматы неявно, кратко, и не заботясь о вынесении состояния «за скобки».
Как минимум, потому что, к своему стыду, мне названные вами абстракции не были знакомы. Бегло посмотрев на генераторы, могу предположить, что его выполнение линейно, а мне в ряде случаев нужно оставаться в состоянии и обрабатывать некоторое количество событий сокета, а не переключаться сразу дальше. С асинхронными функциями сходу не понял, но, замечу, что использование машины состояний в моем коде не имеет явного отношения к асинхронности, промисам и проч.
Выполнение генератора линейно, как и выполнение функции, но ничто не мешает вам оставаться в нужном состоянии сколько угодно долго, используя циклы:
async function shell(password) {
    await write('What is the password?');
    while (true) {
        const guess = await read();
        if (guess === correctPassword) {
            await write('Nice guess!');
            break;
        }
        await write('Nope! ;)');
    }
    
    while (true) {
        await write('Okay, now enter some secret command');
        const command = await read();
        switch (command) {
            case 'secret': {
                await write('Purrrfect!');
                break;
            }
            case 'bye': {
                await write('Okay, see you later');
                return;
            }
        }
    }
}


Естественно, это все можно описать при помощи конечного автомата, потому что это и есть конечный автомат в конечном итоге — но такая запись, как мне кажется, немного человекочитаемее.
Приведённый в комментарии код не про генератор же, нет?
Всё таки мне непривычно, но не буду торопиться с выводами — надо пробовать.
Ну, приведенный в комментарии код можно записать и как генератор — при помощи вот этого, например. В таком случае `await` будет `yield`, а больше ничего не изменится.
Возможно, автор писал свое решение, когда генераторы не поддерживались нативно в nodejs, а транспилировать он не хотел.
Возможно, но что плохого в транспилировании? Речь ведь не о языках типа CoffeeScript или LiveScript, которые должны поддерживаться, и если поддержка прекратится, то все, приехали. Транспиляция идет из фактически будущей версии языка, которую рано или поздно будет поддерживать платформа, и от транспиляции этих функций можно будет отказаться.
Ну есть кое-какие неудобства, для кого-то незначительные, для кого-то show stopper.
А можно пример этих неудобств?
Стек-трейс показывает невесть куда, ошибки могут быть невразумительные, сам процесс занимает время, прикручивать юнит-тесты — занятие не для слабонервных (особенно в karma). В общем, вы не поверите, но в мире до сих пор полно проектов, которые пишутся не на ES2015:)
Стек-трейс показывает невесть куда
Так, первую проблему я вам решил.
ошибки могут быть невразумительные
Это в случае ошибок в самом транспайлере? Нельзя исключать, конечно. А при использовании транспайленного кода sourcemaps это обрабатывают.
сам процесс занимает время
Чего именно, внедрения транспайлера? Тот же babel подключается, например, следующим образом:
exec node --require "foobar/boot.js" "foobar/main.js";
А сам boot.js — это что-то типа такого. Надо будет повыбрасывать оттуда кучу трансформаций, т.к. их уже node давно поддерживает нативно, типа тех же классов. В таком случае, строку запуска можно поменять на что-то типа этого:
exec node \
	--es_staging \
	--expose_debug_as=V8Debug \
	--expose_gc_as=V8GC \
	--harmony \
	--harmony_array_includes \
	--harmony_arrow_functions \
	--harmony_atomics \
	--harmony_concat_spreadable \
	--harmony_default_parameters \
	--harmony_destructuring \
	--harmony_modules \
	--harmony_new_target \
	--harmony_object \
	--harmony_object_observe \
	--harmony_proxies \
	--harmony_reflect \
	--harmony_regexps \
	--harmony_rest_parameters \
	--harmony_sharedarraybuffer \
	--harmony_shipping \
	--harmony_simd \
	--harmony_sloppy \
	--harmony_spread_arrays \
	--harmony_spreadcalls \
	--harmony_tostring \
	--harmony_unicode_regexps \
	--strong_mode \
	--strong_this \
	--throw-deprecation \
	--require "foobar/boot.js" \
	"foobar/main.js";

прикручивать юнит-тесты
Признаться, я пользуюсь Mocha, и с ней никаких проблем не было — запуск тестов ничем не отличается от планового запуска кода, дополнительных телодвижений для этого не нужно. Для karma, как я погуглил, есть варианты:
1) использовать --require boot.js, как в примере выше, тогда все будет «просто работать»
2) использовать штуки типа github.com/babel/karma-babel-preprocessor
в мире до сих пор полно проектов
Есть уважительная причина — legacy. Для новых проектов нет причин, почему не использовать ES2015/ES2016.
Есть уважительная причина — legacy. Для новых проектов нет причин, почему не использовать ES2015/ES2016.

Про это самое я и говорил в первом комментарии:) С чего вы взяли, что это новый проект? Вон, товарищ даже классы не использует.

Про дистанцию от «погуглил, есть варианты» до «емае, наконец-то это поделие работает» в случае с karma писать не буду, больная тема:(
Ну, относительно новый, почти год. Но я не стараюсь писать код с использованием последних нововведений стандартов, поскольку в клиентской части внедрение сдерживается браузерами и менее подконтрольно, чем версия ноды на сервере.
О каких классах речь? TypeScript? Или нативные есть?
в клиентской части внедрение сдерживается браузерами

Парадоксально, но на момент написания этого комментария поддержка ES2015 в Chrome, FF и Edge полнее, чем в последней версии node.js.

Или нативные есть?

Не берусь передать всю силу моих эмоций:) Да, есть.
Нативные есть в es6. Для серверной части новые плюшки можно использовать спокойно. Часть из них нода поддерживает с флагами, часть уже стандартно.
Для клиентской части есть полифилы и библиотеки/бандлеры, которые прозрачно их вставляют.

В любом случае, есть babel, его можно настроить так, чтобы всё работало и на сервере и на клиенте (ну за исключением совсем уж древних браузеров).
Оговорка — «совсем уж древних» — это таких, которые не поддерживают даже ES3. Т.е. стандарт, которому 17 лет.
Для новых проектов нет причин, почему не использовать ES2015/ES2016.

Усложнение сборки проекта. Вернее кроме сборки добавление этапа компиляции.
Тот же babel предлагает require hook, для которого нет вообще никакого дополнительного этапа компиляции — только лишь единственный ключ при старте node, который я привел выше. Так как компиляция происходит единожды при импорте модуля, а затем он кешируется до изменения mtime, то его спокойно можно использовать в продакшене. Результат такой компиляции ничем не отличается от предварительной сборки.
Усложнение

только лишь единственный ключ

Этот единственный ключ может понадобиться ввести в 100500 местах синхронно у всех разработчиков, админов и т. д.
синхронно у всех разработчиков
Какой кошмар, а зачем? Только не говорите, что ваше приложение запускается просто как «node app.js» с командной строки, без npm scripts или какого-нибудь Procfile.
Это хороший вопрос, мне пришлось поставить forever и порыться в его исходниках, чтобы разобраться. Самое простое решение:
{
    "uid": "foobar",
    "append": true,
    "watch": true,
    "script": "foobar.js",
    "sourceDir": "/path/to/app",
    "command": "node --harmony --require /path/to/app/boot.js"
}
Правда, оно не очень красивое, так как sourceDir дублируется в command, потому что авторы forever не предусмотрели экспортировать переменные окружения вида FOREVER_SOURCE_DIR, хотя тут это бы пригодилось.

Решение получше — pull request в forever с экспортом переменных окружения, что я сейчас и сделаю.
Возможно автору будет интересно как в свое время я писал Конечный автомат на bash
Окончательное решение в конце статьи.
Основная идея такова:

Начало главного цикла
Выполнить действие текущего состояния *
Выполнить выход из текущего состояния **
Конец главного цикла

* Выполнение функции, которая связана с текущим состоянием (в моем случае состоянием считалось выполнение какого-то простого действия).
** Выполнение функции, которая жестко привязана к текущему состоянию. Она проверяет условия и выбирает следующее состояние.
Интересно! Но я точно не стал бы делать это на bash :) Синтаксис совсем непривычный, да linux пользуюсь не часто. А тот же js позволяет автоматизировать процессы нодой на линуксе и нодой или WSH под виндой.
Я не о языке реализации (в моем случае надо было запускать различные утилиты под linux, потому bash), а об организации кода. Автомат выполняется в замкнутом цикле. Текущее состояние = ссылка на функцию, но можно сделать и на объект. В последнем случае удобно реализовать функцию перехода к следующему состоянию в виде метода.
Да, я понял. Есть еще один момент — на примере я не уловил, где обработка входных данных? Есть ли вообще? Или состояния используются только для группировки действий? В статьей сказано, что ожидаются результаты каких-то «сервисов», но не совсем понятно — как это происходит.
Состояния используются для выполнения внешних действий — запуска утилит.
После завершения действия выполняется анализ ее результата и других обстоятельств и на основании этого выбирается следующее состояние (действие).

Например:
Состояние — импорт списка стран из внешнего сервиса.
По завершению проверяем все ли страны наша система распознала.
Если нет, то следующее состояние — отправка просьбы о помощи оператору.
Если все страны распознаны, следующее состояние — импорт городов.
Sign up to leave a comment.

Articles