Комментарии 11
Нужно посмотреть как это масштабируется.
Можно ли из машин построить цеха и заводы, как они будут согласовывать свои состояния, и т.д.
Но такой подход мне определённо нравится.
Даже сделали себе небольшую библиотеку для TypeScript, которая позволяет декларативно описывать машины и их свойства — какие данные(payload) будут применены; наследовать состояния; ограничивать, в какие состояния можно переходить из текущего; навешивать коллбэки на входы-выходы из состояний. Правда её код еще есть куда совершенствовать, сделано быстро-быстро на коленке :)
import { IStateDeclaration, StateMachine } from 'tstate-machine';
class ButtonStateMachine extends StateMachine {
// initial state
text: string = 'do request';
diisabled: boolean = false;
// state declarations
// From what state we inherit and in what states we can transit
@StateMachine.extend(StateMachine.INTIAL, ['requestState'])
mainState: IStateDeclaration<ButtonStateMachine> = {}; // no changes relative to parent(initial) state
@StateMachine.extend('mainState', ['doneState'])
requestState: IStateDeclaration<ButtonStateMachine> = {
text: 'sending...',
disabled: true
};
@StateMachine.extend('requestState')
doneState: IStateDeclaration<ButtonStateMachine> = {
text: 'done'
// no change disabled - property inherited from requestState and has `false` value
};
// common but important actions
// states in which one we can transit from initial
@StateMachine.hide
protected get $next(): Array<string> {
return ['mainState'];
}
// remember initial state
constructor() {
super();
this.rememberInitState();
}
}
const machine = new TextStateMachine();
machine.transitTo('maintState');
machine.transitTo('requestState');
console.log(machine.text); // autocomplete works fine!
В больших проектах конечные автоматы, если их моделировать с помощью UML, являются также средством коммуникации и спецификации поведения системы.
Их знание и использование благотворно сказывается на мышлении разработчиков, имхо.
Пример из собственной практики. В одном очень большом и ответственном проекте существовало текстовое описание логики на пяти страницах, которую разработчики отобразили в код. Код изобиловал if else, switch и т.д. Тестеры постоянно находили ошибки. Их залатывали, и старые тесты начинали бастовать. Когда меня послали в это гиблое место, я отобразил текст спецификации в конечный автомат. Он оказался удивительно элегантным на вид и работал замечательно. Правда он был не так просто устроен, как рассмотренный в статье, а со стеком и вложенными состояниями.
Так что советую всем: если логика сложная- попытайтесь постоить конечный автомат!
Был очень классный доклад от товарища из Microsoft Research — https://www.youtube.com/watch?v=VU1NKX6Qkxc
Кроме того, он написал JS библиотечку для работы с конечными автоматами в реакт http://davidkpiano.github.io/xstate/docs/#/
После чего, Раин Флоренс записал видео с использованием ее — https://www.youtube.com/watch?v=MkdV2-U16tc&t=9s
Для xstate еще есть пример кода для визуализации графа — https://codepen.io/davidkpiano/details/ayWKJO
Чего мне не хватает в Redux, так этот способа остановки диспетчеризации действия, основанного на текущем состоянии приложения без загрязнения редьюсера условной логикой.
Можно FSM прикрутить поверх редьюсера, с помощью higher order reducer. Это позволит оставить всю структуру приложения, включая код редьюсеров, вообще без изменений. Мы просто добавляем декларативное описание FSM для конкретных редьюсеров.
Может выглядеть как-то так:
import ActionTypes from '../action-types'
import withFsm from 'withFsm'
const initialState = {
data: []
}
function reducer(state = initialState, action) {
switch (action.type) {
case ActionTypes.SUCCESS: return {...state, data: action.payload}
}
}
// до этого момента был исходный код редьюсера без изменений
// теперь опишем для него FSM
const fsm = {
name: 'idle',
transitions: {
'idle': {
[ActionTypes.CLICK]: 'fetching'
},
'fetching': {
[ActionTypes.SUCCESS]: 'idle',
[ActionTypes.ERROR]: 'error',
},
'error': {
[ActionTypes.RETRY]: 'fetching',
[ActionTypes.CANCEL]: 'idle',
}
}
}
export default withFsm(reducer, fsm)
А сам код редьюсера высшего порядке withFsm
примерно такой:
export default function withFsm(reducer, fsm) {
return function reducerWithFsm(state, action) {
const reducedState = reducer(state, action)
const transitions = fsm.transitions[state._fsmName]
// недопустимый переход - состояние не меняем!
if (!transitions || !transitions[action.type]) return state
// допустимый переход - возвращаем новое состояние
return {
...reducedState,
_fsmName: transitions[action.type]
}
}
}
Вроде бы, получаем все плюсы от FSM плюс все плюсы от использования Redux.
Что скажете?
Мне не так давно пришла в голову очень похожая идея, вот только я сделал это немного по другому, на основе RTK (redux toolkit).
создал issue на githab в RTK:https://github.com/reduxjs/redux-toolkit/issues/1065
Очень давно использую этот подход в разных технологиях, и везде успешно. В середине 2000-х я даже пытался книгу об этом написать. С тех пор подходы существенно эволюционировали, но в основе та же суть.
Взялся что-то написать на эту тему, и в процессе прошелся по чужому опыту. Оказалось полезно, уже написано много разного, но и мне есть чего добавить от себя.
Спасибо за статью!
Мой подход заключается в том, что приложение описывается как дерево состояний в формате JSON. На его основе формируется техническое задание, генерируются шаблоны классов состояний с JDOC, и в runtime формируется дерево состояний.
Это же дерево или его ветки в процессе работы приложения могут сериализоваться и десериализоваться для организации команд CTRL+Z и CTRL+Y и других целей.
В итоге образуется целый ряд преимуществ, но это не описать в комментарии.
Машины состояний и разработка веб-приложений