ArchitectureProcessor とそのカスタマイズ
このページでは、katachi の重要な構成要素の 1 つである ArchitectureProcessor を解説し、katachi の理解を深めます。
後半では、独自の ArchitectureProcessor を書いて実行する方法を解説します。
ArchitectureProcessor とは
Section titled “ArchitectureProcessor とは”katachi の ArchitectureProcessor は、アーキテクチャ定義をもとに何かを行う interface です。
@ExperimentalKatachiApipublic 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 の 2 要素
Section titled “katachi の 2 要素”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 です。
ビルトインの ArchitectureProcessor
Section titled “ビルトインの ArchitectureProcessor”katachi は 5 つの ArchitectureProcessor を提供しています。
| Processor | 何を行うか? | ドキュメント |
|---|---|---|
projectArchitecture.assert()( LayoutCheck) | ファイル配置がアーキテクチャ定義に沿っているかを検証し、エラーがあればアサーション例外を発生させます。 | |
FileConstraintCheck | konsist { } に書かれた制約を評価します。projectArchitecture.assert(FileConstraintCheck()) のように渡します。 | Konsist との統合 |
GenerateDocumentation | プロジェクト定義に書かれた summary, description などのプロパティをもとにドキュメントを生成します。 | ドキュメント生成 |
GenerateCodeFromTemplate | layout のファイルの宣言に付けた .template { } の内容をもとにファイルを生成します。 | テンプレートからのコード生成 |
DescribeTemplates | .template { } を持つ宣言の一覧や、1つのテンプレートのパラメータと生成されるファイルを表示します。ファイルは書きません。 | テンプレートからのコード生成 |
いずれもプロジェクトのアーキテクチャ定義を受け取り、検証や生成に使っています。
ArchitectureProcessor の実行
Section titled “ArchitectureProcessor の実行”ArchitectureProcessor の実行方法にはいくつかのパターンがあります。
推奨される実行方法は ArchitectureProcessor ごとに異なります。個々の実行方法は、それぞれのドキュメントを参照してください。
ArchitectureProcessor に API(多くの場合トップレベル関数)が用意されていれば、その API を呼び出して ArchitectureProcessor を実行できます。
例えば LayoutCheck なら、projectArchitecture.assert() を呼び出すと実行できます。
projectArchitecture.assert() // 内部で LayoutCheck().process(...) を呼び出すコマンドラインから実行するには、ArchitectureProcessor を Gradle plugin に登録しておく必要があります。
katachi { processors { // ...
// ⭐️ ArchitectureProcessor 名 と 対応するクラス名を指定する register("someProcessor", "com.example.SomeProcessor") }}また、登録と同時にデフォルト値を設定することもできます。
katachi { processors { // 登録と同時にデフォルト値を書く register("someProcessor", "com.example.SomeProcessor") { arg("someArg1", "someValue1") }
// 既に登録されている processor にデフォルト値だけ足す args("docs") { arg("mode", "check") } }}登録した processor ごとに Gradle タスクが用意されます。
タスク名は katachi + 登録キーの先頭を大文字にしたものです(例: register("someProcessor", ...) → katachiSomeProcessor)。
実行例:
$ ./gradlew katachiSomeProcessor[1/3] Processors: someProcessor
[2/3] Processing...
========================================[3/3] Katachi processor run: 1 succeeded, 0 failed
[OK] someProcessor
========================================カスタムの ArchitectureProcessor
Section titled “カスタムの ArchitectureProcessor”ArchitectureProcessor は単純な interface なので、自分で実装して自由に処理を追加できます。
GitHub リポジトリの sample/custom-processor モジュールに、カスタム ArchitectureProcessor の例があります。
1. ArchitectureProcessor を実装する
Section titled “1. 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 } }}katachi は引数のパースに kotlinx.serialization を使うため、プロジェクトに kotlinx.serialization の依存を追加してください。
plugins { kotlin("jvm") version "<kotlin-version>" kotlin("plugin.serialization") version "<kotlin-version>"}引数が必要な ArchitectureProcessor の場合は ArchitectureProcessor interface を実装してください。
第1型引数に引数の型、第2型引数に戻り値の型を指定します。
object CountFilesPerRole : ArchitectureProcessor<CountFilesPerRole.Args, Map<String, Int>> { // --arg で受け取る値の型と、その読み方を渡します override val argsSerializer: KSerializer<Args> = Args.serializer()
override fun process(context: ArchitectureProcessContext<Args>): Result<Map<String, Int>> = runCatching { // context.args を使って引数を参照する val args: CountFilesPerRole.Args = context.args val prefix = args.prefix
context.log("Counting files per role under $prefix...") context.roles .filter { role -> role.qualifiedName.startsWith(prefix) } .associate { role -> role.name to context.filesOf(role).size } }
@Serializable data class Args(val prefix: String = "")}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()katachi.processors.register に登録すると、登録したキーから決まる名前の Gradle タスクとして呼べるようになります。
(例: countFilesPerRole → katachiCountFilesPerRole)
katachi { processors { register("countFilesPerRole", "com.example.processors.CountFilesPerRole") }}./gradlew katachiCountFilesPerRole--arg の値は Args のフィールドの型に合わせて読まれます。カンマで分割される型などの細かい決まりは decodeFromStringMap の API リファレンス を参照してください。
用意した実行方法は processor の KDoc などにドキュメントとして書き、実行方法まで利用者に伝えてください。
assert()もドキュメント・テンプレート生成も ArchitectureProcessor です。- 標準の ArchitectureProcessor が使う仕組みと同じ仕組みで、カスタムの ArchitectureProcessor を作れます。
次のステップ
Section titled “次のステップ”- ArchitectureProcessor の API リファレンス …
ArchitectureProcessorの型と、processで使える API の一覧です。 - ドキュメント生成 … 同じ仕組みに乗っている、いちばん大きなビルトイン processor です。
- テンプレートからコード生成 … 定義からコードを出す processor です。