Многослойная архитектура FrontEnd-приложений на основании SOLID, часть 1
Представьте образ, отражающий содержимое репозитория вашего проекта. Если он похож на захламленный балкон, то, вероятно, вы разработчик среднестатистического проекта. Если вы хотите делать проект, в котором все разложено по полочкам, то нужно следить как за качеством кода каких-то конкретных сущностей, так и всей архитектуры в целом. Но в основном сначала получается та самая картина с балконом.
И это абсолютно нормальная ситуация.
Давайте поговорим о том, как можно вылечить эту проблему. Отличительная особенность такого завала — «запашки» (code smells), если говорить языком разработки. Это код, который имеет признаки каких-то проблем системы.
В этом посте мы рассмотрим самые частые проблемы, которые встречаются в современных фронтенд-проектах. Примеры будут на React, однако аналогичная ситуация и в остальных современных фреймворках.
Основные проблемы кодовой базы
Проблема №1 — перегруженные интерфейсы. Это может быть и интерфейс TypeScript, и компоненты любых фреймворков. Проблема в том, что ваш компонент в какой-то момент начинает принимать слишком много разношерстных свойств.
propTypes — описания входных параметров компонентов на React
Проблема №2 — от компонентов можно ждать чего угодно. Смотрим на код компонента и видим EditTariff — некую страничку редактирования. Если мы заглянем в сам код, то увидим управление тарифами, переключалки, расчет цены, работу с cookies — все, что угодно.
Когда в компонентах можно найти любой код, содержимое которого вы не знаете, у вас есть данная проблема.
Проблема №3 — очень большие объемы кода. Тут просто надо представить, как мыслит человек. Что делает разработчик, которому предстоит поддерживать ваш код?
заходит в репозиторий,
открывает компонент
пытается понять, что же делает ваш тариф.
Для того чтобы решить свою задачу, ему нужно сперва впитать все, что происходит в этом компоненте. А возможно, и в соседних. Соответственно, на это уйдет куча времени.
Ниже неплохой пример — есть много вложений, которые что-то делают, что-то рендерят.
Проблема №4 менее заметна, потому что код внутри компонента может быть нормальным, однако ваши компоненты могут начать вкладываться так долго, что образуется «кроличья нора».
Это довольно важный пункт, так как для разработки конечного компонента вам нужно знать о контексте. Для оценки того, как какое-то свойство было прокинуто со стороны страницы и кто по пути мог его изменить, нужно понимать, что происходит на предыдущих шагах. По сути, это та же проблема, что и в третьем пункте, растянутая на большое количество файликов. Чтобы разобрать все, вам нужно прочитать промежуточные файлы.
Проблема №5 весьма банальна. Одинаковые изменения нужно вносить в нескольких местах.
Когда у вас есть несколько скопированных кусков кода и возникает какая-то задача, вам будет нужно вносить правки во всех местах. Я не раз видел такие задачки, когда мы используем какой-нибудь action добавления в корзину, а потом я открываю pull request и вижу, что такая одинаковая доработка была выполнена в шести разных action. Довольно часто такое бывает, кстати.
Проблему №6 в целом можно считать болезнью React. Она заключается в прокидывании свойств (props) — когда мы берем часть свойств в компоненте, а остальные просто прокидываем дальше. Многие исправляют эту проблему используя spread-оператор (…), сокращая тем самым явное описания и давая возможность расширения.
Но это ловушка, потому что, возвращаясь к проблеме кроличьей норы, вы заставляете читающего код искать, откуда эти свойства взялись.
На самом деле, это далеко не все проблемы, только часто встречающиеся. Но, чтобы существенно улучшить кодовую базу, нам достаточно полечить их. А поможет нам в этом SOLID.
SOLID
Я часто рассказываю про SOLID, это одна из ключевых вещей, которые помогли моему коду стать сильно лучше. SOLID представляет собой аббревиатуру из пяти принципов, используемых для упрощения классовых структур в объектно-ориентированном программировании.
Вопрос применимости SOLID в функциональном коде, сродни вопросу применимости паттернов проектирования. Последние тоже решают проблемы в ООП, но на деле их успешно применяют к коду, написанному в любой парадигме, что является большим преимуществом.
Из SOLID нам больше всего интересны первые два принципа и последний.
S — первая буква означает закон единой ответственности (single responsibility principle) и имеет для нас самое большое значение. Этот принцип гласит, что ваши компоненты должны иметь только одну причину для изменений. Не должно быть такого, что в вашем компоненте переплетаются несколько функциональностей, которые делают какие-то независимые вещи. Если это происходит, то вы нарушаете этот принцип и должны разделить их на несколько компонентов.
O — второй принцип сложнее, это Open/Close, суть которого состоит в том, что ваши компоненты должны быть открыты для расширения, но закрыты для модификации. Другими словами, компоненты должны быть написаны так, чтобы после написания не редактировать их код.
Однако вы должны дать возможность извне переопределять структуру компонента, чтобы можно было придумывать более широкие вещи. Например, вы делаете кнопку, в которую больше ничего не запихивается. Но вы должны передавать извне, какую реакцию она будет вызывать на клики. Этот пункт стыкуется с принципом композиции React, и это хороший паттерн, потому что извне вы можете передавать свойства, функции или даже куски React-дерева, тем самым не нарушая принципа.
Буквы L и I, обозначающие Liskov substitution principle и interface segregation principle, для нас не сильно важны, потому что они связаны с проектированием корректных интерфейсов, чтобы ваша модель работала правильно. Они больше относятся к объектному коду.
D — последний же принцип: dependency inversion principle. Он говорит, что вместо того, чтобы вкладывать компонент сверху вниз, вы можете идти в обратную сторону — вы делаете общий layout и вкладываете внутрь компонента сущность, которая находится выше по абстракции. То есть у вас есть приложение, и вместо того, чтобы в единое приложение вкладывать десять страниц, вы можете сделать десять страниц и вложить в них какую-то обертку приложения.
Абстракции
Абстракцию довольно сложно описать каким-то определением или термином, в котором не будет использоваться абстракция. Однако здесь можно подобрать синонимы: моделирование или генерализация.
Задача абстракции — придумать альтернативу, которая не получит привязки к бизнес-логике, но будет максимально описывать объект. Есть хорошее определение: абстракция — представление объекта через минимальное количество свойств, которые максимально подробно описывают его и операции с ним. В нашем же ключе абстракцией можно назвать абсолютно все, что мы пишем. Если взглянуть на файловую структуру проекта, то каждое название папки и файла — определенная абстракция.
Абстракцию можно разделить на два случая.
В первом этот термин будет использоваться в значении слоев абстракции, например, мы выносим папку components в проект, которая будет представлять собой абстракцию UI. Точно так же можно сделать папку store и вынести туда соответствующую абстракцию. Можно говорить об уровне ниже — пусть конкретный компонент представлен абстракцией, описывающей некоторую сущность.
Первоисточники проблем
Разложим наш список проблем по полочкам, руководствуясь описанными выше принципами.
Получается довольно интересная картина — практически в каждом пункте фигурируют single responsibility и абстракция. Это неудивительно, ведьsingle responsibility — самая популярная проблема, к которой можно свести практически все беды.
Когда компонент делает несколько действий, мы нарушаем этот принцип. Но весь код, с которым мы работаем, делает одновременно десятки вещей, хотя мы зачастую этого даже не замечаем. Если бы мы это убрали, то у нас получился бы максимально простой код. (два «бы» в одном предложении)
В первом пункте — перегруженные интерфейсы, явно нарушается single responsibility, потому что склеиваются независимые вещи — работа с layout и какие-нибудь фичи. Каждую из них можно вынести на отдельный компонент. («потому что» очень разговорный оборот, и он встречается в тексте 7 раз, аналогично с «какое-то», «какие-нибудь» — для разгвора это подходит, но для стаитьи нужно искать литературные аналоги)
Open/Close же говорит о том, что вы решаете задачу доработкой внутри компонента. Скорее всего, такие компоненты рождаются так: каждый раз, когда приходит фича, вы добавляете новый props. В одном проекте мы нашли страницу, которая разрослась до 4 000 строчек, у нее было порядка 150 props, а секция объявления занимала четыре экрана.
Когда мы это заметили, неслабо удивились и пошли разбираться. Выяснилось, что каждый член команды в рамках отдельной задачи добавлял по несколько строчекфункционала, в итоге страница благополучно разрослась до немыслимых масштабов.
Второй пункт — когда ожидаем от компонента чего угодно. Если ваш компонент решает множество проблем, то он явно непредсказуем.
Сюда же третья проблема — нужно вчитываться в большие объёмы кода.
Четвертый пункт, кроличья нора компонентов — единственный пункт, не содержащий нрушения single responsibility, потому что основное нарушение идет на dependency inversion principle, то есть мы вкладываем одни компонент в другой. Мы могли бы часть этих вложений убрать и развернуть их наоборот. Тут же Open/Close — если компонент включает в себя код следующих компонентов и надо внести изменения в самый глубокий компонент, то вы можете это сделать либо изнутри, либо прокинув через всех родителей какое-то свойство.
Одинаковые изменения нужно вставлять в нескольких местах — этот пункт больше всего нарушает принцип DRY, о котором мы не говорили. Он гласит, что не стоит копировать код (don«t repeat yourself).
И последний пункт — делегирование. Если задуматься, то делегирование свойств в компоненте — также нарушает single responsibility. Ведь прокидывание свойств — это отдельная задача. Достим компонент прокидывет данные в пять других, выходит одной из его задач является описание структуры компонентов, а прокидывание в данном случае можно отнести к постороннему шуму.