シールドクラスとインターフェース
_sealed_クラスとインターフェースは、クラス階層の継承を制御します。シールドクラスの直接のサブクラスはすべてコンパイル時に既知となります。シールドクラスが定義されているモジュールおよびパッケージの外では、他のサブクラスを宣言することはできません。シールドインターフェースとその実装にも同じ論理が適用されます。シールドインターフェースを含むモジュールがコンパイルされると、新しい実装を作成することはできません。
直接のサブクラスとは、スーパークラスから直接継承するクラスです。
間接的なサブクラスとは、スーパークラスから複数レベル下位で継承するクラスです。
シールドクラスとインターフェースをwhen
式と組み合わせると、可能なすべてのサブクラスの振る舞いを網羅し、新しいサブクラスが作成されてコードに悪影響を与えることがないようにすることができます。
シールドクラスは次のようなシナリオで最もよく使用されます。
- クラス継承を制限したい場合: クラスを拡張する、事前に定義された有限のサブクラスセットがあり、そのすべてがコンパイル時に既知である。
- 型安全な設計が必要な場合: プロジェクトにおいて安全性とパターンマッチングが重要である。特に状態管理や複雑な条件ロジックの処理において。例については、「when式でのシールドクラスの使用」を参照してください。
- クローズドなAPIを扱う場合: サードパーティのクライアントがAPIを意図したとおりに使用することを保証する、堅牢で保守しやすい公開APIをライブラリで提供したい。
より詳細な実用例については、「ユースケースのシナリオ」を参照してください。
Java 15では、同様の概念が導入されました。そこでは、シールドクラスは
sealed
キーワードとpermits
句を使用して、制限された階層を定義します。
シールドクラスまたはインターフェースの宣言
シールドクラスまたはインターフェースを宣言するには、sealed
修飾子を使用します。
// シールドインターフェースを作成
sealed interface Error
// シールドインターフェースErrorを実装するシールドクラスを作成
sealed class IOError(): Error
// シールドクラス'IOError'を拡張するサブクラスを定義
class FileReadError(val file: File): IOError()
class DatabaseError(val source: DataSource): IOError()
// 'Error'シールドインターフェースを実装するシングルトンオブジェクトを作成
object RuntimeError : Error
この例は、ライブラリがスローするエラーをライブラリユーザーが処理できるように、エラークラスを含むライブラリのAPIを表すことができます。このようなエラークラスの階層が公開APIで可視なインターフェースや抽象クラスを含む場合、他の開発者がクライアントコードでそれらを実装または拡張することを妨げるものはありません。ライブラリは外部で宣言されたエラーを知らないため、それらを自身のクラスと一貫して扱うことはできません。しかし、シールドされたエラークラスの階層を使用すると、ライブラリの作成者は、すべての可能なエラータイプを知っており、他のエラータイプが後から現れることがないことを確信できます。
この例の階層は次のようになります。
コンストラクタ
シールドクラス自体は常に抽象クラスであり、結果として直接インスタンス化することはできません。ただし、コンストラクタを含むか、継承することができます。これらのコンストラクタは、シールドクラス自体のインスタンスを作成するためではなく、そのサブクラスのために使用されます。Error
というシールドクラスと、それをインスタンス化するいくつかのサブクラスの次の例を考えてみましょう。
sealed class Error(val message: String) {
class NetworkError : Error("Network failure")
class DatabaseError : Error("Database cannot be reached")
class UnknownError : Error("An unknown error has occurred")
}
fun main() {
val errors = listOf(Error.NetworkError(), Error.DatabaseError(), Error.UnknownError())
errors.forEach { println(it.message) }
}
// Network failure
// Database cannot be reached
// An unknown error has occurred
シールドクラス内でenum
クラスを使用して、enum定数で状態を表し、追加の詳細を提供できます。各enum定数は単一のインスタンスとしてのみ存在しますが、シールドクラスのサブクラスは複数のインスタンスを持つことができます。この例では、sealed class Error
とそのいくつかのサブクラスは、enum
を使用してエラーの深刻度を示します。各サブクラスのコンストラクタはseverity
を初期化し、その状態を変更できます。
enum class ErrorSeverity { MINOR, MAJOR, CRITICAL }
sealed class Error(val severity: ErrorSeverity) {
class FileReadError(val file: File): Error(ErrorSeverity.MAJOR)
class DatabaseError(val source: DataSource): Error(ErrorSeverity.CRITICAL)
object RuntimeError : Error(ErrorSeverity.CRITICAL)
// 追加のエラータイプをここに追加できます
}
シールドクラスのコンストラクタは、2つの可視性のいずれかを持つことができます。protected
(デフォルト) またはprivate
です。
sealed class IOError {
// シールドクラスのコンストラクタはデフォルトでprotected可視性を持ちます。このクラスとそのサブクラス内で可視です。
constructor() { /*...*/ }
// privateコンストラクタ。このクラス内でのみ可視です。
// シールドクラスでprivateコンストラクタを使用すると、インスタンス化をさらに厳密に制御でき、クラス内で特定の初期化プロシージャを有効にできます。
private constructor(description: String): this() { /*...*/ }
// publicおよびinternalコンストラクタはシールドクラスで許可されていないため、エラーが発生します。
// public constructor(code: Int): this() {}
}
継承
シールドクラスおよびインターフェースの直接のサブクラスは、同じパッケージ内で宣言する必要があります。これらはトップレベルでも、任意の数の他の名前付きクラス、名前付きインターフェース、または名前付きオブジェクト内にネストされていても構いません。サブクラスは、Kotlinの通常の継承ルールと互換性がある限り、任意の可視性を持つことができます。
シールドクラスのサブクラスは、適切な完全修飾名を持つ必要があります。これらはローカルオブジェクトまたは匿名オブジェクトにすることはできません。
enum
クラスはシールドクラスやその他のクラスを拡張できません。ただし、シールドインターフェースを実装することはできます。kotlinsealed interface Error // シールドインターフェースErrorを拡張するenumクラス enum class ErrorType : Error { FILE_ERROR, DATABASE_ERROR }
これらの制限は、間接的なサブクラスには適用されません。シールドクラスの直接のサブクラスがsealedとしてマークされていない場合、その修飾子が許可するあらゆる方法で拡張できます。
// シールドインターフェース'Error'は同じパッケージとモジュール内でのみ実装を持ちます。
sealed interface Error
// シールドクラス'IOError'は'Error'を拡張し、同じパッケージ内でのみ拡張可能です。
sealed class IOError(): Error
// openクラス'CustomError'は'Error'を拡張し、可視範囲であればどこでも拡張可能です。
open class CustomError(): Error
マルチプラットフォームプロジェクトにおける継承
マルチプラットフォームプロジェクトにはもう1つの継承制限があります。シールドクラスの直接のサブクラスは、同じソースセットに存在する必要があります。これは、expectedおよびactual修飾子を持たないシールドクラスに適用されます。
シールドクラスが共通ソースセットでexpect
として宣言され、プラットフォームソースセットでactual
実装を持つ場合、expect
とactual
の両方のバージョンがそれぞれのソースセットにサブクラスを持つことができます。さらに、階層構造を使用する場合、expect
とactual
の宣言間の任意のソースセットにサブクラスを作成できます。
マルチプラットフォームプロジェクトの階層構造について詳しく学ぶ。
when
式でのシールドクラスの使用
シールドクラスを使用する主要な利点は、when
式で使用する際に発揮されます。シールドクラスとともに使用されるwhen
式は、Kotlinコンパイラがすべての可能なケースが網羅されていることを徹底的にチェックすることを可能にします。そのような場合、else
句を追加する必要はありません。
// シールドクラスとそのサブクラス
sealed class Error {
class FileReadError(val file: String): Error()
class DatabaseError(val source: String): Error()
object RuntimeError : Error()
}
// エラーをログに記録する関数
fun log(e: Error) = when(e) {
is Error.FileReadError -> println("Error while reading file ${e.file}")
is Error.DatabaseError -> println("Error while reading from database ${e.source}")
Error.RuntimeError -> println("Runtime error")
// すべてのケースが網羅されているため、`else`句は必要ありません。
}
// すべてのエラーをリストする
fun main() {
val errors = listOf(
Error.FileReadError("example.txt"),
Error.DatabaseError("usersDatabase"),
Error.RuntimeError
)
errors.forEach { log(it) }
}
when
式の繰り返しを減らすには、コンテキスト依存の解決(現在プレビュー中)を試してください。この機能により、期待される型が既知の場合、シールドクラスのメンバーをマッチングする際に型名を省略できます。詳細については、「コンテキスト依存の解決のプレビュー」または関連する「KEEP提案」を参照してください。
when
式でシールドクラスを使用する場合、単一のブランチに追加のチェックを含めるためのガード条件を追加することもできます。詳細については、「when
式のガード条件」を参照してください。
マルチプラットフォームプロジェクトでは、共通コードに
expected宣言
としてwhen
式を持つシールドクラスがある場合でも、else
ブランチが必要です。これは、actual
プラットフォーム実装のサブクラスが、共通コードで既知ではないシールドクラスを拡張する可能性があるためです。
ユースケースのシナリオ
シールドクラスとインターフェースが特に役立つ、いくつかの実用的なシナリオを探ってみましょう。
UIアプリケーションにおける状態管理
シールドクラスを使用して、アプリケーション内のさまざまなUI状態を表すことができます。このアプローチにより、UI変更の構造化された安全な処理が可能になります。この例は、さまざまなUI状態を管理する方法を示します。
sealed class UIState {
data object Loading : UIState()
data class Success(val data: String) : UIState()
data class Error(val exception: Exception) : UIState()
}
fun updateUI(state: UIState) {
when (state) {
is UIState.Loading -> showLoadingIndicator()
is UIState.Success -> showData(state.data)
is UIState.Error -> showError(state.exception)
}
}
支払い方法の処理
実用的なビジネスアプリケーションでは、さまざまな支払い方法を効率的に処理することが一般的な要件です。シールドクラスとwhen
式を使用して、そのようなビジネスロジックを実装できます。異なる支払い方法をシールドクラスのサブクラスとして表すことにより、トランザクションを処理するための明確で管理しやすい構造を確立します。
sealed class Payment {
data class CreditCard(val number: String, val expiryDate: String) : Payment()
data class PayPal(val email: String) : Payment()
data object Cash : Payment()
}
fun processPayment(payment: Payment) {
when (payment) {
is Payment.CreditCard -> processCreditCardPayment(payment.number, payment.expiryDate)
is Payment.PayPal -> processPayPalPayment(payment.email)
is Payment.Cash -> processCashPayment()
}
}
Payment
は、Eコマースシステムにおけるさまざまな支払い方法(CreditCard
、PayPal
、Cash
)を表すシールドクラスです。各サブクラスは独自の特定のプロパティを持つことができます。たとえば、CreditCard
にはnumber
とexpiryDate
、PayPal
にはemail
などです。
processPayment()
関数は、さまざまな支払い方法を処理する方法を示しています。このアプローチにより、可能なすべての支払いタイプが考慮され、将来新しい支払い方法が追加されてもシステムは柔軟に対応できます。
APIリクエスト・レスポンスの処理
シールドクラスとシールドインターフェースを使用して、APIリクエストとレスポンスを処理するユーザー認証システムを実装できます。ユーザー認証システムにはログインとログアウトの機能があります。ApiRequest
シールドインターフェースは、ログイン用のLoginRequest
とログアウト操作用のLogoutRequest
という特定の要求タイプを定義します。シールドクラスApiResponse
は、ユーザーデータを含むUserSuccess
、ユーザーが存在しない場合のUserNotFound
、およびあらゆる失敗の場合のError
など、異なる応答シナリオをカプセル化します。handleRequest
関数はwhen
式を使用してこれらのリクエストを型安全な方法で処理し、getUserById
はユーザー検索をシミュレートします。
// 必要なモジュールをインポート
import io.ktor.server.application.*
import io.ktor.server.resources.*
import kotlinx.serialization.*
// Ktorリソースを使用してAPIリクエストのシールドインターフェースを定義
@Resource("api")
sealed interface ApiRequest
@Serializable
@Resource("login")
data class LoginRequest(val username: String, val password: String) : ApiRequest
@Serializable
@Resource("logout")
object LogoutRequest : ApiRequest
// 詳細なレスポンスタイプを持つApiResponseシールドクラスを定義
sealed class ApiResponse {
data class UserSuccess(val user: UserData) : ApiResponse()
data object UserNotFound : ApiResponse()
data class Error(val message: String) : ApiResponse()
}
// 成功レスポンスで使用されるユーザーデータクラス
data class UserData(val userId: String, val name: String, val email: String)
// ユーザー認証情報を検証する関数(デモンストレーション目的)
fun isValidUser(username: String, password: String): Boolean {
// いくつかの検証ロジック(これはプレースホルダーです)
return username == "validUser" && password == "validPass"
}
// 詳細なレスポンスでAPIリクエストを処理する関数
fun handleRequest(request: ApiRequest): ApiResponse {
return when (request) {
is LoginRequest -> {
if (isValidUser(request.username, request.password)) {
ApiResponse.UserSuccess(UserData("userId", "userName", "userEmail"))
} else {
ApiResponse.Error("Invalid username or password")
}
}
is LogoutRequest -> {
// この例ではログアウト操作は常に成功すると仮定
ApiResponse.UserSuccess(UserData("userId", "userName", "userEmail")) // デモンストレーションのため
}
}
}
// getUserById呼び出しをシミュレートする関数
fun getUserById(userId: String): ApiResponse {
return if (userId == "validUserId") {
ApiResponse.UserSuccess(UserData("validUserId", "John Doe", "[email protected]"))
} else {
ApiResponse.UserNotFound
}
// エラー処理もErrorレスポンスになります。
}
// 使用方法を示すメイン関数
fun main() {
val loginResponse = handleRequest(LoginRequest("user", "pass"))
println(loginResponse)
val logoutResponse = handleRequest(LogoutRequest)
println(logoutResponse)
val userResponse = getUserById("validUserId")
println(userResponse)
val userNotFoundResponse = getUserById("invalidId")
println(userNotFoundResponse)
}