Role
Role を定義する
Section titled “Role を定義する”アーキテクチャ定義に従ったアプリには、固有の役割(Role)を持つファイルがあります。
例えば UseCase, Repository, Controller, ViewModel などです。
どんな Role があるのかは、architecture { } の中で定義します。
architecture { // UseCase という Role を定義する "UseCase" { // 説明・置き場所・制約を、この中に書く }}Role の説明・置き場所・制約は、その Role のブロックの中に書きます。
title・summary・descriptionなど … そのRoleの説明です。ドキュメント生成に使われます。layout { }… そのRoleのファイルをどこに置けるかを定義します。- 詳しくは レイアウトの定義 を参照してください。
"制約名".konsist { }… そのRoleのファイルにかけられる制限を定義します。- 制約は Konsist で書きます。詳しくは Konsist との統合 を参照してください。
.template { }…layout { }で宣言したファイルに付けて、そのファイルを新しく作るときのテンプレートを定義します。- 詳しくは テンプレートからコード生成 を参照してください。
architecture { "UseCase" { title = "ユースケース" summary = "各画面で発生するアプリ固有の1つの振る舞い"
example("GetUserUseCase", "ユーザーを取得する")
// どこに置いてよいか layout { "src/main/kotlin/com/example/useCase" / "*UseCase".ktFile() }
// 中身がどうあるべきか "invoke 関数を1つだけ持つこと".konsist { classes().must { it.functions().map { f -> f.name } == listOf("invoke") } } }}それぞれを、もう少し詳しく説明します。
説明を書く
最低限必要なのは summary だけです。生成ドキュメントでRoleの一覧表に並ぶ1行になります。
"UseCase" { summary = "各画面で発生するアプリ固有の1つの振る舞い"}description には長い説明を、example() には代表例を書けます。example() は呼んだ順に保持されます。
"UseCase" { title = "ユースケース" summary = "各画面で発生するアプリ固有の1つの振る舞い" description = """ UI からは UseCase だけを呼び、Repository を直接触らない。
### やってはいけないこと - Android の型に依存する """.trimIndent()
example("GetUserUseCase", "ユーザーを取得する") example("UpdateUserUseCase", "ユーザーを更新する")}置き場所を宣言する
置き場所を1つだけ持つ場合です。
"UseCase" { layout { "src/main/kotlin/com/example/useCase" / "*UseCase".ktFile() }}layout { } は複数書けます。 同じ種類のファイルが複数の場所に現れてよい、という宣言です。種類の違うファイルは、Role を分けて宣言します。
"Screen" { // feature-home のモジュールに layout { "feature-home/src/main/kotlin/screen" / "*Screen".ktFile() }
// feature-settings のモジュールにも layout { "feature-settings/src/main/kotlin/screen" / "*Screen".ktFile() }}宣言していない場所にファイルを置くと違反になります。
src/main/kotlin/com/example/GetUserUseCase.kt ← useCase/ の外に置いてしまったテストを実行すると、katachi が近い置き場所と直し方まで1つのブロックで報告します。
Katachi check failed: 1 violation (Unexpected: 1)
[UnexpectedFile] file:///path/to/project/src/main/kotlin/com/example/GetUserUseCase.kt No role is defined for this file.
Nearby locations: UseCase file:///path/to/project/src/main/kotlin/com/example/useCase/
How to fix: - Move it to one of the locations above - Delete it if it is not needed - Add a new role for it: "GetUserUseCase" { summary = "TODO" layout { "src/main/kotlin/com/example" / "GetUserUseCase".ktFile() } }中身に制約をかける
制約には名前を付けます。この名前がそのまま違反レポートに出ます。
"UseCase" { layout { "src/main/kotlin/com/example/useCase" / "*UseCase".ktFile() }
"invoke 関数を1つだけ持つこと".konsist { classes().must { it.functions().map { f -> f.name } == listOf("invoke") } }}制約の対象は、その Role の layout { } に当てはまったファイルです。layout { } の無い Role には制約を書けません。
mustNot と mustBeEmpty() で禁止も書けます。
"RepositoryInterface" { layout { "src/main/kotlin/com/example/repository" / "*Repository".ktFile() }
// 満たすべき条件を否定で書く "internal にしないこと".konsist { classes().mustNot { it.hasInternalModifier } }
// 該当するものが1つも無いことを求める "Impl を同じ場所に置かないこと".konsist { classes().filter { it.hasNameEndingWith("Impl") }.mustBeEmpty() }}制約を満たさないクラスがあると違反になります。
// useCase/ の中に置かれているが、invoke 以外の関数を持っているclass GetUserUseCase(private val repository: UserRepository) { suspend fun invoke(id: String) = repository.find(id) fun clearCache() = repository.clear() // ← これ}テストで assert(FileConstraintCheck()) を呼ぶと、配置の違反と同じレポートに並びます。どのファイルの何行目かまで出ます。
[UnsatisfiedFileConstraint] file:///path/to/project/src/main/kotlin/com/example/useCase/GetUserUseCase.kt Role: UseCase / FileConstraint: "invoke 関数を1つだけ持つこと" Declaration: GetUserUseCase (line 3) Declared at: ProjectArchitecture.kt:12なお、Role の名前はドキュメント生成などで使うため、ASCII(英字で始まり、英数字・_・- が続く)しか使えません。日本語の表示名は title に書きます。
Role のグループ
Section titled “Role のグループ”"グループ名".group { ... } を使うと、Roleをグループにまとめられます。
architecture { "domain".group { "UseCase" { // ... }
"RepositoryInterface" { // ... } }}Role 設計のベストプラクティス
Section titled “Role 設計のベストプラクティス”良い Role 定義は変更を必要としない
Section titled “良い Role 定義は変更を必要としない”アプリに機能を足すたびに architecture { } を書き換える必要があるなら、その architecture { } の定義は悪い設計です。
architecture { } がディレクトリ構造の写しになって二度手間にならないよう、* や ** といったワイルドカードをうまく使ってください。
1つの Role には1種類のファイルだけを入れる
Section titled “1つの Role には1種類のファイルだけを入れる”1つの Role は、1種類のファイルだけを表すようにしてください。
たとえば Repository の interface と実装は、名前は似ていても別の種類のファイルです。1つの Role にまとめると、説明・制約・テンプレートが「どちらのファイルの話か」で分かれてしまい、konsist { } の制約にも「interface なら…、実装なら…」という条件分岐が入ります。
種類ごとに Role を分ければ、説明も制約もその種類のファイルだけを対象にでき、生成されるドキュメントも種類ごとのページになります。
🆖 2種類のファイルを1つの Role に
"Repository" { summary = "データの取得と保存" layout { "repository" / "*Repository".ktFile() // interface "repository" / "*RepositoryImpl".ktFile() // 実装 } "interface なら宣言だけ、実装なら…".konsist { // どちらのファイルかで制約を分ける必要がある }}🆗 種類ごとに Role を分ける
"RepositoryInterface" { summary = "データの取得と保存の窓口" layout { "repository" / "*Repository".ktFile() } "interface だけを置くこと".konsist { classes().mustBeEmpty() }}
"RepositoryImplementation" { summary = "RepositoryInterface の実装" layout { "repository" / "*RepositoryImpl".ktFile() }}Role 定義を分割する
Section titled “Role 定義を分割する”プロジェクトが大きくなるほど、architecture { } の定義は肥大化します。
architecture { } は Kotlin DSL なので、普通の Kotlin と同じように拡張関数へ切り出せます。
ここではおすすめの Role 定義の分割方法 を紹介します。
architecture { } 定義を分割する
Section titled “architecture { } 定義を分割する”膨らみがちなのは 各 Role 内の詳細 です。 Role ごとに拡張関数に切り出しましょう。
分割前
architecture { "UseCase" { layout { "src/main/kotlin/com/example/project/useCase" / "*UseCase".ktFile() } "invoke 関数を1つだけ持つこと".konsist { classes().must { it.functions().map { f -> f.name } == listOf("invoke") } } // ... }
"RepositoryInterface" { layout { "src/main/kotlin/com/example/project/repository" / "*Repository".ktFile() } "class を置かないこと".konsist { classes().mustBeEmpty() } // ... }
// ...}分割後
- ProjectArchitecture.kt
ディレクトリdomain/
- UseCaseRole.kt
- RepositoryInterfaceRole.kt
architecture { useCase() repositoryInterface()
// ...}// hint: DeclarationContainerScope は architecture { } と "...".group { } のレシーバです。fun DeclarationContainerScope.useCase() = "UseCase" { layout { "src/main/kotlin/com/example/project/useCase" / "*UseCase".ktFile() } "invoke 関数を1つだけ持つこと".konsist { classes().must { it.functions().map { f -> f.name } == listOf("invoke") } } // ...}fun DeclarationContainerScope.repositoryInterface() = "RepositoryInterface" { layout { "src/main/kotlin/com/example/project/repository" / "*Repository".ktFile() } "class を置かないこと".konsist { classes().mustBeEmpty() } // ...}Role 定義の中を分割する
Section titled “Role 定義の中を分割する”Role 定義の中でも、特に "...".konsist { } は、制約が多かったり複雑になったりして肥大化しがちです。
"...".konsist { } は、積極的に private な拡張関数へ切り出しましょう。
分割前
fun DeclarationContainerScope.useCase() = "UseCase" { layout { "src/main/kotlin/com/example/project/useCase" / "*UseCase".ktFile() } "invoke 関数を1つだけ持つこと".konsist { classes().must { it.functions().map { f -> f.name } == listOf("invoke") } } // ... }分割後
fun DeclarationContainerScope.useCase() = "UseCase" { layout { "src/main/kotlin/com/example/project/useCase" / "*UseCase".ktFile() }
onlyInvokeFun() // ... }
private fun RoleScope.onlyInvokeFun() = "invoke 関数を1つだけ持つこと".konsist { classes().must { it.functions().map { f -> f.name } == listOf("invoke") } }- Role は プロジェクトの中に現れてよいファイルの種類 です。パッケージやモジュールとは独立しています。
- 1つのRoleに、説明(
title/summary)・置き場所(layout { })・制約(konsist { })が まとまります。 - Role 定義はすぐに肥大化します。
architecture { }や Role 定義が膨らまないよう、拡張関数を使って適切に分割しましょう。
architecture { "domain".group { "UseCase" { summary = "各画面で発生するアプリ固有の1つの振る舞い"
// 置き場所が増えても、説明はこの1箇所のまま layout { "core/domain/src/main/kotlin/useCase" / "*UseCase".ktFile() }
"invoke 関数を1つだけ持つこと".konsist { classes().must { it.functions().map { f -> f.name } == listOf("invoke") } } } }}次のステップ
Section titled “次のステップ”- Layout … Roleのファイルをどこに置けるか宣言する方法を学びましょう。
- Konsist との統合 … Roleのファイルの中身に制約をかける方法を学びましょう。
- 初めてのアーキテクチャ定義 … あなたのプロジェクトに katachi を導入して、実際に Role を書いてみましょう。