シールドクラスとインターフェース
_シールド_クラスとインターフェースは、クラス階層の継承を制御します。シールドクラスの直接のサブクラスはすべてコンパイル時に既知となります。シールドクラスが定義されているモジュールおよびパッケージの外では、他のサブクラスを宣言することはできません。シールドインターフェースとその実装にも同じ論理が適用されます。シールドインターフェースを含むモジュールがコンパイルされると、新しい実装を作成することはできません。
直接のサブクラスとは、スーパークラスから直接継承するクラスです。
間接的なサブクラスとは、スーパークラスから複数レベル下位で継承するクラスです。
シールドクラスとインターフェースをwhen式と組み合わせると、可能なすべてのサブクラスの振る舞いを網羅し、新しいサブクラスが作成されてコードに悪影響を与えることがないようにすることができます。
シールドクラスは次のようなシナリオで最もよく使用されます。
- クラス継承を制限したい場合: クラスを拡張する、事前に定義された有限のサブクラスセットがあり、そのすべてがコンパイル時に既知である。
- 型安全な設計が必要な場合: プロジェクトにおいて安全性とパターンマッチングが重要である。特に状態管理や複雑な条件ロジックの処理において。例については、「when式でのシールドクラスの使用」を参照してください。
- クローズドなAPIを扱う場合: サードパーティのクライアントがAPIを意図したとおりに使用することを保証する、堅牢で保守しやすい公開APIをライブラリで提供したい。
より詳細な実用例については、「ユースケースのシナリオ」を参照してください。
Java 15では、同様の概念が導入されました。そこでは、シールドクラスは
sealedキーワードとpermits句を使用して、制限された階層を定義します。
シールドクラスまたはインターフェースの宣言
シールドクラスまたはインターフェースを宣言するには、sealed修飾子を使用します。
// Create a sealed interface
sealed interface Error
// Create a sealed class that implements sealed interface Error
sealed class IOError(): Error
// Define subclasses that extend sealed class 'IOError'
class FileReadError(val file: File): IOError()
class DatabaseError(val source: DataSource): IOError()
// Create a singleton object implementing the 'Error' sealed interface
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)
// Additional error types can be added here
}シールドクラスのコンストラクタは、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としてマークされていない場合、その修飾子が許可するあらゆる方法で拡張できます。
// Sealed interface 'Error' has implementations only in the same package and module
sealed interface Error
// Sealed class 'IOError' extends 'Error' and is extendable only within the same package
sealed class IOError(): Error
// Open class 'CustomError' extends 'Error' and can be extended anywhere it's visible
open class CustomError(): Errorマルチプラットフォームプロジェクトにおける継承
マルチプラットフォームプロジェクトにはもう1つの継承制限があります。シールドクラスの直接のサブクラスは、同じソースセットに存在する必要があります。これは、expectedおよびactual修飾子を持たないシールドクラスに適用されます。
シールドクラスが共通ソースセットでexpectとして宣言され、プラットフォームソースセットでactual実装を持つ場合、expectとactualの両方のバージョンがそれぞれのソースセットにサブクラスを持つことができます。さらに、階層構造を使用する場合、expectとactualの宣言間の任意のソースセットにサブクラスを作成できます。
マルチプラットフォームプロジェクトの階層構造について詳しく学ぶ。
when式でのシールドクラスの使用
シールドクラスを使用する主要な利点は、when式で使用する際に発揮されます。シールドクラスとともに使用されるwhen式は、Kotlinコンパイラがすべての可能なケースが網羅されていることを徹底的にチェックすることを可能にします。そのような場合、else句を追加する必要はありません。
// Sealed class and its subclasses
sealed class Error {
class FileReadError(val file: String): Error()
class DatabaseError(val source: String): Error()
object RuntimeError : Error()
}
// Function to log errors
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")
// No `else` clause is required because all the cases are covered
}
// List all errors
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 necessary modules
import io.ktor.server.application.*
import io.ktor.server.resources.*
import kotlinx.serialization.*
// Define the sealed interface for API requests using Ktor resources
@Resource("api")
sealed interface ApiRequest
@Serializable
@Resource("login")
data class LoginRequest(val username: String, val password: String) : ApiRequest
@Serializable
@Resource("logout")
object LogoutRequest : ApiRequest
// Define the ApiResponse sealed class with detailed response types
sealed class ApiResponse {
data class UserSuccess(val user: UserData) : ApiResponse()
data object UserNotFound : ApiResponse()
data class Error(val message: String) : ApiResponse()
}
// User data class to be used in the success response
data class UserData(val userId: String, val name: String, val email: String)
// Function to validate user credentials (for demonstration purposes)
fun isValidUser(username: String, password: String): Boolean {
// Some validation logic (this is just a placeholder)
return username == "validUser" && password == "validPass"
}
// Function to handle API requests with detailed responses
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 -> {
// Assuming logout operation always succeeds for this example
ApiResponse.UserSuccess(UserData("userId", "userName", "userEmail")) // For demonstration
}
}
}
// Function to simulate a getUserById call
fun getUserById(userId: String): ApiResponse {
return if (userId == "validUserId") {
ApiResponse.UserSuccess(UserData("validUserId", "John Doe", "[email protected]"))
} else {
ApiResponse.UserNotFound
}
// Error handling would also result in an Error response.
}
// Main function to demonstrate the usage
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)
}