Вы можете использовать dependency-cruiser, чтобы сохранить чистую архитектуру в процессе эволюции вашего кода.

В своей предыдущей статье я объяснил, как конвертировать (трансформировать) ваш код на основе React в чистую архитектуру.

Ваша следующая задача — сохранить чистую архитектуру в процессе эволюции вашего кода. Это может быть сложно, когда несколько участников работают в команде (особенно когда в команду входят новички). Но есть и хорошие новости. Точно так же, как использование lint для поддержки формата исходного кода, мы можем представить инструмент для соблюдения наиболее важного правила чистой архитектуры: правила зависимости, которое гласит, что зависимости исходного кода могут указывать только вверх (в одну сторону).

Например, в моей предыдущей статье показан следующий граф зависимостей для программы TicTacToe. Вот весь исходный код этого примера. Давайте посмотрим, как этот инструмент может защитить чистую архитектуру за счет эволюции кода.

Начало работы с Dependency-Cruiser

dependency-cruiser – это инструмент, который анализирует все ваши исходные файлы и проверяет зависимости между файлами.



Вы можете легко установить инструмент и инициализировать его файл конфигурации (dependency-cruiser.js) для своего репозитория следующим образом:

$ yarn add dependency-cruiser --dev
$ npx depcruise --init

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

$ npx depcruise src --include-only '^src' --config --output-type err-long

✔ no dependency violations found (25 modules, 38 dependencies cruised)

Но есть еще кое-что. В сочетании с Graphviz dependency-cruiser создает классный график зависимостей.



$ npx depcruise src --include-only '^src' --config --output-type dot | dot -T svg > dependency-graph.svg && open dependency-graph.svg

Прохладный! Вы можете видеть, что все зависимости соответствуют вашим ожиданиям.

Поскольку мы будем часто использовать эти две команды, рекомендуется создать сценарии для них в файле package.json.

"scripts": {
    "depcruise:validate": "depcruise src --include-only '^src' --config --output-type err-long",
    "depcruise:tree": "depcruise src --include-only '^src' --config --output-type dot | dot -T svg > dependency-graph.svg && open dependency-graph.svg"
}

Затем давайте посмотрим, как dependency-cruiser обнаруживает недопустимые зависимости. Мы добавляем следующую недопустимую строку в src/Domain/UseCase/ClickOnBoardUseCase.ts.

// src/Domain/UseCase/ClickOnBoardUseCase.ts
import { TicTacToeView } from "../../Presentation";

Поскольку уровень представления ниже уровня домена, этот оператор import должен быть обнаружен как ошибка. Вуаля! Dependency-cruiser дает нам подробный анализ в виде текста и кристально чистый график.

$ yarn depcruise:validate
yarn run v1.22.17
$ depcruise src --include-only '^src' --config --output-type err-long

  warn no-circular: src/Presentation/hook/useTicTacToeModelController.ts → 
      src/Domain/UseCase/index.ts →
      src/Domain/UseCase/ClickOnBoardUseCase.ts →
      src/Presentation/index.ts →
      src/Presentation/TicTacToeView.tsx →
      src/Presentation/hook/useTicTacToeModelController.ts
    This dependency is part of a circular relationship. You might want to
    revise your solution (i.e. use dependency inversion, make sure the modules
    have a single responsibility)

  warn no-circular: src/Domain/UseCase/ClickOnBoardUseCase.ts → 
      src/Presentation/index.ts →
      src/Presentation/TicTacToeView.tsx →
      src/Domain/UseCase/index.ts →
      src/Domain/UseCase/ClickOnBoardUseCase.ts
    This dependency is part of a circular relationship. You might want to
    revise your solution (i.e. use dependency inversion, make sure the modules
    have a single responsibility)


✖ 2 dependency violations (0 errors, 2 warnings). 25 modules, 39 dependencies cruised.

✨  Done in 0.81s.

$ yarn depcruise:tree    
yarn run v1.22.17
$ depcruise src --include-only '^src' --config --output-type dot | dot -T svg > dependency-graph.svg && open dependency-graph.svg
✨  Done in 1.08s.

Dependency-cruiser определяет эту строку как нарушение правила «no-circular», предопределенного в файле конфигурации (dependency-cruiser.js), следующим образом:

  forbidden: [
    {
      name: "no-circular",
      severity: "warn",
      comment:
        "This dependency is part of a circular relationship. You might want to revise " +
        "your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ",
      from: {},
      to: {
        circular: true,
      },
    },
    ...
  ]

Таким образом, в конфигурационном файле по умолчанию уже есть общие правила, и вы можете обнаруживать такие нарушения, как отсутствие цикла.

Настройте правила для чистой архитектуры

Правило по умолчанию «no-circular» — хорошее начало, но нам нужна более строгая проверка зависимостей, чем общие правила. Например, можем ли мы обнаружить следующий недопустимый случай?

// src/Presentation/TicTacToeView.tsx
import { RepositoryImpl } from "../Data";

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

Нам нужно объявить, что Presentation может зависеть только от домена следующим образом:

  allowedSeverity: "error",
  allowed: [
    {
      from: { path: "(^src/Presentation)" },
      to: { path: ["^src/Domain"] },
    },
  ]

В этой новой конфигурации вы видите, что ссылка зависимости от Presentation/TicTacToeView.tsx до Data/index.ts помечена как «не разрешенная». Большой! Теперь dependency-cruiser определяет эту зависимость как нарушение нашего нового правила.

Но вы можете заметить, что другие действительные зависимости также помечены как «не разрешенные». Если вы используете «разрешенные» правила, dependency-cruiser будет выдавать сообщение «не разрешено» для каждой зависимости, которая не удовлетворяет хотя бы одному из них. Это означает, что теперь вам нужно явно указать все действительные зависимости. Хотя поначалу это кажется громоздким, это хорошая привычка — создавать зависимости и добавлять новые правила всякий раз, когда вы добавляете новые папки в репозиторий.

Вот окончательный файл конфигурации:

  allowedSeverity: "error",
  allowed: [
    {
      from: { path: "(^src/Main)" },
      to: {
        // “$1” is introduced from regular expression’s “group matching”. 
        // You can reference the part matched between brackets in “from” 
        // string by using “$1” in “to”.
        path: ["^$1", "^src/Presentation", "^src/Data", "^src/Domain"],
      },
    },
    {
      // Presentation other than Presentation/hook
      from: { path: "(^src/Presentation)", pathNot: "^src/Presentation/hook" },
      to: { path: ["^$1", "^src/Domain"] },
    },
    {
      // We want no hooks to depend on Presentation to make hooks independent
      // from any graphical presentation
      from: { path: "(^src/Presentation/hook)" },
      to: { path: ["^$1", "^src/Domain"] },
    },
    {
      from: { path: "(^src/Data)" },
      to: { path: ["^$1", "^src/Domain"] },
    },
    {
      from: { path: "(^src/Domain/UseCase)" },
      to: { path: ["^$1", "^src/Domain/Repository", "^src/Domain/Model"] },
    },
    {
      from: { path: "(^src/Domain/Repository)" },
      to: { path: ["^$1", "^src/Domain/Model"] },
    },
    {
      from: { path: "(^src/Domain/Model)" },
      to: { path: ["^$1"] },
    },
    {
      // Files outside of established folders (e.g. src/index.tsx) can reference
      // any files.
      from: {
        pathNot: ["^src/Main", "^src/Presentation", "^src/Data", "^src/Domain"],
      },
      to: {},
    },
  ],

Это исчерпывающее определение действительных зависимостей дает вам следующую ошибку и график:

$ yarn depcruise:validate
yarn run v1.22.17
$ depcruise src --include-only '^src' --config --output-type err-long

  error not-in-allowed: src/Presentation/TicTacToeView.tsx → src/Data/index.ts

✖ 1 dependency violations (1 errors, 0 warnings). 25 modules, 39 dependencies cruised.

error Command failed with exit code 1.

Визуализируйте зависимости в большом репо

Мы визуализировали граф зависимостей, показывающий все файлы. Это хорошо, когда ваше репо маленькое. Но вскоре ваш репозиторий будет включать в себя десятки и сотни файлов, как только члены вашей команды начнут работать над одним и тем же репозиторием. И вы хотели бы визуализировать все репо на более высоком уровне, чтобы проверить ссылки на зависимости.

Dependency-cruiser предоставляет вариант графика (ddot), который суммирует модули на уровне папки, и вы можете настроить его с помощью тем и фильтров. См. следующий высокоуровневый график для того же репо.

$ npx depcruise src --include-only '^src' --config --output-type ddot | dot -T svg > dependency-graph.svg && open dependency-graph.svg

Интеграция проверки зависимостей в Git

Когда над вашим репозиторием работают несколько участников, сложно сохранить зависимости, разработанные с помощью чистой архитектуры. Рекомендуется запускать dependency-cruiser всякий раз, когда какой-либо участник пытается зафиксировать изменения.

Давайте воспользуемся husky, чтобы автоматизировать проверку зависимостей перед любой фиксацией и запретить фиксацию, если обнаружено нарушение зависимости.



$ npx husky-init && yarn
$ npx husky add .husky/pre-commit 'yarn depcruise:validate'

После того, как этот прекоммит-хук настроен, никто не может делать коммиты, нарушающие зависимости. Например, ниже показано, как вы получаете уведомление о нарушении при попытке сделать недопустимую фиксацию.

$ git commit -m 'chore: intentinal violation of dependencies'
yarn run v1.22.17
$ depcruise src --include-only '^src' --config --output-type err-long

  error not-in-allowed: src/Presentation/TicTacToeView.tsx → src/Data/index.ts

✖ 1 dependency violations (1 errors, 0 warnings). 25 modules, 39 dependencies cruised.

error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
husky - pre-commit hook exited with code 1 (error)

Вы можете увидеть финальное репо здесь.

Заключение

Я показал, как проверить ваше репо с точки зрения чистой архитектуры. Dependency-cruiser — это мощный инструмент, который можно интегрировать в обработчик предварительной фиксации вашего репозитория git. Таким образом, вы можете поддерживать свое репо в соответствии с правилами зависимостей чистой архитектуры. Я надеюсь, что этот инструмент поможет вам поддерживать ваши проекты.