Kotlin の object は「静的(static)なクラス」ではなく、シングルトン(singleton)オブジェクトを定義する構文です。
あなたの CryptoManager の使い方はかなり典型的です。
object CryptoManager {
fun encrypt(...) { ... }
}
呼び出しは
val encrypted = CryptoManager.encrypt(data, password)
となり、
val manager = CryptoManager()
のようなインスタンス化はできません。
objectとは?
Javaで書くとイメージ的には
public final class CryptoManager {
private static final CryptoManager INSTANCE =
new CryptoManager();
private CryptoManager() {}
public static CryptoManager getInstance() {
return INSTANCE;
}
}
に近いです。
Kotlinの
object CryptoManager
は
- インスタンスが1つだけ
- 遅延生成される
- スレッドセーフ
という特徴があります。
staticとの違い
Javaの
public class Util {
public static int add(int a, int b) {
return a + b;
}
}
に近い用途で
object Util {
fun add(a: Int, b: Int): Int {
return a + b
}
}
と書けます。
使う側はほぼ同じです。
Util.add(1, 2)
ただし内部的には
- Java → 本当に static
- Kotlin object → シングルトンインスタンス
です。
Composeでよく使う?
Composeでもよく使います。
例えば
object AppColors {
val Primary = Color(0xFF6200EE)
val Secondary = Color(0xFF03DAC5)
}
や
object Routes {
const val HOME = "home"
const val SETTINGS = "settings"
}
のような
- 定数管理
- ユーティリティ
- 状態を持たない処理
に向いています。
あなたの CryptoManager もまさに
暗号化処理をまとめたユーティリティ
なので object が適切です。
objectは状態も持てる
実は変数も持てます。
object Counter {
var count = 0
}
Counter.count++
これは
Counter.count
がアプリ全体で共有されます。
つまり
object
は
- static utility
- singleton instance
の両方の役割を持っています。
sealed classとは?
次にこちら。
sealed class EncryptionException(message: String)
: Exception(message)
sealed は
継承できる型を限定する
ための仕組みです。
通常の class はどこからでも継承できます。
open class Animal
class Dog : Animal()
class Cat : Animal()
しかし
sealed class Animal
にすると
同じファイル内でしか継承できません。
sealed class Animal
class Dog : Animal()
class Cat : Animal()
これはOK。
別ファイルで
class Bird : Animal()
はNGです。
なぜ使うの?
種類を限定したいからです。
例えば
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val msg: String) : Result()
object Loading : Result()
}
すると
when(result) {
is Result.Success -> ...
is Result.Error -> ...
Result.Loading -> ...
}
で
else
が不要になります。
コンパイラが
Result の種類は全部知ってる
と判断できるからです。
あなたの例では
sealed class EncryptionException(message: String)
: Exception(message)
の子クラスが
class EmptyPassword
class EmptyData
class Failed
だけであることを保証しています。
つまり
try {
CryptoManager.encrypt(...)
} catch (e: CryptoManager.EncryptionException) {
when (e) {
is CryptoManager.EncryptionException.EmptyPassword -> ...
is CryptoManager.EncryptionException.EmptyData -> ...
is CryptoManager.EncryptionException.Failed -> ...
}
}
のように安全に分岐できます。
object と sealed class を組み合わせることも多い
例えば
sealed class UiState {
object Loading : UiState()
object Empty : UiState()
data class Success(val data: String) : UiState()
}
この
object Loading
object Empty
は
「値を持たない状態」
なのでインスタンスが1個で十分です。
Composeでは非常によく見るパターンです。
あなたのコードでいうと、
class EmptyPassword : EncryptionException(...)
はインスタンスごとに状態を持たないので、
実は
object EmptyPassword : EncryptionException(...)
と書くこともできます。
例えば
sealed class EncryptionException(message: String)
: Exception(message) {
object EmptyPassword : EncryptionException("パスワードが空です")
object EmptyData : EncryptionException("データが空です")
class Failed(cause: Throwable)
: EncryptionException("暗号化に失敗しました: ${cause.message}")
}
の方が「例外の種類だけを表したい」場合は少し自然です。
ただし例外オブジェクトを毎回新規生成したい考え方もあるので、今の class でも全く問題ありません。
おまけ
sealed class DecryptionException(message: String) : Exception(message)
Java経験者だと最初は
class WrongPasswordException : Exception()
class CorruptedDataException : Exception()
class EmptyPasswordException : Exception()
みたいに別々の例外クラスを作りがちですが、Kotlinでは
sealed class DecryptionException
でまとめる設計がとてもよく使われます。
そのおかげで
catch (e: DecryptionException)
だけで関連する例外を一括で扱えるようになります。