Предисловие
В первой части я говорил общими терминами и больше рассуждал про бизнес-процессы (почитать можно тут). Далее я буду более детально углубляться в структуру проекта и частные случаи, постараюсь продемонстрировать разные подходы и порассуждать, чем они хороши или плохи. Но, для начала, еще пара общих слов.
Ошибки — это норма
Ошибаются все, от джунов до генеральных директоров. Важен не сам факт ошибки, а реакция на нее. Если пытаться сразу оправдать или начать отрицать ее, придумывать кучу отговорок или посыпать голову пеплом, то ничего хорошего из этого не выйдет. Напротив, если принять ее, проанализировать, разобрать причины, предшествовавшие им, и попытаться исправить данную ошибку, то можно неплохо прокачать свои навыки. Этим, по моему мнению, и отличаются младшие разработчики от средних, средние от старших, а старшие от руководителей групп или лидов. Не качеством и скоростью написания кода, хотя это тоже имеет место быть, но опытом и объемом проработанных ошибок (ошибки не обязательно должны быть допущены самим разработчиком).
Ошибаться — это нормально. Из ошибок, своих и чужих, складывается ваш профессиональный опыт. Все зависит от реакции на ошибку.
Чистый код - чистая архитектура
Можно сколько угодно тасовать систему папок и файлов, как колоду карт, решать, что выбрать, микросервисы или монолиты, подбирать фреймворки и библиотеки, но чистая архитектура невозможна, по моему субъективному мнению, без чистого и понятного кода. Само собой, чистота кода — дело субъективное, и да, не бывает эталонного кода, который можно поставить в палату мер и весов (разве что пустую строку). Всегда можно что‑то улучшить, где‑то подкрутить, что‑то декомпозировать, оптимизировать и т. д. Но, как говорится, весь код чистым сделать мы не можем, но стремиться обязаны. Надо всегда держать в голове, что код мы пишем не только для того, чтобы закрыть фичу и отдать ее заказчику, но и для других разработчиков, которые будут с этим кодом работать. Возможно, этим разработчиком будете вы сами.
При этом не надо изобретать велосипеды, все давно уже придумано. Иной раз даже принципов SOLID достаточно, но только если вы их понимаете. Недавно я наткнулся на статью «Как TypeScript помогает решать проблемы обратной совместимости в UI‑библиотеках». Ни в коем случае не хочу выразить неуважение к автору статьи, так как не считаю его решение проблемы наихудшим или самым не подходящим, но данной статье можно увидеть пример нарушения принципа «open/closed», когда пытаются добавить новый функционал в элемент Input, но при этом меняют параметры, передаваемые в метод onChange. В данном случае, следуя принципам SOLID, следовало бы либо сделать новый компонент, либо добавить новый метод onClear или что‑то еще, так как уже реализованные методы onChange, передаваемые извне, могут быть завязаны на втором аргументе и использование нового функционала в этих местах будет недоступно без внесения дополнительных изменений. Также давайте подумаем о том, если вскоре потребуется добавить новый функционал, который будет затрагивать метод onChange и требовать новых изменений в нем. Что тогда будем делать? Снова манипулировать типами и усложнять код? Рано или поздно это в любом случае приведет к созданию нового компонента, следовательно, можно было это сделать сразу. Да и практику передачи вторым аргументом нативного ивента в компонентах типа Input, Select и т. д. я нахожу избыточной, если честно. Всегда можно использовать ref.
Чем чище и понятнее код, тем проще и приятнее вносить изменения. Каждый раз возвращаясь к своему коду спустя несколько месяцев или годов и уже позабыв, что вообще в данном месте происходит, вы будете говорить самому себе «спасибо» за то, что позаботились о качестве кодовой базы и можете спокойно внести изменения без мата и оскорбления себя из прошлого.
Чистота кода также помогает быстрее разрабатывать. Если сразу наметить основные сущности реализуемой «фичи» (в голове или на бумаге) и разнести все это на самостоятельные функции/классы, которые имеют минимальный уровень зацепления, а не писать «портянкой», то будет легче что‑то поменять, объединить, вынести в другой файл или выкинуть. Как правило, качественная реализация нового функционала имеет несколько итераций и позволяет проще исправлять и модифицировать изолированные модули, чем императивный код с сайд эффектами. Таким образом мы и экономим время и нервы.
Также хотел бы отдельно сказать, что чистый код — это навык. И навык этот не врожденный, он приобретается благодаря часам, потраченным на улучшение свеженаписанного кода, его анализ и поиск альтернативных решений. Возможно, придется потратить много времени для его выработки, но в какой‑то момент вы определите для себя правила, закономерности и паттерны, благодаря которым приступая к новой задаче уже сразу сможете разложить будущие сущности по полочкам и практически с первого раза написать красивый, удобный и понятный код.
Про чистый код написано много статей, но я очень рекомендую в первую очередь ознакомиться с книгой под названием «Чистый код» за авторством Роберта Мартина. Я сам не раз возвращался к этой книге для закрепления знаний. Также, у него есть книга «Чистая архитектура», но там обсуждаются уже более высокие материи, и фронт занимает там лишь малую часть.
Чистый код - ваш помощник. Чем чище кодовая база, тем легче ее поддерживать.
Документирование
окументация - одна из важнейших вещей, которая помогает поддерживать проект. Когда кодовая база разрастается, в ней становится сложно ориентироваться. Особенно доки начинает не хватать, когда на проекте применяются нестандартные решения, написанные в спешке, которые не сразу понятны. Можно потратить очень много времени на расшифровку функционала, реализованного в коде, прежде чем понять, как можно модифицировать код без сайд эффектов.
В идеальном мире код должен быть самодокументируемым. Иными словами, код должен быть понятным и очевидным и не должен вызывать затруднений при работе с ним. Не всегда такое возможно, да и термины "понятность" и "очевидность" весьма субъективны. Особенно ярко это проявляется в сложных и нагруженных системах, таких, как платформа для трейдинга, 3D-редактор декора помещений или сетевая браузерная игра. Такие приложения, как правило, содержат множество контроллеров, DTO, адаптеров, фасадов и прочего умного и это может приводить к так называемому спагетти-коду, в котором можно бесконечно проваливаться из одного контроллера или фасада в другой. Тут то нам на помощь и приходит документация. Она помогает понять суть и цель определенного участка кода без погружения в него.
Документировать можно абсолютно все, функции, классы, отдельные строчки кода и даже, хотя нет, особенно способы и методики, применяемые на проекте (style guide, архитектура, дизайн-система проекта и т. д.). Это всегда работает и помогает разработчикам экономить время, затрачиваемое на объяснение принципов работы кода другим разработчикам. И это не так дорого стоит. Зачастую, на комментирование уходит очень малый процент времени, так что аргумент "нет времени" тут особо не подходит.
Если несколько разработчиков умело документируют свой код, то они становятся "друзьями по переписке". Не надо больше изучать git blame и бегать по всем разработчикам, принимавшим участие в написании кода, в поисках ответов на вопросы "что это такое?", "зачем тут эта переменная?", "почему я меняю код, а {ожидаемое действие} не происходит?".
С другой стороны, следует избегать избыточной документации. Не надо описывать каждый свой очевидный шаг и каждую функцию, название которой говорит само за себя. Документация ради документации не имеет смысла. Должен быть здоровый баланс, иначе получите вот это:
/*
* Функция для суммирования двух чисел
* @param {number} x - первое число
* @param {number} y - второе число
* @returns {number} result - сумма x и y
*/
function sum(x: number, y: number) {
return x + y; // сразу возвращаем результат суммирования
}
Также хотел бы отдельно проговорить, что документировать можно/следует не только код, но и проект в целом (как его настроить и запустить, какие дополнительные инструменты и как использовать и т. д.), но если проект находится на стадии MVP0 и ниже, то с документацией проекта можно повременить, но как только все более-менее стабилизируется, необходимо сразу заняться данным вопросом.
Документация - очень удобная вещь, помогающая разработчикам взаимодействовать друг с другом и ускоряющая разработку, но она должна быть осмысленной и своевременной.
Style guide
Еще одна вещь, которую я считаю обязательной, это style guide. Он является одновременно и договоренностью между разработчиками о том, как писать код, и быстрым стартом для новичков на проекте, и, в некоторых ситуациях, судьей в споре между двумя разработчиками. Благодаря такому соглашению код на проекте будет единообразным и консистентным.
Style guide может быть как документом, так и настройками eslint'а и stylelint'а. Все зависит от проекта и количества разработчиков. В идеале должен быть документ, в котором описаны основные принципы, чтобы любой новичок мог прийти, почитать и понять, как писать код на проекте, а также должны быть настроены линтеры, содержащие в себе уже полный перечень правил, принятых на проекте.
Микросервисы и монолиты
Ох уж эти новомодные микросервисы, о которых кричат на каждом углу и которых в какой-то момент становится так много, что можно запутаться в них, не говоря уже о проблемах, связанных с взаимодействием между ними. И ох уж эти монолиты, которые почти всегда вырастают в огромного монстра за пару лет. Такой сервис становится неповоротливым, в нем много дублированного кода, а бандл его весит столько, что аж страшно становится. На самом деле, и у первого, и у второго подхода есть свои плюсы и минусы. Главное - понимать, что и когда использовать.
Первый пример
Рассмотрим проект, который обещает быть большим (50+ уникальных страниц). Наша задача запустить MVP0 в кратчайшие сроки, а дальше, в зависимости от результатов аналитики, уже будет понятно, стоит ли дальше развивать проект или он закроется так и не реализовавшись. Нам передали дизайн в фигме, при этом в нем нет единой дизайн-системы, какие-то повторяющиеся элементы являются компонентами, какие-то - нет, отступы пляшут и т. д. Стоит ли сразу делить все на микросервисы? А как быть с ui-kit? Я думаю, что в данном случае ответ очевиден - делаем монолит. Но мы знаем, что в будущем проект может разрастись, а значит, следует это предусмотреть. Как это сделать? тут нам на помощь приходит модульность (рассказываю об этом ниже). Если различные папки, такие, как components, pages и т. д. будут представлять из себя модули, это позволит нам без особых усилий и усложнений разделить проект на микросервисы и библиотеки.
Остается лишь вопрос о повторяющихся компонентах с бизнес-логикой. Я называю такие компоненты виджетами или умными компонентами (компоненты высшего порядка). Куда их то девать и как их сделать модульными? они же сами могут ходить на бэк и вытягивать данные из стора, а значит, имеют высокую степень связанности. Тут уже вопрос со звездочкой. Для начала необходимо разобраться, что вообще эти компоненты должны и не должны уметь делать. Во-первых, а должны ли они залезать в стор? Зачастую, таким компонентам также можно задать внешнее api, через которое мы можем передавать данные из стора и методы, которые будут эти данные мутировать, а значит, к стору лучше изначально не привязываться, так как в этом нет необходимости. А вот с походом на бэк ситуация неоднозначная. Да, с одной стороны, мы можем так же через api передавать url запроса и параметры для его исполнения, а также адаптер для обработки данных результата запроса под виджет. Но тогда при изменении виджета придется синхронно вносить изменения во всех проектах или каждый раз создавать дубль виджета с набором нового функционала. Так или иначе перспективы так себе. Следовательно, проще всего сделать так, чтобы виджет сам отвечал за запросы. Но нам это не мешает вынести виджеты в отдельный пакет или сервис, элементы которого будут динамически подгружаться в необходимые места.
Второй пример
Теперь рассмотрим похожий проект, но с небольшим отличием: должно быть два сайта, которые имеют разный функционал, но общих пользователей. Каждый сайт необходимо запустить одновременно и в кратчайшие сроки (надо было вчера). Какой подход использовать в данном случае?
Сперва может показаться, что тут то сразу нужно заводить 3 разных проекта, делать пакеты с ui-kit'ом, виджетами, а также с сервисом авторизации. Но не спешите. Также следует учесть сроки, а разработка и поддержка сразу нескольких проектов (в том числе и библиотек) требует гораздо больших усилий, нежели монолит. Например, для того, чтобы обновить кнопочку из ui-kit'а на всех трех сайтах, необходимо изменить ту самую кнопочку в ui-kit'е, затем зарелизить его, потом обновить версию ui-lit'а в каждом проекте и уже теперь можно использовать обновленную кнопку. Маршрут получается так себе. А если еще и изменениями косякнули? Надо заново проделать всн манипуляции. И все это ради одной кнопки. Звучит так себе, не правда ли?
Тут нам на помощь приходят git submodules(подмодули) yarn/npm workspaces . Если вкратце, то мы создаем проект, в котором можно хранить пакеты (packages). Прелесть этого подхода в том, что подпроекты могут быть как сайтами, так и пакетами. И это может быть не один репозиторий. Каждый пакет может быть подмодулем, следовательно, этот подмодуль в любой момент может стать обособленным пакетом. Таким образом, мы можем за один присест внести изменения в ui-kit, сразу же собрать его и обновить его версию в соседнем пакете, который является сайтом, все проверить, применить новые изменения и все, что нам остается сделать, это сначала запушить изменения в ui-kit, затем запушить изменения в проектах. Согласитесь, это гораздо быстрее. При всем этом наш проект уже готов к масштабированию и разбиению на отдельные микросервисы.
Третий пример
Предположим, что у нас уже есть проект, которому несколько лет, над ним работает несколько команд разработчиков, кодовая база запутана и непонятна, а сам проект разросся настолько, что его сборка занимает 5–10 минут минимум. Как быть в данной ситуации? Увы, нет единого решения для всех подобных случаев, но чаще всего необходимо переписать проект заново и перевести его на микросервисы. Это, на мой взгляд, самое верное решение, так как мы можем учесть все ранее допущенные ошибки и плавно переехать со старого решения на новое подменяя по частям одну страницу за другой. Есть только одна угроза для данного подхода - если дизайн-систему так и не разработали, то, вполне вероятно, получится то же самое приложение, но на микросервисах.
У монолитов и микросервисов есть свои плюсы и минусы. Каждый подход должен быть использован исходя из текущей ситуации. Для того, чтобы можно было уйти от одного подхода к другому, необходим модульный подход.
Модульность
Выше я уже затронул тему модулей. Так что же это такое? Модуль — это часть проекта, которая имеет минимальное зацепление и является самостоятельной. Иными словами, это кирпичики, из которых можно построить приложение, причем и не одно. Например, даже один компонент вроде Button , если он имеет нулевую связанность, представляет собой отдельный модуль.
Также, это может быть папка вроде components, в которой лежат dummy компоненты. Если все компоненты в ней не обвязаны бизнес-логикой и имеют строгое внешнее api, то эту папку можно будет легко преобразовать в ui-kit библиотеку. Напротив, если в ней будут храниться компоненты, которые, например, завязаны на текущем сторе, то изолирование данной папки в отдельный проект станет болезненным процессом. То же самое касается и страниц. Каждая страница — это отдельный модуль. Да, у нас в любом случае будет общее хранилище данных (например, данные сессии и пользователя), которое будет распространяться на все страницы, но в этом хранилище не должны находиться данные для специфических страниц. Их следует хранить в самой странице. В таком случае, в будущем мы сможем каждую страницу вынести в отдельный микросервис и объединить их под одним, главным сервисом, который будет всем управлять.
Также, модульность обеспечивает более удобное покрытие тестами, что очень важно в наше время. Каждый модуль достаточно легко протестировать и отловить баги, что повышает надежность продукта.
Модульность помогает нам поддерживать и развивать проект за счет низкой степени зацепления.
Умные/глупые компоненты
Выше я упоминал данные термины. Здесь расскажу, чем они отличаются и почему я считаю такое разграничение компонентов удобным. Не сложно догадаться, что глупые компоненты — это компоненты, которые не содержат в себе никакой бизнес-логики. Они не контролируют свое состояние и не знают, где будут использованы. У них только две задачи: ввод и вывод данных. Приведу пример:
import styles from "./_styles.module.scss";
type TButtonTheme = "primary" | "secondary";
interface IButton
extends React.DetailedHTMLProps<
React.ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
> {
theme?: TButtonTheme;
loading?: boolean;
}
export const Button: React.FC<IButton> = ({
children,
theme = "primary",
loading = "false",
...restProps
}) => {
return (
<button className={`${styles.button} ${styles[theme]}`} {...restProps}>
{loading ? <Preloader /> : children}
</button>
);
};
Такой компонент соблюдает все правила. Он не контролирует свое состояние и не указывает, где и как его использовать, но имеет четкое и строгое внешнее api . Даже сложные компоненты вроде слайдера являются глупыми.
Умные компоненты, в свою очередь, сами диктуют условия. Они содержат в себе бизнес-логику и управляют состояниями других компонентов. Например, компонент формы с отправкой данных на сервер:
import { useState } from "react";
import styles from "./form.module.scss";
import { Button } from "../Button/Button";
export const Form: React.FC = () => {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [loading, setLoading] = useState(false);
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
fetch("someUrl.com", {
method: "POST",
body: JSON.stringify({ name, email }),
}).finaly(() => setLoading(false));
};
if (loading) return <Preloader />;
return (
<form className={styles.form} onSubmit={onSubmit}>
<Input value={name} onChange={setName} />
<Input value={email} onChange={setEmail} />
<Button>Подтвердить</Button>
</form>
);
};
Как мы видим, компонент выше контролирует состояния инпутов и свое собственное. Мы не можем никак повлиять на его поведение извне. Это и делает его умным.
Данное разделение компонентов помогает нам структурировать код и придерживаться модульного подхода. Если перед реализацией каждого нового компонента мы будем думать, к какому типу нам следует его отнести и где он потенциально окажется через год или два, то станет гораздо проще масштабировать проект в будущем.
Деление компонентов на умные и глупые помогает нам придерживаться модульной структуры.
В следующей статье я более подробно расскажу про структуры файлов и папок на проекте.
P.S. Если вам интересна какая либо из вышеперечисленных тем, то пишите в комментарии.
Комментарии (11)
olku
28.12.2024 20:56Спасибо за статью. Какие метрики вы применяете для оценки чистоты Реакт кода? Какие пороговые значения этих метрик триггерят техдолг?
boopiz
28.12.2024 20:56автор! ты графоман! ни критериев, ни определений. одна сплошная субъективщина. что такое "чистота"? ты и сам определить не можешь в статье, как и все остальные направления. в общем виде, критерий "чистоты" определяется как достаточная лаконичность. пословица даже есть: краткость - сестра таланта. а у тебя энтропия растёт в геометрической прогррессии.
13luck
28.12.2024 20:56Везде рассказывают про архитектуру в вакууме, забывая рассказать о специфике браузерной среды. Расскажите лучше про это. Это очень важно, если вы делаете что-то сложнее чем формы и вывод контента, например, игры. Я никогда особо не обращал внимание на то, что браузер может засыпать, что приводит к остановке некоторых вычислений. То есть "железную" логику легко поломать, если просто переключить фокус на другой таб.
nin-jin
28.12.2024 20:56Деление компонентов на умные и глупые помогает нам придерживаться модульной структуры.
Как вы думаете, почему это разделение есть только в React экосистеме и не имеет никакого смысла в о
стальныхфреймворках?kacetal
28.12.2024 20:56Может просто потому что это удобно, и у реакта есть инструменты для этого.
nin-jin
28.12.2024 20:56Удобно поддерживать 2 параллельные иерархии компонент, а для любой кастомизации копипастить умный компонент, собирая новый с нуля из глупых, провязывая их друг с другом и пачкой хуков вручную с постоянной борьбой с ререндерами? Расскажите же про эти крутые инструменты!
SergeiZababurin
const [loading, setLoading] = useState(false);
А так ведь на реакте уже не пишут. Для этого специальный хук есть уже.
Вы статью не успели написать, а код уже устарел.
Обожаю реакт за это.
А еще через пол года он без мата не будет запускаться, а через год проще новое будет с нуля написать, описывая самую чистую архитектуру в мире, которая скорее всего к написанию статьи уже устареет.
HungryGrizzzly Автор
Да, разделяю боль) я пример накидал по быстрому, так сказать, для наглядности, да и на реакте, в силу места работы, давно не писал) на самом деле, переезд на новые версии сейчас не является болью, обратная совместимость работает) в статье больше пытался сделать упор на глобальные вещи)
Подскажите, как хук называется?
SergeiZababurin
У меня правило, если можно React не использовать я его не использую. Ещё не было ни одного случая, когда React бы мне потребовался, кроме одного двух проектов, которые вставить к себе надо было.
Вот тут хук описан, про который я говорю.
https://habr.com/ru/articles/870216/
На работе я объяснил, почему реакт это в 90% случаев отвратительный выбор. И если мне скажут на нем писать я буду, если не скажут, я пишу без него.
Пока не заставили его использовать и слава богу.
А минусы за справедливое замечание по поводу реакта, это только говорит о токсичности реакт разработчиков.
kellas
Согласен, но это же относится не только к реакту, может только он чаще обновляется.
У нас когда-то была отлично написанная внутренняя админка на AngularJS, теперь это жуткое легаси. Сейчас испытываю огромную боль по поддержке проекта на RiotJS, который когда-то был примером хорошего масштабируемого кода. Вчера понадобилось внести изменения в свой максимально простой пет проект состоящий из одной страницы, без фреймворков, так там сборщик устарел, первый parcel , проект прост не запускался и не собирался. Сначала пришлось переписать сборку.
И даже при использовании только чистого js , без других утилит приходится обновлять кодовую базу, вот появился недавно structuredClone и на его фоне все эти JSON.parse(JSON.stringify( или какие-нибудь утилиты deepClone уже смотрятся "грязно"
Кажется это неизбежно
nin-jin
На $mol код за 10 лет почти и не устарел. И до сих пор служит примером хорошего поддерживаемого кода.