プロンプトとコンテキスト
ある Kotlin サービス内の複数の関数が、同一の UserService、ロガー、またはトランザクションコンテキストを必要としています。チームは、グローバル変数に依存関係を隠すことなく、繰り返されるパラメータの受け渡し(plumbing)を減らしたいと考えており、context receiver から Kotlin 2.2 の context parameter へ段階的に移行する必要があります。セマンティクス、呼び出し側の挙動、あいまいさの処理、および適用境界について説明してください。対象は Kotlin/JVM エンジニアであり、特定の DI フレームワークは前提としません。
面接官が評価するポイント
優れた回答では、context parameter は実行時コンテナではなく、コンパイル時に利用可能な暗黙の依存関係であると説明します。解決は呼び出し側において現在のスコープ内で一致する型を見つけることで行われ、同一レベルに互換性のある複数の値が存在する場合はあいまい(ambiguous)となります。候補者は context receiver と context parameter を区別し、移行や機能の制限を理解し、パブリック境界においては通常のパラメータの方が明確であることが多い理由を説明します。
最初に確認すべき明確化のための質問
- その依存関係はパブリックモジュールやライブラリ API の境界を越えますか?もしそうなら、明示的なパラメータの方が検出やテストが容易であり、他言語からの呼び出しもしやすくなります。
- その依存関係はローカル DSL やトランザクション処理の内部で安定していますか?もしそうなら、context parameter によって反復的なパラメータの受け渡しを削減できます。
- 同じ型の2つの実装が同時にスコープ内に存在する可能性はありますか?もしそうなら、名前付きコンテキスト引数またはセマンティックなラッパー型を設計してください。
- プロジェクトではすでに context receiver を使用していますか?もしそうなら、コンパイラフラグと移行スコープを確認してください。同一モジュール内で両方のモードを混在させることはできません。
30秒の回答フレームワーク
「context parameter は関数のシグネチャ上で依存関係を宣言し、呼び出し側のスコープがそれを提供します。コンパイラは型によってそれを解決し、同一レベルに2つの一致がある場合はエラーになります。これはローカル DSL、トランザクション、ロギングコンテキストに適しており、反復的な受け渡しを回避できます。パブリック API、クロス言語呼び出し、または頻繁に置き換えられる依存関係については、明示的なパラメータを維持します。移行時は、両機能に互換性がないため、対応するコンパイラオプションを有効化し、レシーバ呼び出しやクラスレベルのケースを精査します。」
ステップごとの詳細解説
依存関係を宣言し、スコープ内でそれを提供します。
interface UserService {
fun findUser(id: Int): String
}
context(users: UserService)
fun greeting(id: Int): String = "Hello ${users.findUser(id)}"
fun main() {
val service = object : UserService {
override fun findUser(id: Int) = "user-$id"
}
context(service) {
println(greeting(7))
}
}シグネチャには依然として依存関係が宣言されているため、呼び出し側から確認できます。context(service) は呼び出し側で値を提供します。解決は変数名ではなく現在のスコープ内の型と一致させます。同一の互換性のある型の serviceA と serviceB が同じレベルに存在する場合、暗黙のうちに一方を選択するのではなくコンパイルエラーになります。両方の意味が有効である場合は、明示的なコンテキスト引数またはセマンティックなラッパー型を使用してください。
通常のパラメータとの境界は、情報密度に関係します。ローカル DSL に1つの不変コンテキストを共有する多数の関数がある場合、context parameter は反復的な受け渡しを削減します。ある関数が依存関係を1回しか使用しない場合、通常のパラメータの方が明確です。パブリックライブラリ API では、Java からの呼び出し、IDE での検出性、生成されるドキュメントも考慮する必要があります。暗黙的なソースが遠くに伝播するほど、保守コストが高くなります。
context receiver からの移行は、単なるキーワードの置き換えではありません。JetBrains は、context parameter には名前が必要であり、クラスレベルの context receiver には直接対応するものがないことを指摘しています。モジュール内で両方のモードを有効にすることはできません。モジュールごとにコンパイラオプションを切り替え、呼び出し側、呼び出し可能参照(callable references)、クラスレベルのレシーバを精査してください。公式ドキュメントには制限事項も記載されています。コンストラクタは context parameter を宣言できず、コンテキストプロパティはバッキングフィールドやイニシャライザを持つことができません。
無名パラメータを使用すると、未使用のローカル名を回避しながら、解決に参加させることができます。
context(_: UserService)
fun logGreeting(id: Int) {
println(greeting(id))
}判断基準は次のとおりです。「ローカルで安定した共有状態にはコンテキストを使用し、境界を越える依存関係、明示的な置き換え、または複数の実装には通常のパラメータや明示的な DI を使用する。」context parameter をサービスロケータに変えてはなりません。テストでは呼び出し側のスコープを介して依存関係を提供し続け、外側のオブジェクトがそのライフタイムを所有し続けます。
高品質な回答サンプル
context parameter は、暗黙的な依存関係のコンパイル時宣言です。関数はそのシグネチャに UserService を含み、呼び出し側は context(service) 内でインスタンスを提供します。コンパイラは現在のスコープを型によって検索し、2つの値が一致した場合はあいまいさを報告します。安定したコンテキストを共有するローカル DSL、トランザクション、ロギングチェーンにはこれを使用します。パブリック API、Java 相互運用性、頻繁に置き換えられる依存関係には、明示的なパラメータまたは DI を維持します。context receiver からの移行中は、モジュールごとにコンパイラオプションを切り替え、名前、呼び出し可能参照、クラスレベルのレシーバを精査し、コンストラクタやバッキングフィールド付きプロパティなどの制限を考慮します。
よくある間違い
- 間違い → context parameter を実行時 DI コンテナと呼ぶ → 失敗する理由: コンパイラが呼び出し側で解決するため → 修正: スコープ、型マッチング、コンパイル時のあいまいさについて説明する。
- 間違い → 同じ型の値が変数名によって選択されると思い込む → 失敗する理由: 同一レベルの一致はあいまいになるため → 修正: 明示的なコンテキスト引数またはセマンティックなラッパー型を使用する。
- 間違い →
context receiverを機械的にcontext parameterに置き換える → 失敗する理由: 命名、クラスレベルのケース、呼び出し可能参照が異なるため → 修正: モジュールごとに移行し、各呼び出し側を精査する。 - 間違い → すべての依存関係を暗黙的にする → 失敗する理由: パブリック境界での検出性が低下するため → 修正: コンテキストをローカルかつ安定した状態に保ち、境界では通常のパラメータを使用する。
フォローアップの質問と回答
フォローアップ 1: テストでコンテキスト依存関係をどのように置き換えますか?
同じインターフェースを実装するフェイクまたはスタブを作成し、context(fake) { ... } 内で対象を実行します。テストで実装を頻繁に切り替える場合、通常は明示的なパラメータの方が明確であり、テストレポートで依存関係が可視化されます。
フォローアップ 2: 同じ型の2つのサービスが共存しなければならない場合はどうしますか?
PrimaryUserService や AuditUserService などのセマンティックなラッパー型を導入するか、呼び出し側で明示的なコンテキスト引数を渡します。解決処理は宣言順序を優先度として使用しないため、宣言順序に依存してはなりません。
フォローアップ 3: なぜコンストラクタで context parameter を宣言できないのですか?
現在の Kotlin のドキュメントでは、コンストラクタが context parameter を宣言することを明示的に制限しています。オブジェクトレベルの依存関係には、通常のコンストラクタパラメータまたは明示的なファクトリを使用してください。グローバルなコンテキストを強制すると、ライフタイムやスレッドの境界が不明瞭になります。