概要
Kotlin の sealed クラスの使い方を、実際に使ってみて考えたので書いてみます。
sealed とは
同じクラス内か同じ .kt ファイル内のクラスのみ継承を許可するキーワードです。
sealed という英単語は seal の過去形または過去分詞形で、「封印された」という意味のようです。某ポータルサイトで画像検索するとアザラシの画像ばかり出てきます。
実装
以下の状況でこの sealed が役立ちそうだと思いました。
- 同一の振る舞いをして、属性が異なるようなクラス群を定義したい
- 振る舞いの実装は外から書き換えられないようにしたい
例として、Android のアプリ内保存領域のラッパーを作る際に役立つのではないかと考えました。Android アプリでは CacheDir と FilesDir の2種類をアプリ内のデータ保存領域として用いることができます。その2種類の領域では基本的に同じようにファイルを扱うことができます。
領域 | 違い |
---|---|
CacheDir | アプリケーション管理でキャッシュを削除すると保存したファイルが消える |
FilesDir | アプリを削除すると保存したファイルが消える |
そのため、実装は共通させて、保存場所だけを初期化時に固定値で分ける、というやり方でクラスを定義したいと考え、こういう時に sealed がはまるのではないかと思いました。
共通の振る舞いを実装
File オブジェクトの割り当て、取得、インデックス指定での削除、全削除の関数を定義します。
open class StorageWrapper(context: Context, dirName: String) {
private val dir: File
fun assignNewFile(name: String): File {
val matcher = ILLEGAL_FILE_NAME_CHARACTER.matcher(name)
if (matcher.find()) {
return File(dir, matcher.replaceAll("_"))
}
return File(dir, name)
}
fun get(index: Int): File? {
if (index < 0 || index > listFiles().size) {
return null
}
return listFiles()[index]
}
fun removeAt(index: Int): Boolean {
if (index < 0 || index > listFiles().size) {
return false
}
return listFiles()[index].delete()
}
fun clean() = dir.listFiles().forEach { it.delete() }
}
準備
sealed キーワードを付けてコンストラクタを追加します。
sealed class StorageWrapper(context: Context, dirName: String) {
init {
dir = File(getDir(context), dirName)
if (!dir.exists()) {
dir.mkdirs()
}
}
protected abstract fun getDir(context: Context): File
サブクラスを定義
sealed クラスのサブクラスを定義する場合、同じ .kt ファイルの、sealed クラスの外側か内側かに定義します。
同一 .kt ファイル内の場合
まず外側に定義するやり方です。通常通りサブクラスを定義すれば OK です。
sealed class StorageWrapper(context: Context, dirName: String) {
private val dir: File
protected abstract fun getDir(context: Context): File
// ...
}
class CacheDir(context: Context, dirName: String) : StorageWrapper(context, dirName) {
override fun getDir(context: Context): File = context.cacheDir
}
class FilesDir(context: Context, dirName: String) : StorageWrapper(context, dirName) {
override fun getDir(context: Context): File = context.filesDir
}
かつてはこの方法はサポートされていなかったらしいですが、現在はOKのようです。こちらですと、下記のように CacheDir や FilesDir クラスを直接指定してインスタンスにすることが可能です。
val cacheDir = CacheDir(context, "dir_name")
val filesDir = FilesDir(context, "dir_name")
同一クラス内の場合
次に、内側に定義するやり方です。sealed クラスの内部にサブクラスを定義します。
sealed class StorageWrapper(context: Context, dirName: String) {
private val dir: File
protected abstract fun getDir(context: Context): File
// ...
class CacheDir(context: Context, dirName: String) : StorageWrapper(context, dirName) {
override fun getDir(context: Context): File = context.cacheDir
}
class FilesDir(context: Context, dirName: String) : StorageWrapper(context, dirName) {
override fun getDir(context: Context): File = context.filesDir
}
}
このケースではインスタンスを生成する際、下記のように
StorageWrapper.CacheDir(context, "dir_name")
StorageWrapper.FilesDir(context, "dir_name")
とする必要があります。