Упростите свой код с помощью конечных автоматов
вступление
Вы когда-нибудь натыкались на кусок кода и говорили: «Что, черт возьми, здесь происходит?! Что это за логика?» 🥴. Если ответ положительный (и даже если нет 😜), вам следует узнать о вычислительной модели под названием «конечный автомат» и о том, как она может помочь вам упростить код.
Что такое «государственная машина»?
Конечный автомат — это поведенческая модель. Он состоит из конечного числа состояний и поэтому также называется автоматом с конечным числом состояний (FSM). Основываясь на текущем состоянии и заданном входе, машина выполняет переходы между состояниями и производит выходные данные. Вот пример, чтобы лучше понять.
Опишем мою жизнь:
const alive = new Date().getFullYear() === '2062' const repeat = () => { while (alive) { eat(); sleep(); code(); } } repeat();
Теперь я создам конечный автомат своей жизни:
Я начинаю свой день с кодинга. Если я голоден, я буду есть, а если я устал, я буду спать — это означает, что из состояния кодирования я перейду либо к еде, если я голоден, либо к сну, если я устал.
Вы можете взять любую вычислительную модель и создать для нее конечный автомат.
Хорошо, что теперь? 🤷♂️
Надеюсь, теперь вы хорошо понимаете, что такое конечный автомат. Итак, как это может помочь вам в повседневных задачах кодирования? Как однажды сказал Ричард Фейнман:
«То, что я не могу создать, я не понимаю».
В следующем разделе мы покажем пример внешнего интерфейса, написанного с помощью React. Мы будем использовать это приложение, чтобы продемонстрировать использование конечных автоматов. Обратите внимание, что это относится к любому языку программирования и для понимания примера не нужны специальные знания. Кроме того, код этого приложения написан таким образом, чтобы помочь вам лучше понять концепцию.
Упростите наш код 🤩
Скажем, у нас есть приложение, которое показывает таблицу данных. Таблица данных извлекает данные из API и при разрешении показывает данные. Пока вызов API не разрешен, мы показываем загрузчик:
Вот наш код, который имеет дело с логикой содержимого таблицы:
import { TableCaption, Tbody, Td, Tr } from '@chakra-ui/react'; import { AreaLoader } from 'src/components/common/AreaLoader'; type TableContentProps = { data: Data[]; isLoading: boolean; }; export const TableContent = ({ isLoading, getTableBodyProps, prepareRow, rows }: TableContentProps) => { if (isLoading) { return ( <TableCaption> <AreaLoader /> </TableCaption> ); } return ( <Tbody> {data.map((row) => <Tr> {row.cells.map((cell) => ( <Td>{cell}</Td> ))} </Tr> )} </Tbody> ); };
Как вы можете видеть в приведенном выше примере, если мы находимся в состоянии загрузки, мы возвращаем заголовок таблицы с загрузчиком, иначе мы отрисовываем тело нашей таблицы.
Каждый раз, когда мы меняем фильтр таблицы данных, мы получаем новые данные из API. В этом случае мы либо видим отфильтрованные результаты, либо видим раздел без результатов.
type TableContentProps = { data: Data[]; isLoading: boolean; isFilterOrSearchResultsEmpty: boolean; }; export const TableContent = ({ isLoading, isFilterOrSearchResultsEmpty, data, }: TableContentProps) => { if (isLoading) { return ( <TableCaption> <AreaLoader /> </TableCaption> ); } if (isFilterOrSearchResultsEmpty) { return ( <TableCaption> <EmptyList text="No results found" /> </TableCaption> ) } return ( <Tbody> {data.map((row) => <Tr}> {row.cells.map((cell) => ( <Td>{cell}</Td> ))} </Tr> ); } </Tbody> ); };
Не все программные системы свободны от греха, и иногда у нас есть ошибки. Я хочу убедиться, что мы покажем пользователю правильную ошибку в таком редком случае.
А теперь полный пример кода:
type TableContentProps = { data: Data[]; isLoading: boolean; isError: boolean; isFilterOrSearchResultsEmpty: boolean; }; export const TableContent = ({ isLoading, isFilterOrSearchResultsEmpty, isError, data, }: TableContentProps) => { if (isLoading) { return ( <TableCaption> <AreaLoader /> </TableCaption> ); } if (isFilterOrSearchResultsEmpty) { return ( <TableCaption> <EmptyList text="No results found" /> </TableCaption> ); } if (isError) { return ( <TableCaption> <Box> Something went wrong... Please refresh this page or <Box as="a">Contact support</Box> if the problem persists </Box> </TableCaption> ); } return ( <Tbody> {data.map((row) => <Tr> {row.cells.map((cell) => ( <Td>{cell}</Td> ))} </Tr> ); } </Tbody> ); };
Как вы можете видеть выше, этот код очень быстро усложнился. В нашем коде есть несколько операторов «если», невозможные переходы между состояниями и странное поведение пользовательского интерфейса. Теперь я собираюсь показать, как я могу использовать конечный автомат для упрощения этого кода (также примите во внимание, что существует дополнительная логика вызовов API и длина данных как таковая, которые не включены в примеры кода, но вы можете представьте, как они будут выглядеть).
Какие состояния у меня есть?
Если мы посмотрим на код выше, мы можем разбить его на 4 состояния:
1. Загрузка — когда мы получаем данные из API или когда мы выполняем действие.
2. Успех — когда вызов API разрешен и у нас есть данные для отображения.
3. Ошибка — когда мы сталкиваемся с ошибкой при выполнении вызовов API.
4. Нет результатов — когда мы ищем или фильтруем, мы не получаем никаких результатов.
Создайте функцию конечного автомата 🤓
Прежде чем мы начнем решать нашу проблему и упростить наш код, я хочу написать функцию конечного автомата (фрагмент кода, который будет хранить для меня логику состояния). Эта функция будет производить на основе моего текущего состояния и заданного ввода, каково мое следующее состояние.
Я создам новое перечисление для представления моего состояния:
export enum TableStateEnum { ERROR = 'error', LOADING = 'loading', SUCCESS = 'success', EMPTY_FILTER_OR_SEARCH_RESULT = 'emptyFilterOrSearchResult', }
И это будет моя функция конечного автомата:
type TableStateParams = { data: Data[]; loading: boolean; error: boolean; isFilterOrSearchResultsEmpty: boolean; }; export const getTableState = ({ loading, error, data, isFilterOrSearchResultsEmpty }: TableStateParams) => { if (loading) { return TableStateEnum.LOADING; } if (error) { return TableStateEnum.ERROR; } if (isFilterOrSearchResultsEmpty) { return TableStateEnum.EMPTY_FILTER_OR_SEARCH_RESULT; } return TableStateEnum.SUCCESS; };
В приведенной выше функции мы просматриваем каждое логическое условие и определяем текущее состояние нашего приложения. Что хорошего в этой функции, так это то, что мы можем понять все состояния нашего приложения, не погружаясь в логику (которая может быть очень сложной).
Перенесите нашу логику
В приведенном выше примере кода мы видим множество «если рендеринга», которые создают сложную логику компонента. Я предпочитаю, чтобы мой код выглядел лучше и проще, чем пытаться ориентироваться во всех больших функциях рендеринга. Для этого воспользуемся Mapper.
Функция сопоставления даст мне возможность отображать соответствующий компонент для каждого заданного состояния. Это позволит нам иметь меньший компонент с понятной логикой (упрощенной 💯).
export const MAP_CONTENT_BY_STATE = { [TableStateEnum.LOADING]: () => ( <TableCaption> <AreaLoader /> </TableCaption> ), [TableStateEnum.ERROR]: () => ( <TableCaption> <Box> Something went wrong </Box> </TableCaption> ), [TableStateEnum.EMPTY_FILTER_OR_SEARCH_RESULT]: () => ( <TableCaption> <EmptyList text="No results found" /> </TableCaption> ), [TableStateEnum.SUCCESS]: ({ data, }: TableContentProps) => ( <Tbody> {data.map((row) => <Tr> {row.cells.map((cell) => ( <Td>{cell}</Td> ))} </Tr> ); } </Tbody> ), };
Перестроить компонент — окончательный результат
Теперь у нас есть все наши части, и мы можем увидеть последний пример:
import { getTableState } from './utils'; import { MAP_CONTENT_BY_STATE } from './mapper'; export const TableContentSimplified = ({ isLoading, data, isError, isFilterOrSearchResultsEmpty, }: TableContentProps) => { const state = getTableState({ loading: isLoading, error: isError, data, isFilterOrSearchResultsEmpty }); const StateComponent = MAP_CONTENT_BY_STATE[state]; return <StateComponent data={data} />; };
Как вы можете видеть выше, этот компонент намного чище и проще. Теперь я могу легко смоделировать каждое состояние, просто указав, какое состояние я хочу видеть. Это поможет при отладке, разработке и чтении компонента.
Как вы можете использовать эту технику в других областях разработки?
Хотя приведенный выше пример относится к внешнему приложению, вы можете применить парадигму конечного автомата к любой области разработки. Это применимо к любой другой области развития.
Должен ли я всегда использовать конечные автоматы в своем коде?
Простой ответ — нет, но вы обязательно должны к этому стремиться, чтобы стандартизировать свой код. Трудно создавать код с нуля в качестве конечного автомата, потому что мы не понимаем, сколько состояний у нас будет в большинстве случаев. Сказав это, когда вы с самого начала строите свой код как конечный автомат (да, даже если у вас есть только два состояния), легко добавить новые состояния и протестировать их или изменить существующие состояния. Иногда сложно понять полную картину со всеми «если» и логикой. Конечные автоматы могут помочь вам выделить более широкую картину и на самом деле могут помочь вам решить, где вы должны добавить свой новый код, а где нет (что не менее важно).
это обертка
Мы рассмотрели, что такое конечный автомат, и создали пример кода, чтобы помочь вам понять многочисленные плюсы использования парадигмы конечного автомата в вашем коде. Я надеюсь, что это поможет вам реализовать эту парадигму в вашей кодовой базе. Часто, когда я работаю с очень сложной кодовой базой, я пытаюсь превратить ее в конечный автомат. Это помогает мне понять логику. Поэтому, даже если вы не можете изменить существующий код, я призываю вас принять конечные автоматы в качестве ментальной модели.
Спасибо 💜