Чистая архитектура: Применение на React

Этот текст является частью серии текстов об анализе чистой архитектуры на примере различных фреймворков и языков.

Цели этого текста соответствуют целям предыдущих текстов, а именно: 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.

Оцените статью
devanswers.ru
Добавить комментарий