Этот текст является частью серии текстов об анализе чистой архитектуры на примере различных фреймворков и языков.
Цели этого текста соответствуют целям предыдущих текстов, а именно: I. Показать архитектурное разделение приложения React с использованием Clean Architecture; II. Руководство по реализации новых функций в этой предложенной архитектуре.
- Архитектурное разделение
- Руководство по реализации
- Первый шаг: Создайте бизнес-правила на уровне домена
- Второй шаг: Реализация правил на уровне данных
- Третий шаг: Реализация страниц в презентационном слое
- Четвертый шаг: Подключите все уровни для работы запросов
- Пятый шаг: Применить страницу, созданную в приложении
- Заключение
- Ссылки
Архитектурное разделение
Начальным шагом является анализ того, как выполняется разделение.
cypress/
src/
data/
protocols/
test/
usecases/
domain/
errors/
models/
test/
usecases/
infra/
cache/
http/
test/
main/
adapters/
config/
decorators/
factories/
cache/
decorators/
http/
pages/
usecases/
routes/
scripts/
index.tsx
presentation/
assets/
components/
hooks/
pages/
protocols/
routes/
styles/
test/
requirements/
validation/
errors/
protocols/
test/
validators/
В деталях назначение каждой файловой структуры выглядит следующим образом:
- cypress: Содержит файлы сквозного тестирования приложения (для больших проектов эту папку рекомендуется располагать в отдельном проекте, чтобы команда, ответственная за e2e-тесты, могла позаботиться об этом, так как им не нужно знать код проекта).
- src: Содержит все файлы, необходимые для работы приложения.
- Data: Папка data представляет собой слой данных чистой архитектуры, зависящий от слоя домена. Содержит реализации бизнес-правил, которые объявлены в домене.
- Домен: Представляет слой домена Clean Architecture, самый внутренний слой приложения, не имеющий зависимости от других слоев, где содержатся бизнес-правила.
- Infra: Эта папка содержит реализации, относящиеся к протоколу HTTP и кэшу, это также единственное место, где у вас будет доступ к внешним зависимостям, связанным с этими двумя упомянутыми элементами. В этой папке также содержится большинство внешних библиотек.
- Main: Она соответствует основному слою приложения, где интерфейсы, разработанные в слое представления, интегрируются с бизнес-правилами, созданными в папках, представляющих внутренние слои архитектуры Clean Architecture. Все это происходит благодаря использованию таких паттернов проектирования, как Factory Method, Composite и Builder.
- Презентация: Эта папка содержит визуальную часть приложения с его страницами, компонентами, крючками, активами и стилизацией.
- Требования: Содержит документированные системные требования.
- Валидация: Здесь содержатся реализации валидаций, используемых в полях.
В отличие от подхода Flutter, где существовала центральная папка, в которой были сосредоточены все тесты, в данном подходе тесты расположены в соответствующих папках внутри src
.
Руководство по реализации
В этом разделе будет описана рекомендуемая логическая последовательность для лучшей производительности реализации React-систем, использующих данную архитектуру.
Для упрощения объяснения, юнит-тесты не будут описаны подробно. Однако настоятельно рекомендуется начинать с юнит-тестов до разработки (TDD) каждого шага, используя требования для поддержки сценариев. А после завершения работы над сценариями протестировать сквозной поток (если он является одним из основных, помните о пирамиде тестов).
Ниже показано создание потока Login для входа в приложение.
Первый шаг: Создайте бизнес-правила на уровне домена
Внутри src/domain/usecases создайте authentication.ts
. Этот файл будет интерфейсом, описывающим бизнес-правило аутентификации.
import { AccountModel } from '@/domain/models/';
export interface IAuthentication {
auth(params: Authentication.Params): Promise<Authentication.Model>;
}
export namespace Authentication {
export type Params = {
email: string;
password: string;
};
export type Model = AccountModel;
}
Как мы видим, это интерфейс, который имеет функцию auth()
, которая получает Authentication.Params, объявленные в пространстве имен ниже — содержащие тип параметров (email и пароль) и тип модели (AccountModel) — и ожидает асинхронно вернуть Authentication.Model.
AccountModel — это именованный экспорт модели, созданной в src/domain/models, которая представляет собой токен, возвращаемый после аутентификации для сохранения сессии.
export type AccountModel = {
accessToken: string;
};
Второй шаг: Реализация правил на уровне данных
На этом уровне мы создаем сценарий использования для реализации правила, созданного ранее на доменном уровне, но внутри src/data/usecases.
Обычно файл выглядит так, как показано в примере ниже.
import { IHttpClient, HttpStatusCode } from '@/data/protocols/http';
import { UnexpectedError, InvalidCredentialsError } from '@/domain/errors';
import { IAuthentication, Authentication } from '@/domain/usecases';
export class RemoteAuthentication implements IAuthentication {
constructor(
private readonly url: string,
private readonly httpClient: IHttpClient<RemoteAuthenticationamespace.Model>
) {}
async auth(
params: Authentication.Params
): Promise<RemoteAuthenticationamespace.Model> {
const httpResponse = await this.httpClient.request({
url: this.url,
method: 'post',
body: params,
});
switch (httpResponse.statusCode) {
case HttpStatusCode.ok:
return httpResponse.body;
case HttpStatusCode.unauthorized:
throw new InvalidCredentialsError();
default:
throw new UnexpectedError();
}
}
}
export namespace RemoteAuthenticationamespace {
export type Model = Authentication.Model;
}
Как мы видим, класс RemoteAuthentication реализует интерфейс IAuthentication, получая HTTP-клиента и url для запроса. В функции auth()
он получает параметры и вызывает httpClient, передавая url, метод (в данном случае это post) и тело (это параметры). Возвратом является httpResponse типа, относящегося к Authentication.Model, который имеет код состояния ответа, и который, в зависимости от результата, выдает соответствующий возврат — и может вернуть значение, ожидаемое запросом, или ошибку.
Кодами состояния являются коды HTTP:
export enum HttpStatusCode {
ok = 200,
created = 201,
noContent = 204,
badRequest = 400,
unauthorized = 401,
forbidden = 403,
notFound = 404,
serverError = 500,
}
Третий шаг: Реализация страниц в презентационном слое
Для упрощения понимания будут представлены только фрагменты кода, относящиеся к вызову функции аутентификации. Экран входа в систему содержит больше действий и деталей, выходящих за рамки аутентификации. Для упрощения визуализации рассмотрим прототип страницы ниже.
В src/presentation/pages/ будет создана страница Login, состоящая из компонентов, методов и функций. Компонентом, вызывающим функцию аутентификации, является <Button/>
, который содержится в форме для получения значений ввода, как показано в следующем фрагменте кода:
<form
data-testid="loginForm"
className={Styles.form}
onSubmit={handleSubmit}
>
<Input
autoComplete="off"
title="Enter your e-mail"
type="email"
name="email"
/>
<Input
autoComplete="off"
title="Enter your password"
type="password"
name="password"
minLength={6}
/>
<Button
className={Styles.loginBtn}
type="submit"
disabled={state.isFormInvalid}
title="Login"
data-testid="loginButton"
/>
</form>
При нажатии на Button
вызывается handleSubmit()
, которая находится в onSubmit
формы form
.
const handleSubmit = async (
event: React.FormEvent<HTMLFormElement>
): Promise<void> => {
event.preventDefault();
try {
const account = await authentication.auth({
email: state.email,
password: state.password,
});
setCurrentAccount(account);
history.replace('/');
} catch (error) {
// Error handling here
}
};
Где authentication.auth()
по щелчку вызывает фабрику (мы увидим позже) для выполнения аутентификации. В данном случае ей передаются параметры, захваченные вводом, а значение, возвращенное из запроса, сохраняется в кэше через setCurrentAccount(account)
.
Четвертый шаг: Подключите все уровни для работы запросов
После того как все реализовано, теперь просто соедините все части. Для этого используется паттерн проектирования Factory Method.
Внутри src/main/factories/usecases мы создаем фабрику реализуемого варианта использования. В данном примере это связано с аутентификацией.
Создается makeRemoteAuthentication, которая возвращает RemoteAuthentication, получающую в качестве параметра фабрику Http-клиента и фабрику, создающую URL. URL API, который вы хотите запросить, передается в качестве параметра вместе с фабрикой, создающей URL. В примере это URL, который заканчивается на /login.
import { RemoteAuthentication } from '@/data/usecases/';
import { IAuthentication } from '@/domain/usecases';
import { makeAxiosHttpClient, makeApiUrl } from '@/main/factories/http';
export const makeRemoteAuthentication = (): IAuthentication => {
const remoteAuthentication = new RemoteAuthentication(
makeApiUrl('/login'),
makeAxiosHttpClient()
);
return remoteAuthentication;
};
После этого в src/main/factories/pages создается папка для фабрик Login. На страницах с формами также внедряются валидации форм, но поскольку в данном тексте речь идет об интеграциях, мы опустим этот момент.
import React from 'react';
import { Login } from '@/presentation/pages';
import { makeRemoteAuthentication } from '@/main/factories/usecases/';
const makeLogin: React.FC = () => {
const remoteAuthentication = makeRemoteAuthentication();
return (
<Login
authentication={remoteAuthentication}
/>
);
};
export default makeLogin;
Создается makeLogin
const, представляющая фабрику. У нее есть makeRemoteAuthentication
, который внедряется внутрь страницы Login, созданной в презентационном слое, чтобы страница имела доступ к этим запросам.
Пятый шаг: Применить страницу, созданную в приложении
Наконец, необходимо вызвать фабрику Login в приложении, чтобы к ней мог получить доступ пользователь.
В файл router.tsx, расположенный в src/main/routes, добавьте страницу фабрики, созданную в Switch внутри BrowserRouter. Маршрут передается в пути, в данном случае это /login, а страница в компоненте, который в данном случае является указателем на фабрику makeLoginPage
. Эта логика используется со всеми остальными страницами, меняя Route на PrivateRoute только в том случае, если маршрут аутентифицирован. Код выглядит следующим образом.
const Router: React.FC = () => {
return (
<ApiContext.Provider
value={{
setCurrentAccount: setCurrentAccountAdapter,
getCurrentAccount: getCurrentAccountAdapter,
}}
>
<BrowserRouter>
<Switch>
<Route exact path="/login" component={makeLogin} />
<PrivateRoute exact path="/" component={makeDashboard} />
</Switch>
</BrowserRouter>
</ApiContext.Provider>
);
};
Заключение
Несмотря на то, что чистая архитектура немного сложна для понимания и реализации в начале — и даже кажется излишней — абстракции необходимы. Несколько паттернов проектирования применяются для обеспечения качества и независимости кода, облегчая эволюцию и независимое сопровождение фреймворка. В подобных случаях, если вы хотите сменить фреймворк с React на Angular или любой другой фреймворк на основе Typescript, просто измените презентационный слой и внесите коррективы в зависимости.
Следование процессу разработки и понимание того, почему вы делаете это именно так, облегчает создание кода. Через некоторое время это становится естественным, поскольку процесс разработки является линейным: I. Вариант использования на уровне домена; II. Создание сценария использования в слое данных; III. Создание пользовательского интерфейса в презентационном слое; IV. Создание фабрик для интеграции всех слоев в основной слой; V. И вызов основной фабрики в маршрутах приложения.
Поскольку пример содержит много абстрактных частей, рекомендуется прочитать код скрытых частей для лучшего понимания. В этом репозитории вы можете получить доступ к абстрактному коду, аналогичному приведенному в этом примере.
Вы также можете получить доступ к этой архитектуре, просто выполнив команду npx @rubemfsv/clean-react-app my app
, аналогичную create-react-app, но в более чистом и масштабируемом виде. Узнайте, как это сделать, прочитав этот пост.
Ссылки
- Rodrigo Manguinho https://github.com/rmanguinho/clean-react
- МАРТИН, Роберт К. Чистая архитектура: Руководство ремесленника по структуре и проектированию программного обеспечения. 1-е изд. США: Prentice Hall Press, 2017. ISBN 0134494164.