Современный веб — это сложно. Количество фреймворков и темп их развития заставляет разработчика скакать галопом. Кто-то новые либы юзает, кто-то модные книжки читает. Но иногда чтение и потраченные силы на углубление в архитектуру, ООП, TDD, DDD и т.д. не оправдывают ожидания. А порой книжки запутывают! И даже, самое страшное, неимоверно поднимают ЧСВ!
Я рискну по-простому изложить основную мысль Чистой Архитектуры применительно к фронтенду. Надеюсь, это будет полезно и для людей, которые хотят прочитать эту книжку, и для тех кто уже читал, но не использует полученные знания в реальной жизни. И для тех, кому интересно, как я сюда приплел фронтенд.
Мотивация
Впервые я прочитал ЧА за года полтора до написания статьи по совету одного из старших разработчиков. Перед этим меня сильно впечатлили краткие выдержки из Чистого Кода адаптированные под JavaScript (https://github.com/ryanmcdermott/clean-code-javascript). Я держал эту вкладку открытой на протяжении полугода, чтобы применять лучшие практики в своей работе. Причем, мои первые попытки прочитать оригинал Чистого Кода провалились. Возможно, потому что слишком сильно прилип к особенностям фронтенд-проектов, или потому что чтение должно быть закреплено продолжительной практикой. Но тем не менее ЧК — это практическое руководство, которое можно взять и сразу применить к написанной функции (очень советую прочитать в первую очередь, если ещё не знакомы).
А вот с ЧА все сложнее — здесь приводятся наборы принципов, законов и советов по построению программы в целом. Там нет конкретики, которую можно сразу взять и заюзать в компоненте. Эта книга призвана изменить отношение к написанию ПО, и дать вам мысленные инструменты и метрики. Ошибочно считать, что ЧА полезна только архитекторам, и я постараюсь донести это на адаптированном под фронтенд примере.
Бизнес-логика и фронтенд
Когда речь идет о ЧА, у многих людей в воображении рисуются кружочки (рис. под заголовком), гигантских размеров проекты, невероятно сложная бизнес-логика, и куча вопросов — а по каким папочкам раскладывать ЮзКейсы? Мысль о применимости принципов из ЧА к созданию компонента аккордеона вводит в недоумение. Но проблемы, которые возникают при разработке современных интерфейсов требует серьезного отношения. Современные интерфейсы — это сложно, и часто мучительно. В первую очередь давайте разберемся, что на фронтенде самое важное.
Всем давным-давно известно, что нужно бизнес-логику от представления отделять, и принципы SOLID соблюдать. Дядюшка Боб в ЧА расскажет вам об этом очень подробно. Но что такое бизнес-логика? Р. Мартин предлагает несколько определений и подкатегорий, одно из них звучит примерно так:
Бизнес-логика (бизнес-правила) — это правила, которые приносят организации деньги даже без средств автоматизации.В общем, бизнес-логика, это что-то очень важное. (Слышу, как бэкендеры хихикают, когда слышат про бизнес-логику от фронтов). Но я предлагаю нам фронтендерам немного расслабиться и вспомнить, что у нас на фронте может быть очень важным? И как бы это странно ни звучало, самым важным на фронте является пользовательский интерфейс. Первым шагом к пониманию ЧА для меня стало осознание того, что на фронте в центре кружочка должна быть логика интерфейса! (рис. под заголовком).
Я понимаю, что это голословное утверждение, и с ним можно сильно поспорить. Но давайте вспомним, что у нас на фронте меняется часто и больно? Не знаю как вас, а меня чаще всего просят менять поведение интерактивных элементов — аккордеонов, формочек, кнопочек. Стоит отметить, что верстка (дизайн) меняется намного реже поведения интерфейса. В ЧА особо обсуждаются потоки изменений и акторы (инициаторы изменений), обратите внимание.
Противная формочка
Давайте не будем пока сильно заморачиваться за терминологию. Приведем пример и немного проясним, что я называю логикой интерфейса.
Есть пара инпутов с валидацией и условным отображением. Можно заполнить только одно поле, и форма валидна, но появятся опциональные инпуты. (Обратите внимание, нам безразлично что там за данные. Главное — интерактивность)
Надеюсь, логика понятна. И на первых порах реализация не вызывает вопросов. Вы засовываете это дело в компонент своего фреймворка и смело переиспользуете в других формочках как составную часть и самостоятельную. Где-то приходится передавать флаги, чтобы чуток поменять валидацию, где-то немного верстку, где-то особенности подключения к родительской форме. В общем, кодите по тонкому льду. И однажды, вы получаете задачу по этой форме (и использующим ее) и зависаете на пару дней, хотя казалось бы дел на 15 минут. Проклинаете менеджера, формочки и скучные тупые таски.
В чем ошибка? Казалось бы, вы не первый день работаете, отлично продумали композицию компонентов, попробовали разные хоки, передачу темплейтов через пропсы, колдовали с фреймворком по построению форм, даже старались следовать SOLID при написанисании этих компонентов.
Note: компоненты в ЧА !== компоненты в реакт/ангулар и ко.
А дело в том, что вы забыли выделить логику. Немного успокоимся, вернемся к задаче и поиграем в моделирование.
Слишком простая задача
В ЧА подчеркивается, что для больших проектов архитектура имеет критическое значение. Но это не отменяет полезности подходов ЧА и к маленьким задачам. Нам кажется, что задача слишком проста, чтобы говорить об архитектуре какой-то формочки. А как обозначить способ внесения изменений, если не через архитектуру? Если не определить границы и составные части интерактивного интерфейса, запутаться будет еще проще. Вспомните с какими мыслями вы берете таск по изменениям формы на своей работе.
Но давайте к делу. Что это за формочка? Пробуем моделировать, излагая мысли псевдокодом.
ContactFormGroup
+getValue()
+isValid()
По-моему, вся наша задача для внешнего мира сводится к созданию объекта с двумя методами. Звучит легко — так оно и есть. Продолжим описывать, что видим и что нас интересует.
ContactFormGroup
emailFormGroup
phoneFormGroup
getValue()
=> [emailFormGroup.getValue(), phoneFormGroup.getValue()]
isValid()
=> emailFormGroup.isValid() || phoneFormGroup.isValid()
Наверно, стоит явно обозначить видимость второстепенных инпутов. Когда менеджер просит быстренько внести 10-ую правку в форму, то в его голове все выглядит просто — прям как этот псевдокод.
EmailFormGroup
getValue()
isValid()
isSecondaryEmailVisible()
=> isValid() && !!getValue()
Можно наметить место для странных требований…
PhoneFormGroup
getValue()
isValid()
isSecondaryPhoneVisible()
=> isValid() && today !== ‘sunday’
Так могла бы выглядеть одна из реализаций нашей формочки на Angular.
export class ContactFormGroup {
emailFormGroup = new EmailFormGroup();
phoneFormGroup = new PhoneFormGroup();
changes: Observable<unknown> = merge(this.emailFormGroup.changes, this.phoneFormGroup.changes);
constructor() {}
isValid(): boolean {
return this.emailFormGroup.isValid() || this.phoneFormGroup.isValid();
}
getValue() {
return {
emails: this.emailFormGroup.getValue(),
phones: this.phoneFormGroup.getValue(),
};
}
}
export class EmailFormGroup {
emailControl = new FormControl();
secondaryEmailControl = new FormControl();
changes: Observable<unknown> = merge(
this.emailControl.valueChanges,
this.secondaryEmailControl.valueChanges,
);
isValid(): boolean {
return this.emailControl.valid && !!this.emailControl.value;
}
getValue() {
return {
primary: this.emailControl.value,
secondary: this.secondaryEmailControl.value,
};
}
isSecondaryEmailVisible(): boolean {
return this.isValid();
}
}
Таким образом, мы получаем три интерфейса (или класса, не важно). Следует поместить эти классы в отдельный файл на видном месте, чтобы можно было разобраться в подвижных частях интерфейса, просто заглянув в него. Мы выделили, вытащили и подчеркнули проблемную логику, и теперь управляем поведением формочки, комбинируя реализации отдельных частей ContactFormGroup. А требования для разных вариантов использования можно легко представить в виде отдельных объектов.
Кажется, это стандартная реализация паттерна MVC, и не более того. Но я бы не стал пренебрежительно относиться к элементарным вещам, которые на практике вообще не соблюдаются. Смысл не в том, что мы вытащили кусок кода из представления. Смысл в том, что мы выделили важную часть, подверженную изменениям, и описали ее поведение так, что она стала простой.
Итого
ЧА рассказывает нам о законах написания ПО. Дает метрики, по которым мы можем выделять важные части от второстепенных и правильно направлять зависимости между этими частями. Описывает преимущества ООП и подходов к решению задачи через моделирование.
Если вы хотите улучшить качество своих программ, сделать их гибкими, использовать в своей работе ООП, научиться управлять зависимостями в вашем проекте, говорить в коде о решении задачи, а не о деталях вашей библиотеки, то я очень рекомендую прочитать Чистую Архитектуру. Советы и принципы из этой книги актуальны для любого стека и парадигмы. Не бойтесь экспериментов и на своих задачах. Удачи!
P.S. О стейт-менеджменте
Очень большим препятствием для понимания ЧА может стать приверженность библиотеке для стейт-менеджмента. На самом деле, такие библиотеки как redux/mobx попутно решают задачу оповещения компонентов об изменениях. И для некоторых разработчиков фронт без стейт-менеджера — что-то немыслимое. Я считаю, что принципы ЧА можно применять и с использованием стейт-менеджера и без него. Но сделав упор на стейт-менеджмент библиотеку, часть гибкости вы потеряете неизбежно. Если мыслить в терминах ЧА и ООП, то понятие стейт-менеджмента вообще отпадает. Простейшая реализация без стейт-менеджмента здесь habr.com/ru/post/491684
P.P.S.
Честно говоря, я показывал решение похожей интерфейсной задачи своему другу — он не оценил, и переписал все на реакт-хуки. Мне кажется, что в большей степени отторжение происходит из-за того, что в реальных проектах ООП практически не используется, и у большинства молодых фронтендеров нет ни малейшего опыта с ООП решениями. На собеседованиях бывает спрашивают про SOLID, но часто лишь чтобы отсеять кандидатов. Более того, порывы к развитию в области ООП в некоторых командах могут пресекаться на ревью. И людям часто проще разобраться в новой библиотеке, чем прочитать скучную книжку или отстоять свое право на вынесение логики из компонента. Прошу, поддержите ООП активистов :)
AriesUa
Не читал данную книгу. Но за почти 20 лет программирования из них последние 10 лет отдано фронту. За это время пришел к простым правилам написания фронта. Конечно, где когда-то что-то подсмотрел, к чему-то сам пришел. Итак мои небольшие правила:
И еще мое любимое выражение — «Ребята, не усложняйте. Чем проще, тем лучше.»
Justerest Автор
Я поддерживаю ваши правила. Вроде, в статье им не противоречил.
Но хотел бы уточнить, а что вы называете бизнес-логикой на фронте?
AriesUa
Я не протеворечу, что вы. Просто решил дополнить статью своими мыслями.
Что касается вашего вопросы, то:
Логика компонентов — код который нужен для рендера элемента. К примеру, в реакте/ангуляре мы что-то показываем, а что-то скрываем исходя из внешних условий.
Бизнес логика или логика приложения — это сервисный слой. Где уже будет создание сущьностей. Их обработка и хранение. Запросы к серверу и обработка ответов. Данный слой не зависит от фреймворков и компонентов. А значит он может быть выделен даже в отдельный проект.
Justerest Автор
В статье я как раз хотел показать, что логика компонентов (логика интерфейса) бывает сложной, часто меняется и зависит, как вы говорите, от множества внешних условий. И я предлагаю ее выделять, делать простой и независимой от фреймворка. Все в разумных пределах, то что реально потом будет больно менять. Печально, что у меня плохо получилось это донести.
Да, сервисный слой для получения/сохранения данных необходим. Чаще всего он сам по себе получается независимым, потому что контроль управления исходит от компонента фреймворка. Обязательно его нужно выделять. Но как по мне, с точки зрения ЧА для фронта этот слой будет деталью. С ним редко бывают проблемы, если апи стабильное.
Я призываю пересмотреть приоритеты на фронте. Интерфейсы сейчас сложные, требования меняются по разными причинами. Я хотел бы, чтобы логику из компонентов почаще вытаскивали, хоть в ней и нет осязаемых сущностей типа employee. Нам нужны сущности для интерфейсной логики.