要約
- プリミティブ型に拡張関数を書くと、必要以上に広いスコープが対象になってしまう
-
value class
を活用するとスコープと用途を限定できて良いかも- 絶対こうすべきという強い思想はまったくなく、検討する価値のある方法の1つという意
よくある場面
🙂<1500
のようにIntで扱っている値を画面に表示する際に、1,500円
のようにカンマ区切りにして「円」をつけたStringに変換したいな
val formatedPrice: String = "%,d".format(1500) + "円"
println(formatedPrice)
// -> 1,500円
🙂<この変換はいろいろな箇所で使うからInt.kt
ファイルに拡張関数として追加しておこう
kotlin Int.kt
fun Int.format(): String = "%,d".format(this) + "円"
😊<簡単に呼び出せるようになって、コードもスッキリした
val formatedPrice: String = 1500.format()
println(formatedPrice)
// -> 1,500円
問題点
- 追加したい拡張関数がこれだけなら問題ない
- (ほとんどの場合は追加したい処理がどんどん増えていく)
- 用途が限定的なので、Intすべてが使える関数として定義するにはスコープが広すぎる
- 今後同じようなことをやりたい場合
Int.kt
に続々と関数が追加され肥大化する- このままいくとIntはグローバルなので、金額の変換、日付の変換などすべてが一同に介してしまう
1ヶ月後...
🤮<Int.kt
に拡張関数を生やしすぎてが1000行以上あって混沌としている!
kotlin Int.kt
// 円を末尾につける
fun Int.formatYen(): String = {...}
// ドルを末尾につける
fun Int.formatDoller(): String = {...}
// ユーロを末尾につける
fun Int.formatEuro(): String = {...}
// yyyy/MM/ddの形式に変換する
fun Int.formatYYYYMMDD(): String = {...}
...
...
解決策 -> value classの利用を検討しよう
value class
とは
- data classとだいたい同じ
- コンパイル時にプリミティブな値としてバイトコードを生成するので、パフォーマンスはdata classよりも良い
- 単一のプロパティしか持てない
-
@JvmInline
をつける必要がある - Value Objectとして利用することに適している
- 参考:https://star-zero.medium.com/kotlin%E3%81%AEvalue-class-27e865696f35
value classにメソッドを生やすことで、スコープを限定できる
-
Int
の拡張関数を使うのとパフォーマンス的にもほぼ変わらずにかける -
Price
,Date
のように扱う対象ごとに分けられる - プリミティブ値は値の渡し間違いなどがあるため、value classを使うと型安全に扱える(value object)
kotlin Price.kt
@JvmInline
value class Price(private val value: Int) {
fun format(): String = "%,d".format(this.value) + "円"
}
val price: Price = Price(1500) // <- オリジナルの型として扱える
val formatedPrice: String = Price(1500).format()
println(formatedPrice)
// -> 1,500円
プリミティブに拡張関数を生やすべきでないか
- まずは
value object
で扱う範囲を限定して汎用関数にする方法を検討するとクリーンなコードになる - 上記では収まらず本当に汎用的に行う処理であれば、プリミティブ型の拡張関数を作るのは良さそう
- プリミティブ型の汎用的な関数は、言語仕様として豊富に提供されている
- 標準の関数で事足りない場合、そのプロジェクト特有の対象(ドメイン)を扱っている可能性が高い
- であれば、グローバルなプリミティブ型に直接生やすのではなく、value objectを作ってドメインとして扱ったほうがクリーンな気がする
- 大半の場合はvalue objectに生やす方向で事足りそうなので、一見の価値あり
参考文献