Создаем модульную архитектуру для большого React-приложения

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Від редакції: матеріал готувався до повномасштабного вторгнення росії на територію України. Привет, меня зовут Антон Пинкевич, я Front-end Tech Lead в Universe, продуктовой компании из экосистемы бизнесов Genesis. Front-end разработкой я занимаюсь уже больше восьми лет. Много времени отдал архитектуре проектов на React: мне всегда было интересно искать варианты ее улучшения. В этой статье хочу рассказать о своём опыте создания и внедрения нового архитектурного паттерна в edtech-платформе компании Universe. Это огромный продукт, и важно наладить внедрение большого количества функций малыми усилиями. Поэтому моей задачей было сделать так, чтобы при росте системы время разработки нового функционала оставалось линейным. Материал будет интересен, в первую очередь, всем, кто пишет на React, и Middle+ разработчикам, которые хотят улучшить свои приложения.

Ссылка на репозиторий с примером: github.com/...​ych/modular-react-example (будет дополняться)

Идея

React — отличный, простой фреймворк, но на мой взгляд, ему не хватает структуры. Разработчики этого фреймворка советуют использовать context’ы для инкапсуляции бизнес-логики. Это помогает, но есть нюанс: при обновлении любого свойства в одном из контекстов, обновляется всё дерево компонентов, что негативно влияет на производительность приложений. С другой стороны, есть Redux, он ставит всё на рельсы, но даже с ним сложно работать в приложении из-за многочисленных связей между компонентами и обилия скрытой логики в middlewares. При росте системы всё это превращается в большой комок грязи (big ball of mud) и распутать эти взаимосвязи становится очень сложно. На создание текущей архитектуры меня вдохновил RIBs (router-interactor-builder), разработанный компанией Uber для своих мобильных приложений. У них хорошо реализована инверсия зависимостей и поток данных. Именно эту архитектуру я немного доработал, и в итоге получилось нечто вроде Angular для React. Основная цель заключалась в том, чтобы сделать систему максимально гибкой на любых масштабах. Мы не делали свой фреймворк, а просто договорились о том, какие файлы будут в системе и как система будет из этих блоков выстраиваться. Это позволило делиться логикой между командами и «общаться» на одном языке.

Преимущества нашей архитектуры

  • Она позволяет развернуть зависимости приложения и сделать так, чтобы бизнес-логика отвечала за презентацию, а не наоборот, как обычно бывает в веб-приложениях.
  • Разделяет приложение на независимые модули, позволяя переиспользовать большую часть написанного кода, миксуя модули между собой.
  • Обладает высокой тестируемостью кода. Каждый модуль разделен на дополнительные унифицированные компоненты, поэтому можно писать тесты только для бизнес-логики, игнорируя инфраструктуру.
  • В нашем случае унифицирует написание React-кода, чтобы вся команда работала по одинаковым шаблонам.

Далее расскажу подробней обо всех элементах системы, которые можно использовать для расширения в разные стороны. Преимущество модульности в том, что можно начать с малого и добавлять новые файлы по необходимости. Эту архитектуру можно использовать с любым фреймворком для React: create-react-app, nextjs и другими.

Абстрактная аналогия

Для более легкого восприятия информации возьмем аналогию с процессом работы небольшого ресторана. Далее расскажу как это переложить на React. Итак, давайте представим себе ресторан. У нас есть:

  • три сотрудника (хостес, официант, повар);
  • список столиков;
  • терминал для приёма платежей;
  • кухня;
  • список заказов, которые находятся в работе;
  • место выдачи заказов с кухни.

Задача

К нам приходит клиент и его нужно провести по всем этапам взаимодействия:

  1. Посадить за столик.
  2. Принять заказ.
  3. Приготовить заказ.
  4. Подать заказ.
  5. Получить от клиента оплату.

Посадка за столик

  1. Клиент сообщает хостес свои пожелания.
  2. Хостес находит нужный столик.
  3. Проводит к нему клиента.

Прием заказа

  1. Далее официант видит клиента за столиком.
  2. Принимает заказ.
  3. Отправляет заказ в очередь на приготовление.

Приготовление заказа

  1. Повар видит новый заказ в списке.
  2. Готовит его на кухне.
  3. Как только заказ готов, ставит его на место выдачи.

Подача заказа

  1. Официант видит приготовленный заказ.
  2. Забирает и относит его клиенту.

Приём платежа за заказ

  1. Как только клиент закончил, официант это видит и рассчитывает клиента.
  2. Для этого он использует сервис приёма платежей.

Теперь давайте отсортируем сущности, которые у нас были похожи по свойствам:

  • Модули — сотрудники.
  • Хранилища — список столиков, список заказов и место выдачи заказа с кухни.
  • Сервисы — кухня и терминал для приема платежей.

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

  • Он может самостоятельно принимать решения (interactor).
  • Знает о том, где взять нужную ему информацию (index).
  • Имеет определенный внешний вид (router).

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

Модуль (Module)

Основной строительный блок системы — Модуль (Module). Это независимая единица, которая содержит некоторое инкапсулированное поведение. При этом модуль может существовать без визуальной презентации. Это позволяет развернуть стандартные зависимости приложения и сделать так, чтобы View не управлял всем приложением, а перестраивался в зависимости от нужного поведения. В нашей архитектуре мы описываем, что должно быть в системе. По умолчанию, логика лежит возле каждого модуля. При этом не все модули содержат визуальную презентацию: жёлтым выделены модули, которые выполняют логику, но не отображаются пользователю в интерфейсе. Это важное изменение позволяет нам развернуть зависимости. Теперь приложение зависит не от визуального слоя, а от логического. Кроме того, это делает нашу архитектуру «кричащей», как завещал дядюшка Боб.

Из чего состоит модуль

  • Базовый модуль состоит из двух файлов: Index, Interactor.
  • Если в модуле нужна визуализация — добавляем Router.
  • Если модуль состоит из сложной визуализации, к примеру есть несколько А/B тестов, которые отличаются только визуально, но имеют общую логику — добавляем View (или несколько, по необходимости).

На схеме стрелками обозначены зависимости между компонентами модуля

Interactor

Этот файл содержит бизнес-логику. Желательно структурировать систему так, чтобы интерактор был изолирован от внешнего мира и получал нужные зависимости через пропсы. Также хорошей практикой считается делать его хуком.

type Payload = {
  authenticationService: IAuthenticationService
  authenticationStore: IAuthenticationStore
  router: IRouter
}
 
interface IUserSignupByEmailInteractor {
  redirectToSignin: () => void
  passwordRecoveryUrl: string
  children: {
	signupByEmail: boolean
	signupByGoogle: boolean
  }
}
 
const useSignupPageInteractor = ({ authenticationService, router, authenticationStore }: Payload): IUserSignupPageInteractor => {
  // logic implementation here
 
  return {
	redirectToSignin: () => router.redirect('/signin'),
    passwordRecoveryUrl: '/password-recovery',
	children: {
  	signupByEmail: true,
  	signupByGoogle: canUserSignupByGoogle,
	}
  }
}

Так мы получаем нужное хранилище и сервис из пропсов. Сначала обозначаем, какие дочерние модули может рендерить данный модуль. Затем выполняем проверки и возвращаем булевые значения для каждого дочернего модуля. Также добавляем другие пропсы при необходимости. Рендер реактовских компонентов будет выполняться уже в роутере.

Router

Файл, который связывает бизнес-логику и визуализацию. Другими словами, прокидывает props в компоненты, вызывает нужные коллбэки и расставляет дочерние модули в layout’e.

interface IProps {
  signupByEmail: React.ReactNode
  signupByGoogle: React.ReactNode
  interactor: IUserSignupByEmailInteractor
}
 
const SignupPageRouter: React.FC = ({ signupByEmail, signupByGoogle, interactor }) => (
  <>
	
	
	{interactor.children.signupByEmail && signupByEmail}
	{interactor.children.signupByGoogle && signupByGoogle}
  </>
)

Роутер получает все нужные зависимости через пропсы, и рендерит нужные модули и компоненты. Содержит только логику проверок if else чтобы понять, нужно ли рендерить тот или иной модуль.

View

Задача View — группировать разные «глупые» компоненты (dumb components).

interface IProps {
  link: string
}
 
// classNames опущены для лучшей читабельности
const ForgotPassword: React.FC = ({ link }) => (

Forgot password?

Recover here
)

Index

Собирает нужные зависимости для интерактора и роутера. В нём мы вызываем все useContext, загружаем нужные сервисы и хранилища.

const SignupByEmail = () => {
  con†st { authenticationService } = useServices()
  const { router } = useUtils()
  const { authenticationStore } = useStores()
 
  const interactor = useSignupByEmail({ authenticationService, router, authenticationStore })
 
  return (
	}
  	signupByGoogle={}
  	interactor={interactor}
    />
  )
}

Основные файлы для построения модуля — Index и Interactor. Остальные добавляем по необходимости. Для удобства модули без UI можно назвать Activity, а те, что с UI, оставить как и есть — Module. До тех пор, пока нам не нужно расширение системы, можем хранить состояние прямо внутри модулей через useState. Для общих данных можно создать базовый createContext. О том, как правильно хранить данные для масштабирования, расскажу дальше. Для получения данных из внешних источников (API и другие сервисы) можно писать небольшие функции прямо в index файлах. Минимальное приложение может выглядеть вот так:

Сервисы (Services)

Если нужно использовать логику получения данных из внешних источников в разных модулях, выносим ее в отдельные сервисы. Сервис — это простой request-response механизм. Он может хранить внутреннее состояние, но только то, которое ему нужно для успешного выполнения запросов. Это может быть закэшированное состояние или токен. О самом механизме кэширования много информации в интернете. Для него также можно использовать библиотеки по типу react-query. Примеры сервисов:

  • Authentication — проверяет credentials пользователя, позволяет зарегистрироваться/залогиниться, восстановить пароль и т. д.
  • Analytics — отправляет аналитику.
  • Payment — обрабатывает платежи.

Реализовать сервис можно абсолютно разными способами. Я советую разбивать сервисы на небольшие логические блоки при проектировании и реализовывать их в классах. Это упрощает их использование. Приложение с подключенными сервисами:

Хранилища (Stores)

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

  • Authentication — хранит данные аутентификации, такие как token, refreshToken и другие.
  • User — хранит данные пользователя. Имя, email и другие.

Для реализации хранилищ мы используем Mobx. Он простой, быстрый и позволяет проектировать необходимую систему без сложной структуры (в отличии от Redux). Но для каждого проекта лучше подбирать свою систему хранения. Приложение с подключенными хранилищами: Сервисы и хранилища позволяют реализовать флоу любой сложности. К примеру, нам нужно получить и сохранить токен пользователя. Для этого создаём модуль Authentication, который может ходить в сервис аутентификации, запрашивать у него токены и сохраняет их в authentication store. Для того, чтобы добавить сохранять хранилища локально, можно добавить ещё один модуль PersistStores, который будет отвечать за то, чтобы сохранять хранилища при изменении и загружать их во время загрузки приложения.

Адаптеры/Шлюзы (Adapters/Gateways)

При росте системы сервисы и хранилища могут использовать разные адаптеры. К примеру, analytics service изначально может использовать только Google Analytics для отправки данных, но позже могут добавиться Facebook Pixel, Amplitude, Mixpanel и другие. Для того, чтобы не писать новый сервис каждый раз, достаточно просто передать нужный адаптер в сервис. Таким образом, в сервисе появляется новая зависимость — analytics adapter. Интерфейс этого адаптера описывается в сервисе, а реализуется уже внешними адаптерами. Например:

export interface IAnalyticsAdapter {
  sendEvent: (eventName: string, eventData: unknown) => Promise
}
 
export class AnalyticsService {
  private adapter: IAnalyticsAdapter
 
  constructor(adapter: IAnalyticsAdapter) {
	this.adapter = adapter
  }
 
  // ... implementation
}

И далее делаем нужные адаптеры

class AnalyticsAdapter implements IAnalyticsAdapter {
  private adapters: IAnalyticsAdapter[] = []
 
  constructor(adapters: IAnalyticsAdapter[]) {
	this.adapters = adapters
  }
 
  sendEvent: IAnalyticsAdapter['sendEvent'] = (eventName, eventData) => {
	this.adapters.forEach((adapter) => adapter.sendEvent(eventName, eventData))
  }
}
 
class GoogleAnalyticsAdapter implements IAnalyticsAdapter {
  sendEvent: IAnalyticsAdapter['sendEvent'] = (eventName, eventData) => {
	// google analytics implementation
  }
}
 
class FacebookPixelAdapter implements IAnalyticsAdapter {
  sendEvent: IAnalyticsAdapter['sendEvent'] = (eventName, eventData) => {
	// facebook pixel implementation
  }
}
 
export const analyticsAdapter = new AnalyticsAdapter([
  new GoogleAnalyticsAdapter(),
  new FacebookPixelAdapter(),
])

И используем в сервисе:

const analyticsService = new AnalyticsService(analyticsAdapter)

Приложение с адаптерами:

Внедрение зависимостей (Dependency Injection)

Сервисы и хранилища необходимо как-то связать с модулями. Для этого проще всего использовать контексты React. Например:

interface IServices {
  authenticationService: IAuthenticationService
  analyticsService: IAnalyticsService
}
 
const ServicesContext = createContext(null)
 
export const useServices = (): IServices => useContext(ServicesContext)
 
export const ServicesProvider: React.FC = ({ children }) => {
  // initialize services
 
  return (
	
  	{children}
	
  )
}

Пример того, как это выглядит в дереве файлов: Оборачиваем модуль, или страницу (в случае nextjs можно _app.tsx) максимально выше по дереву компонентов в данные контексты и позже используем в коде, как показано в примере index файла в модуле.

  
  	
  

Приложение с внедрением зависимостей: В нашей системе сервисы и хранилища это обычные классы, которые не обновляются при изменении состояния (ссылка остаётся постоянной). Таким образом у нас нет никаких лишних обновлений в системе.

Кейсы использования (Use cases)

При росте количества модулей будет появляться общая логика. Чтобы не копировать ее из одного места в другое, можно выносить эту логику в Use cases. К примеру, нам нужно реализовать покупку, которую совершает пользователь. Обычно она состоит из нескольких шагов:

  1. Запросить цену на продукт по ID.
  2. Загрузить данные для шлюза оплаты.
  3. Отобразить шлюз оплаты пользователю в модальном окне.
  4. Подождать ввода данных от пользователя.
  5. Верифицировать покупку.
  6. Открыть доступ к нужному продукту пользователю (обновить данные в хранилищах).

Чтобы каждый раз не писать эти шаги заново, мы выносим логику в processPaymentUseCase. Use case’ы могут использовать и сервисы, и хранилища через контексты, о которых я упоминал ранее. Они априори не могут использоваться вне модулей, поэтому можно сказать, что сервисы и хранилища будут всегда доступны. Приложение с кейсами использования:

Модели (Models)

Если полученные из внешних источников объекты нужно использовать в нескольких модулях и к ним могут применяться однотипные действия, их можно выделить в отдельную сущность «Модель». Это позволит валидировать данные, применять к ним общую логику и хранить в одном месте. ⚠️ Важно. Модели в этой архитектуре являются изолированными и не имеют доступа к внешним источникам. Если нужно сохранить модель в базе данных, используем modelNameService и т. д. Это нужно для создания юнит-тестов для всех моделей. Примеры моделей:

  • Product.
  • Course.
  • Lesson.

Приложение с моделями:

Утилиты (Utils)

Без них невозможно комфортно работать. Сюда относится всё, что помогает в разработке, например:

  • Token parsers.
  • Date formatters.
  • Device type detection.

Утилиты — это все, чему не нужно иметь доступ к внешним источникам. При этом их можно использовать как в сервисах, так и в модулях.

Обработка ошибок/исключений (Exceptions handling)

⚠️ Важно. Все ошибки должны обрабатываться в модулях. Сервисы просто возвращают исключения. Это нужно для того, чтобы логика обработки ошибок была в одном месте. Для типизации мы используем тип Result, который возвращается при вызове методов в сервисах.

export type Result<R, E extends Error> = R | E

Пример использования:

class MyError1 extends Error {}
class MyError2 extends Error {}
 
// example-service.ts
class ExampleService {
  example = (): Result<string, MyError1 | MyError2> => {
	// implementation
  }
}
// example-module/interactor.ts
const useExampleInteractor: React.FC = ({ exampleService }) => {
  useEffect(() => {
	exampleService.example()
  	.then((result) => {
    	// typeof result = string | MyError1 | MyError 2
    	if (result instanceof MyError1) {}
    	// typeof result = string | MyError2
    	if (result instanceof MyError2) {}
    	// typeof result = string
    	// do whatever you want with the pure result type
  	})
  }, [])
 
  return {}
}

Итоги

Модули — это связующее звено между получением данных (сервисы) и их хранением (хранилища). Для упрощения взаимодействий модули используют кейсы (use cases) и утилиты. Разбивка по зонам позволяет сделать так, чтобы каждая зона системы была ответственна только за одну задачу (single responsibility principle). Пример структуры полного приложения: И пример файловой структуры: Данная архитектура веб-приложения позволяет гибко растить проект во все стороны, при этом логика остаётся инкапсулированной и понятной. Не нужно вчитываться в код, чтобы понять, как разные компоненты системы взаимодействуют между собой. Вероятно, покрыты не все проблемы, которые могут появиться в работе над проектом, но текущий подход позволяет нашей команде двигаться быстро в разработке и постоянно добавлять новые функции, при этом не трогая старые.

Сподобалась стаття? Натискай «Подобається» внизу. Це допоможе автору виграти подарунок у програмі #ПишуНаDOU

👍ПодобаєтьсяСподобалось11
До обраногоВ обраному13
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

интересно, спасибо

а вы в сторону Remix смотрели?

Ну вот как раз текущий сетап и сделан на nextjs :)

Смотрели, но он вышел уже после того, как мы начали делать на nextjs. Поэтому на нём и остановились.

Remix це не про архітектуру , а скоріше про DX

Для подобной ситуации есть еще один неплохой вариант — «взял Angular и написал» )))

Якраз хотів написати «поступово повторно винаходиться Ангулар»)))

Розписано дуже гарно та структуровано. В цілому зрозуміло якими методами ви досягли поставлених цілей. Але як на мене, виглядає дещо переускладнено місцями. У вас випадково не буде якогось маленького приклада написаного по цій архітектурі/методології?

Дякую за відгук і запит. Я думаю зможу підготувати гарний приклад і викласти його до кінця цього тижня 🙌

Місцями дійсно це оверкіл, зокрема для маленьких проектів. Це дуже схоже з графіком часу/складності проекту у DDD (domain-driven design): khalilstemmler.com/...​img/wiki/anemic/chart.svg (зелене — проект на модульній архітектурі, червоне — проект на контекстах)

Додав посилання на репозиторій, дублюю тут: github.com/...​ych/modular-react-example

Дякую за посилання. Подивився, потицькав і ось шо хочу сказати.
Спочатку те шо не стосується архітектури:
1. В мене мабудь дитяча травма пов’язана з Ramda, але коли я бачу це слово в проекті у мене починає смикатися око. Бо я такий трешак бачив, коли люди самі не розуміли через день шо вони там написали. Але то таке.
2. Кинулося в око використання MobX.subscription в useEffect. Я розумію для чого це зроблено, але наразі виходить так, що ми юзаємо «реактивний» MobX, але все ще смикаємо хуки вручну. Але то теж таке ))))
3. Один тест в мене таки впав )))

Щодо архітектури:
1. Виглядає дійсно дуже професійно і так би мовити по ентерпрайзному. Все дуже гарно розкладено, структуровано, послідовно, але....
2. Дуже не зрозуміло щодо того як воно все працює. Я говорю в розрізі того що це не схоже на web додаток. Не прослідковується структура додатку, не дуже зрозуміло який модуль де буде вивидитись, що від чого залежить, бо ми не маємо ні структури роутів, ні візуальної ієрархії. Я розумію що я витратив на це 15 хвилиночок, а ті люди які з цим працюють то вони і розуміють більше і краще і ця архітектура не викликає таких питань. Але мені написане кажеться дуже переускладненим і я з жахом собі уявляю дійсно великий додаток на цій архітектурі. Це звісно може бути вкусовщиною, бо я просто прихільник простоти і лаконічності, але ні в якому разі не кажу що ваша архітектура погана. Вона просто для мене надважка.
3. Тести писати дійсно дуже круто і легко. Це прям для мене виглядає як найкраща частина того що я побачив у прикладі

Прошу не приймати мої слова персонально, я лише хочу висвітлити свої думки. В будь якому разі дуже дякую вам за такий труд щодо опису архітектури і особливо за надання прикладу. Такі статті розвивають фронтенд-комьюніті!

3. Один тест в мене таки впав )))

Писав уночі ¯\_(ツ)_/¯
Репозиторій ще буду доробляти, хочу зробити гарний приклад.

Кинулося в око використання MobX.subscription в useEffect

Ми тільки ініціюємо інтерактори, щоб вони могли виконувати свою роботу і реагувати на зміни у сховищах. У данному прикладі ми не викликаємо методи у інтеракторах вручну, а тільки кажемо коли їх робота розпочалася.

І я все розімую, не існує магічного рішення для всіх проектов. Це тільки моє бачення того, як може виглядати комфортна робота великої команди.

Дякую велике за відгук і за те, що прочитали статтю 🙌
Обговорення важлива частина розвитку. А критика допомагає виявити проблеми.

---

Як додатковий висновок за останні 2 місяці, коли стаття готувалася до поблікації, такі собі плюси і мінуси даного підходу з точки зору команди.

Плюси:
1. Ми можем вчотирьох працювати над проектом і додавати функції незалежно один від одного. В нас ще не було конфліктів у коді;
2. Джуніорам можна давати працювати над версткою, і щоб над логікою працював мідл/сеньор. Це трапляється із-за того, що в нас розділена візуальна і логічна частина додатку;
3. Весь додаток покритий юніт-тестами, що спрощує тестування перед релізом;
4. Коли треба зробити А/Б тест з новою логікою/флоу, то ми просто додаємо новий модуль, не змінючи вже існуючі. А потім заміняємо модуль на потрібний для кожного окремого тесту.

Мінуси:
1. Онбордінг нової людини (джуніора) займає 2 місяці. Сеньор почав працювати вже через тиждень;
2. Архітектура не схожа на популярні рішення для реакту, тому виникає новий барьер для нових людей, кто не готовий вчитися. Але це як додатковий етап при наймі 😅

Ну якщо порівнювати плюси і мінуси, то як на мене мінуси тут перекривають. Зараз спробую пояснити чому

Ми можем вчотирьох працювати над проектом і додавати функції незалежно один від одного. В нас ще не було конфліктів у коді;

Це можливо досягнути і без такого складного підходу

Весь додаток покритий юніт-тестами, що спрощує тестування перед релізом;

Це теж не прерогатива вашої архитектури

Онбордінг нової людини (джуніора) займає 2 місяці. Сеньор почав працювати вже через тиждень;

Як на мене, якщо сіньору треба тиджень шоб розібратися, то тут або в сіньорі проблема, або дійсно в архітектурі.

Я знов хочу наголосити на тому що я не кажу що вона погана, я лише кажу що занадто складна. І тих плюсов що ви досягли можна досягнути і меньшими зусиллями. Але ваша доповідь є прикладом того, що вона (архітектура) таки працює і для вас мінуси не такі значущі.

Архітектура не схожа на популярні рішення для реакту,

Тут нажаль пустеля, всі більш меньш нормальні підходи що я бачив концентруються на тому щоб спростити DX розводячи магію під капотом. Так що насправді тут непахане поле для експериментів. Єдине цікаве що я можу згадати то feature-sliced-design, але там теж заплутана штука.

@Dmytro Braginets,

Це можливо досягнути і без такого складного підходу

чи можете ви поділитися статтями/підходами, що за вашою думкою/досвідом мають такі ж плюси і в цілому виглядають легше з архітектурної точки зору з можливістю подальшого масштабування

Нажаль я поки що не бачив описаних статей та підходів які б виглядали легше. І я розумію як це звучить, мовляв «чого ж ти тогда тут повітря розріджуєш» ))) Але у мене на цей рахунок є така думка, що на фронті підходи намагаються притягнути з бека і тому вони виявляються трохи перевантаженими.

У нашій компанії ми тестуємо так називаєму widget-based-arhitecture яка по своїй сутності проста як п’ять копійок. Так це і не архітектура в цілому а більше підхід. Вже два проекти успішно по ній їдуть, і я маю бажання написати про це статтю на доу, але трохи пізніше.

Так що поки я нажаль не можу поділитися посиланням на шось просте і цікаве.

я маю бажання написати про це статтю на доу, але трохи пізніше

де можливо підписатися, щоб не пропустити?

Та можливо тут прям. Але я не знаю як працює система підписок на доу ))))

Підписатись на коментарі