Deptrac の設定を見通し良くするために ─ 関心事によるレイヤーの分離

こんにちは。エンジニアの濱田 (@hamakou108) です。

PHP 開発者の皆さん、 Deptrac を活用していますか?

私たちのプロダクトでは、アーキテクチャの整合性を保つために Deptrac を利用して依存関係のチェックを行っています。プロダクトの成長に伴ってアーキテクチャが複雑になるにつれ、 Deptrac の設定ファイルも肥大化しやすくなり、メンテナンス性の低下を招くことがあります。そこで今回、設定ファイルの構成を見直すことで、見通しのよい構成へと改善し、メンテナンス性の低下を抑えることができました。本記事では、その取り組みについて紹介します。

Deptrac とは?

Deptrac は、 PHP コードベースにおけるレイヤー間の依存関係を定義し、ルールに基づいて検証する静的解析ツールです。

よくある4層レイヤードアーキテクチャを例に挙げて説明します。4層レイヤードアーキテクチャは、プレゼンテーション層、アプリケーション層、ドメイン層、インフラ層の4つのレイヤーに分かれており、ドメイン層に向かって外側から内側のレイヤーへと依存するような構造を持ちます。

4層レイヤードアーキテクチャの依存関係

このアーキテクチャの依存関係を Deptrac で表現する場合、たとえば次のようになります。

deptrac:
  layers:
    - name: Presentation
      collectors:
        - type: directory
          value: './src/Presentation/.*'
    - name: Application
      collectors:
        - type: directory
          value: './src/Application/.*'
    - name: Domain
      collectors:
        - type: directory
          value: './src/Domain/.*'
    - name: Infrastructure
      collectors:
        - type: directory
          value: './src/Infrastructure/.*'

  ruleset:
    Presentation:
      - Application
    Application:
      - Domain
    Infrastructure:
      - Domain

layers はコードベースを論理的なレイヤーに分割するための設定で、それぞれのレイヤーがどのディレクトリに対応するかは collectors で指定されます。たとえば、 Presentation レイヤーは src/Presentation/ 以下のファイルを対象とします。

ruleset はレイヤー間の依存関係ルールを定義しており、どのレイヤーがどのレイヤーに依存してよいかを示します。この例では、 Presentation から ApplicationApplication から DomainInfrastructure から Domain への依存が許可されており、これ以外のレイヤー間の依存は禁止されます。このようにしてアーキテクチャの意図と異なるような依存関係が発生してしないか検証することができます。

Deptrac の設定の肥大化

私たちのプロダクトでは、これまで4層のレイヤードアーキテクチャを採用してきました。しかし、プロダクトの成長に伴い、このアーキテクチャだけではコードベースを効果的に管理することが難しくなってきました。そこで、同じコードベース内でサービスごとにモジュールを分割する、いわゆるモジュラーモノリスアーキテクチャも併せて採用することにしました。

モジュラーモノリス + レイヤードアーキテクチャ

せっかく Deptrac を使っているので、レイヤードアーキテクチャの依存関係に加えて、サービス境界を跨いだ依存関係が発生していないことも検証してみようと思います。 Deptrac では1つのクラスが複数のレイヤーに含まれると警告として検知されてしまいます。このため、そのようなクラスが発生しないように注意しつつ、先ほどの Deptrac の設定ファイルを変更してみます。

# Foo と Bar という2つのサービスが
# トップレベルで分割されている状況を想定

deptrac:
  layers:
    - name: FooPresentation
      collectors:
        - type: directory
          value: './src/Foo/Presentation/.*'
    - name: FooApplication
      collectors:
        - type: directory
          value: './src/Foo/Application/.*'
    - name: FooDomain
      collectors:
        - type: directory
          value: './src/Foo/Domain/.*'
    - name: FooInfrastructure
      collectors:
        - type: directory
          value: './src/Foo/Infrastructure/.*'
    - name: BarPresentation
      collectors:
        - type: directory
          value: './src/Bar/Presentation/.*'
    - name: BarApplication
      collectors:
        - type: directory
          value: './src/Bar/Application/.*'
    - name: BarDomain
      collectors:
        - type: directory
          value: './src/Bar/Domain/.*'
    - name: BarInfrastructure
      collectors:
        - type: directory
          value: './src/Bar/Infrastructure/.*'

  ruleset:
    FooPresentation:
      - FooApplication
    FooApplication:
      - FooDomain
    FooInfrastructure:
      - FooDomain
    BarPresentation:
      - BarApplication
    BarApplication:
      - BarDomain
    BarInfrastructure:
      - BarDomain

少し見通しが悪くなってきました。同じような言葉が複数回出てくるようになり、記述が冗長に見えます。

より現実的な状況を考えると、レイヤーを定義するための collectors の条件が複雑になったり、より細かな単位で依存関係をチェックする必要があったりと、さまざまな要請から設定ファイルが肥大化する可能性があります。このまま拡張を続けると、 Deptrac の設定のメンテナンス性は徐々に低下していくでしょう。

関心事による Deptrac 設定の分割

この問題に対してどのように取り組めば良いでしょうか。実は Deptrac のドキュメント中にその指針が示されています。

Typically software has more than just one view. It is possible to use multiple config files, to take care about different architectural views.

一般的にソフトウェアにはさまざまなビューが存在します。それぞれのアーキテクチャビューに対応するために、複数の設定ファイルを使い分けることができます。

https://deptrac.github.io/deptrac/concepts/#different-layers-and-different-views

つまり、アーキテクチャの関心事に沿って設定ファイルを分解することで、見通しのよい設定にすることができます。

それぞれのアーキテクチャを独立したビューで捉える

この指針に従って、前述の例の設定ファイルを分割してみましょう。各ファイルには、それぞれのアーキテクチャの関心事に関連するレイヤーとルールのみを記述します。

deptrac.layered-architecture.yaml

deptrac:
  layers:
    - name: Presentation
      collectors:
        - type: directory
          value: './src/.*/Presentation/.*'
    - name: Application
      collectors:
        - type: directory
          value: './src/.*/Application/.*'
    - name: Domain
      collectors:
        - type: directory
          value: './src/.*/Domain/.*'
    - name: Infrastructure
      collectors:
        - type: directory
          value: './src/.*/Infrastructure/.*'

  ruleset:
    Presentation:
      - Application
    Application:
      - Domain
    Infrastructure:
      - Domain

deptrac.bounded-context.yaml

parameters:
  paths:
    - ./src
  layers:
    - name: Foo
      collectors:
        - type: directory
          value: './src/Foo/.*'
    - name: Bar
      collectors:
        - type: directory
          value: './src/Bar/.*'

  # レイヤー間の依存関係は許可しないため、 ruleset の定義は不要

deptrac.layered-architecture.yaml は記事の序盤で紹介した設定ファイルとほぼ同じですが、各レイヤーは Foo と Bar のそれぞれサービス用ディレクトリに存在するため、 collectors で指定するパスを正規表現に変更しています。 deptrac.bounded-context.yaml は単純に各サービスに対応するレイヤーを定義するだけで済むようになりました。

チェックを実行する際は、分割された設定ファイルそれぞれについて deptrac コマンドを実行する必要があります。

./vendor/bin/deptrac analyse --config-file=deptrac.layered-architecture.yaml
./vendor/bin/deptrac analyse --config-file=deptrac.bounded-context.yaml

設定ファイル数の増加に伴ってチェックの総実行時間も増加しますが、 Deptrac はキャッシュ機能を備えているため、私たちの環境ではそれほど増えることはありませんでした。

まとめ

プロダクトの成長に伴いアーキテクチャの構成が増えていく中で、 Deptrac の設定ファイルを関心事ごとに分割することで、メンテナンス性の低下を抑える取り組みについて紹介しました。 Deptrac を活用している皆さんも、設定ファイルの構成を見直すことで、より効果的にアーキテクチャを管理できる可能性があります。ぜひ一度、設定の見直しを検討してみてはいかがでしょうか。