Чистая архитектура фронтенд приложений. Часть вторая

cca492ead8421a32a276212fbaa70ec5.jpg

Предисловие

В первой части я говорил общими терминами и больше рассуждал про бизнес-процессы (почитать можно тут). Далее я буду более детально углубляться в структуру проекта и частные случаи, постараюсь продемонстрировать разные подходы и порассуждать, чем они хороши или плохи. Но, для начала, еще пара общих слов. 

Ошибки — это норма 

Ошибаются все, от джунов до генеральных директоров. Важен не сам факт ошибки, а реакция на нее. Если пытаться сразу оправдать или начать отрицать ее, придумывать кучу отговорок или посыпать голову пеплом, то ничего хорошего из этого не выйдет. Напротив, если принять ее, проанализировать, разобрать причины, предшествовавшие им, и попытаться исправить данную ошибку, то можно неплохо прокачать свои навыки. Этим, по моему мнению, и отличаются младшие разработчики от средних, средние от старших, а старшие от руководителей групп или лидов. Не качеством и скоростью написания кода, хотя это тоже имеет место быть, но опытом и объемом проработанных ошибок (ошибки не обязательно должны быть допущены самим разработчиком).  

Ошибаться — это нормально. Из ошибок, своих и чужих, складывается ваш профессиональный опыт. Все зависит от реакции на ошибку. 

Чистый код — чистая архитектура

Можно сколько угодно тасовать систему папок и файлов, как колоду карт, решать, что выбрать, микросервисы или монолиты, подбирать фреймворки и библиотеки, но чистая архитектура невозможна, по моему субъективному мнению, без чистого и понятного кода. Само собой, чистота кода — дело субъективное, и да, не бывает эталонного кода, который можно поставить в палату мер и весов (разве что пустую строку). Всегда можно что-то улучшить, где-то подкрутить, что-то декомпозировать, оптимизировать и т. д. Но, как говорится, весь код чистым сделать мы не можем, но стремиться обязаны. Надо всегда держать в голове, что код мы пишем не только для того, чтобы закрыть фичу и отдать ее заказчику, но и для других разработчиков, которые будут с этим кодом работать. Возможно, этим разработчиком будете вы сами. 

При этом не надо изобретать велосипеды, все давно уже придумано. Иной раз даже принципов 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
	> {
	theme?: TButtonTheme;
	loading?: boolean;
}

export const Button: React.FC = ({
	children,
	theme = "primary",
	loading = "false",
	...restProps
}) => {
	return (
		
	);
};

Такой компонент соблюдает все правила. Он не контролирует свое состояние и не указывает, где и как его использовать, но имеет четкое и строгое внешнее 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) => {
		e.preventDefault();

        setLoading(true);
      
		fetch("someUrl.com", {
			method: "POST",
			body: JSON.stringify({ name, email }),
		}).finaly(() => setLoading(false));
	};

    if (loading) return ;

	return (
		
); };

Как мы видим, компонент выше контролирует состояния инпутов и свое собственное. Мы не можем никак повлиять на его поведение извне. Это и делает его умным. 

Данное разделение компонентов помогает нам структурировать код и придерживаться модульного подхода. Если перед реализацией каждого нового компонента мы будем думать, к какому типу нам следует его отнести и где он потенциально окажется через год или два, то станет гораздо проще масштабировать проект в будущем. 

Деление компонентов на умные и глупые помогает нам придерживаться модульной структуры.

В следующей статье я более подробно расскажу про структуры файлов и папок на проекте.

P.S. Если вам интересна какая либо из вышеперечисленных тем, то пишите в комментарии.

© Habrahabr.ru