Конечные автоматы на примере плавающей боковой панели (sidebar)
Предыстория
Когда-то давным давно, когда на дворе был javascript 2015… мне понадобилось реализовать вот такую плавающую боковую панель (sidebar):
На тот день я нашел несколько плагинов, Sticky-Kit, StickyJS и т.п.
Я честно пытался их подключить… но что-то каждый раз шло не так. То работали как-то странно, то не работали вообще.
В общем, я решил написать свою реализацию — однако задача оказалась совсем не такой простой.
Условий становилось все больше и больше, а код становился все более запутанный — приходилось постоянно бороться с каким-нибудь новым багом.
Я даже отчаялся, что смогу заставить эту панель заработать… правильно! Но уж раз начал — надо доделывать!
В итоге код расчета позиции выглядел примерно так:
Как выяснилось позже, примерно такой же подход был и в плагинах, которые я пытался подключить. В общем, что-то вроде этого:
- Много вложенных условий
- Логику работы панели понять невозможно
- Нет уверенности, что все работает правильно
- Очень сложно что-то поправить, не сломав при этом всего остального
А что можно предложить?
Прошел год — я познакомился с понятием конечного автомата (finite state machine или сокращенно fsm).
И через некоторое время понял, что концепция конечных автоматов идеально подходит для решения задачи с плавающей панелью.
Про теорию конечных автоматов (не скучно и доступно!) можно почитать тут: https://tproger.ru/translations/finite-state-machines-theory-and-implementation/.
Как применить?
Наш конечный автомат будет состоять из:
- Множества состояний (START, BOTTOM_FIXED, TOP_FIXED, UNFIXED, FINISH)
- Множества событий (каждое событие состоит из набора условий, например, “панель влезает в экран и пересекла точку старта”)
- Описания переходов (Событие + Некоторое состояние => Новое состояние)
- Действия для каждого состояния (например, “зафиксировать панель сверху” при переходе в состояние TOP_FIXED)
- Начального состояния (в нашем случае START)
Для начала определим наши состояния:
Отдельно перечислять события мы не будем. Почти все события уникальны для конкретного перехода, и их удобнее видеть рядом с этим переходом.
Поэтому мы сразу перейдем к описанию переходов от одного состояния к другому:
То что находится в функции when, это и есть набор условий — то есть событие.
В переменной d (dimensions) я передаю объект с заранее рассчитанными значениями текущего положения панели, размера экрана и т.д.
Далее опишем действия при переходе в то или иное состояние:
Теперь нужно определить функцию, которая создает конечный автомат:
Остается создать конечный автомат с нашей конфигурацией и “запустить” его на скролле:
Пользователь будет скроллить страницу, а мы будем пытаться найти доступный переход для текущего состояния панели + некоторых рассчитанных значений (dimensions).
Если переход будет найден — мы перейдем в новое состояние и выполним действие для этого состояния, например, зафиксируем панель.
Дальше все повторится уже с новым состоянием.
Что в итоге?
Хорошая читаемость кода за счет разделения на “смысловые” части (состояния, переходы, действия).
Легкая отладка. Мы всегда можем проследить всю цепочку переходов, найти некорректный переход и исправить его условия.
Надежность. Мы можем смело изменять условия проблемных переходов, не боясь сломать какое-либо другое поведение, так как все переходы независимы.
Кому интересно, полный код плагина можно посмотреть тут: