コンテンツにスキップ
  • 上級者向け

ArchitectureProcessor とそのカスタマイズ

このページでは、katachi の重要な構成要素の 1 つである ArchitectureProcessor を解説し、katachi の理解を深めます。

後半では、独自の ArchitectureProcessor を書いて実行する方法を解説します。

katachi の ArchitectureProcessor は、アーキテクチャ定義をもとに何かを行う interface です。

@ExperimentalKatachiApi
public interface ArchitectureProcessor<Args, R> {
// 引数の parser
public val argsSerializer: KSerializer<Args>
// 何かを行う
public fun process(context: ArchitectureProcessContext<Args>): Result<R>
}

process の本体は runCatching { } (もしくは runCatchingScoped ユーティリティ) で囲ってください。runCatchingScoped は、見つかった不足を failure(...) で全部貯めて、最後にまとめて失敗にできます(sample/custom-processor の RoleDocCoverage.kt)。

戻り値 意味 Gradle タスクでの扱い
Result.success 成功 [OK]
Result.failure 失敗
(違反が見つかった、ドキュメントが古い、処理ができなかった など)
[FAILED]。例外のメッセージを表示し、exit が非ゼロになる

katachi は大きく分けて、アーキテクチャ定義 と ArchitectureProcessor の 2 つの要素でできています。

flowchart LR
    D["architecture { }<br/>アーキテクチャ定義"]
    D --> Layout["LayoutCheck<br/>配置を検証する"]
    D --> Konsist["FileConstraintCheck<br/>konsist { } の制約を検証する"]
    D --> Doc["GenerateDocumentation<br/>ドキュメントを生成"]
    D --> Template["GenerateCodeFromTemplate<br/>コードを生成"]
    D -.-> Custom["あなたの ArchitectureProcessor<br/>集計・抽出・別形式への出力"]

    style D fill:#5B8DEF22,stroke:#5B8DEF,stroke-width:2px
    style Custom fill:#BDBDBD22,stroke:#BDBDBD,stroke-dasharray:4 3,color:#8A8A8A

アーキテクチャ定義そのものは何も実行しません。ArchitectureProcessContext を受け取って何かをするのが ArchitectureProcessor です。

  • アーキテクチャ定義は、プロジェクトのアーキテクチャを architecture { } ブロックの中に書いたものです。
  • アーキテクチャを定義しただけでは何も起こりません。その定義を使って何かを行うのが ArchitectureProcessor です。

katachi は 5 つの ArchitectureProcessor を提供しています。

Processor何を行うか?ドキュメント
projectArchitecture.assert()
(LayoutCheck)
ファイル配置がアーキテクチャ定義に沿っているかを検証し、エラーがあればアサーション例外を発生させます。
FileConstraintCheckkonsist { } に書かれた制約を評価します。projectArchitecture.assert(FileConstraintCheck()) のように渡します。Konsist との統合
GenerateDocumentationプロジェクト定義に書かれた summary, description などのプロパティをもとにドキュメントを生成します。ドキュメント生成
GenerateCodeFromTemplatelayout のファイルの宣言に付けた .template { } の内容をもとにファイルを生成します。テンプレートからのコード生成
DescribeTemplates.template { } を持つ宣言の一覧や、1つのテンプレートのパラメータと生成されるファイルを表示します。ファイルは書きません。テンプレートからのコード生成

いずれもプロジェクトのアーキテクチャ定義を受け取り、検証や生成に使っています。

ArchitectureProcessor の実行方法にはいくつかのパターンがあります。

推奨される実行方法は ArchitectureProcessor ごとに異なります。個々の実行方法は、それぞれのドキュメントを参照してください。

ArchitectureProcessor に API(多くの場合トップレベル関数)が用意されていれば、その API を呼び出して ArchitectureProcessor を実行できます。

例えば LayoutCheck なら、projectArchitecture.assert() を呼び出すと実行できます。

projectArchitecture.assert() // 内部で LayoutCheck().process(...) を呼び出す

ArchitectureProcessor は単純な interface なので、自分で実装して自由に処理を追加できます。

GitHub リポジトリの sample/custom-processor モジュールに、カスタム ArchitectureProcessor の例があります。

ArchitectureProcessorNoArg か ArchitectureProcessor を実装したクラスを用意し、process メソッドを実装してください。

process メソッドに渡される context には、アーキテクチャ定義や引数などが入っています。これらを必要に応じて参照しながら、ArchitectureProcessor のロジックを書きます。

引数が必要ない ArchitectureProcessor の場合は ArchitectureProcessorNoArg interface を実装してください。

型引数には実行時の戻り値の型を指定します。

// ArchitectureProcessorNoArg を実装した object を定義
object CountFilesPerRole : ArchitectureProcessorNoArg<Map<String, Int>> {
override fun process(context: ArchitectureProcessNoArgContext): Result<Map<String, Int>> = runCatching {
// context.architecture ... アーキテクチャ定義
// context.roles / groups ... 定義された Role・group
// context.declaredEntries ... layout を flat にした宣言の一覧
// context.filesOf(role) ... その Role にマッチした実ファイル
// context.rawArgs ... 渡された引数(名前と値)をそのまま
// context.log() ... 進捗を表示します
context.log("Counting files per role...")
context.roles.associate { role -> role.name to context.filesOf(role).size }
}
}

context から取得できる情報の詳細は ArchitectureProcessContext の API リファレンス を参照してください。

Role の metadata

場合によっては、ArchitectureProcessor の側で、アーキテクチャ定義に独自のプロパティを指定してほしくなるかもしれません。 その場合は Role の metadata を使えます。

例えば、すでに非推奨なのにそのまま残っている Role を Obsolete としてマークしたいなら、以下のように Obsolete という metadata を定義して設定できます。

// metadata のキー
val Obsolete: MetadataKey<Boolean> = metadata()
var MetadataScope.obsolete: Boolean? by Obsolete
// Processor 側
object ObsoleteRoles : ArchitectureProcessorNoArg<List<String>> {
override fun process(context: ArchitectureProcessNoArgContext): Result<List<String>> = runCatching {
context.roles.filter { it[Obsolete] == true }.map { it.name }
}
}
// 利用側
architecture {
"UseCase" {
obsolete = true
}
}

2. ArchitectureProcessor を実行する方法を用意する

Section titled “2. ArchitectureProcessor を実行する方法を用意する”

利用者が ArchitectureProcessor を実行する方法を用意してください。

プログラムから実行したいなら API を用意する方法を、ワンショットで実行したいならコマンドラインから実行する方法をおすすめします。

Architecture の拡張関数を用意します。katachi が assert() を用意しているのと同じ形です。

fun Architecture.countFilesPerRole(): Map<String, Int> = process(CountFilesPerRole, CountFilesPerRole.Args()).getOrThrow()

引数なしの定義の場合は、process(CountFilesPerRole) と書きます。

利用者は processor を知らなくても、用意された API を呼び出せます。

val counts = projectArchitecture.countFilesPerRole()

用意した実行方法は processor の KDoc などにドキュメントとして書き、実行方法まで利用者に伝えてください。

  • assert() もドキュメント・テンプレート生成も ArchitectureProcessor です。
  • 標準の ArchitectureProcessor が使う仕組みと同じ仕組みで、カスタムの ArchitectureProcessor を作れます。