Delegation = 委任
Delegationには色々な意味がありますが、ここでは『Delegation = 委任:他の人やモノに任せる、委ねること』と考えるとわかりやすいです。
また、“by”は、方法や手段を表す時などに使われる単語です。『〜で(〜を使って)』と訳すことができます。
たとえば、”I went to the library by car.” を日本語に訳すと、『図書館に車で行きました。』となります。
すなわち、byデリゲートとは、『何らかの設定や処理を、byによって指定されたモノに任せること』と表現することができます。
シンプルな例を使って、byデリゲートについてもう少し具体的に考えてみましょう。
通常、interface(インターフェース)を継承したクラスは、インターフェースで定義されたプロパティやメソッドをオーバーライドする必要があります。
interface Profile {
val name: String
}
class MyProfile: Profile {
override val name = "Tom"
}
ここまでは、interfaceとclassの関係の基本ですね。
Profileインターフェースを継承したclassを定義するには、nameプロパティをオーバーライドしなければならない。当然です。
ですが、Profileを継承するclassがもっとたくさん必要になったら、いちいちオーバーライドするのはとても面倒ですよね。
それでも、下のようにオーバーライドするしか方法はないのでしょうか?
interface Profile {
val name: String
}
class MyProfile: Profile {
override val name = "Tom"
}
class MyProfileB: Profile {
override val name = "Tom"
val age = 26
}
このようにしてはダメだということはないのですが、こんな面倒なことをしなくてもKotlinでは、Profileインターフェースの継承をMyProfileクラスに委任することができます。
そこで出てくるのが、byデリゲートです。
下は、byデリゲートを使ってProfileインターフェースの継承をMyProfileクラスに委任することで、MyProfileBクラスでオーバーライドを省略できるようにした例です。
interface Profile {
val name: String
}
class MyProfile: Profile {
override val name = "Tom"
}
class MyProfileB: Profile by MyProfile() {
val age = 26
}
fun main() {
val myProfileB = MyProfileB()
println(myProfileB.name) // Tom
println(myProfileB.age) // 26
}
MyProfileBクラスは、nameプロパティのオーバーライドをMyProfileクラスに委任しているため、自身ではオーバーライドする必要がありません。
まさに、他のクラスに処理(設定)を『任せている』ということですね。これがbyデリゲートの基本です。
ただし、この例だけではさすがにbyデリゲートの理解を深めることは難しいかと思います。
というわけで、次のセクションではもう少し応用的なbyデリゲートの基本をご紹介します。
POINT!
- ・Delegation = 委任!
- ・『何らかの設定や処理を他のモノに任せてしまう』と理解しよう!
- ・例えば、プロパティのオーバーライドをbyデリゲートで他のクラスに委任することができる!
プロパティの設定を委任する(Delegated properties)
あるクラスのインスタンス作成時に文字列を設定すると、その文字列が全て大文字に変換された上で、『**文字列**』というふうに、装飾された文字列が変数に格納されるようにしたい場合、どのような方法が考えられるでしょう?
具体的には次のようなイメージです。
NameDecorationClass("Tom")
➔ "**TOM**" is set into the variable.
なお、変数に格納された文字列は、クラスのインスタンスを作成した後でも変更可能なように設計する必要があるとします。
この時、まず思い浮かぶのは、カスタムゲッターとカスタムセッターを利用する方法ではないでしょうか?
たとえば、次のようにclassを定義すれば、NameDecorationクラスのインスタンスが作成された時と、その後プロパティを変更した時で、それぞれ期待する結果が得られます。
class NameDecoration(var name: String) {
var decoratedName: String = name
get() = "**${field.uppercase()}**"
set(value) {
field = value.uppercase()
}
}
fun main() {
val myName = NameDecoration("Tom")
println(myName.decoratedName) // **TOM**
myName.decoratedName = "Marie"
println(myName.decoratedName) // **MARIE**
}
やりたいことができて一件落着!…と言いたいところですが、次のような問題が出てきたらどうしましょう?
『文字列が大文字に変換されるのではなく、小文字に変換されるようにしたものも必要になった』
既に作成したclassとほとんど同じような仕組みなのに、わざわざもう一つ同じようなclassを追加しなければならないのでしょうか?次のように。
class NameDecoration(var name: String) {
var decoratedName: String = name
get() = "**${field.uppercase()}**"
set(value) {
field = value.uppercase()
}
}
class NameDecorationToLowercase(var name: String) {
var decoratedName: String = name
get() = "**${field.lowercase()}**"
set(value) {
field = value.lowercase()
}
}
fun main() {
val myName = NameDecoration("Tom")
val myLowercaseName = NameDecorationToLowercase("Tom")
println(myName.decoratedName) // **TOM**
println(myLowercaseName.decoratedName) // **tom**
myName.decoratedName = "Marie"
myLowercaseName.decoratedName = "Marie"
println(myName.decoratedName) // **MARIE**
println(myLowercaseName.decoratedName) // **marie**
}
なんだか冗長で、無駄の多いコードになってしまった感じがしますね。
サンプルでは、classの役割が『文字列を大文字または小文字に変換して装飾を加える』というシンプルなものなので、まだこれぐらいで収まっていますが、classの役割が複雑になるとさらにコードが長くなってしまうことでしょう。
そこで活用したいのが、『byデリゲートによるプロパティの委任』です。
classごとにカスタムゲッターやセッターを記述してプロパティを設定するのではなく、その部分は他のところに記述して、プロパティの設定はそいつに任せる…というイメージです。
Kotlinではプロパティの設定を他のclassに委任(デリゲート)できるように、KPropertyという仕組み(API)が用意されています。
今回の例で使用した、『文字列になんらかの処理(大文字に変換するなど)の関数を実行し、装飾された文字列を返す』という仕組みを他のclassでデリゲートできるように、byデリゲート用のclassを定義すると次のようになります。
import kotlin.reflect.KProperty
class Delegate(initialValue: String, someFunction: (String) -> String) { // ..0
private var storedValue: String = initialValue // ..1
private val delegateFun = someFunction // ..2
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "**${delegateFun(storedValue)}**" // ..3
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
storedValue = value // ..4
}
}
なんだかいきなり色々出てきて混乱しそうになるかもしれませんが、慌てずに一つ一つ分解して理解していきましょう。
コードにコメントで番号を振っているので、その番号順に解説を進めていきたいと思います。
0: コンストラクタの設定
まず前提として、今回のDelegateクラスでは『文字列と、文字列を引数に受け取って文字列を返す関数』をコンストラクタとして受け取る必要があるということを確認しておきましょう
文字列を装飾したいので文字列を受け取る必要があるのは当然ですが、文字列を大文字や小文字に変換して文字列を返す…といった処理(関数)も受け取る必要がある点に注意が必要です。
1: 初期値の格納
最初はclassのインスタンスが作成された時にコンストラクタに渡された文字列がプロパティに設定されるようにする必要があります。
ということで、初期値は受け取った文字列(=classのインスタンス作成時に渡される文字列)です。
2: 受け取った関数の格納
コンストラクタで渡されたパラメータはコンストラクタの実行が終了するとスコープの終了と共に参照できなくなるため、渡された関数をoperator funの中で使えるようにインスタンス変数に格納しておきます。
3: 文字列の装飾と関数の実行
getValueメソッドで、1と2で格納しておいた変数を使って文字列に関数を実行&装飾した結果を返します。
4: 文字列の更新
setValueメソッドでは、文字列が更新された場合に、storedValueが更新されるようにしています。
『で、KPropertyって何?』とか、『thisRef: Any?って使われてないけど何これ?』とか、ここでは解説しきれないものも残っていますが、byデリゲートの基本的な仕組みを知る上では、こういった細かい点はあまり重要ではありません。
byデリゲートの基本を理解し、扱いに慣れて興味が出てきた頃に、公式ドキュメントを読み込んで理解を深めたら良いと思います。
さて、byデリゲートの準備が整ったところで、Delegateクラスを利用してプロパティの設定を委任してみましょう。
コードは次のようになります。
class NameDecoration(val name: String) {
var decoratedName by Delegate(name) {
it.uppercase()
}
var decoratedLowercaseName by Delegate(name) {
it.lowercase()
}
}
fun main() {
val myName = NameDecoration("Tom")
println(myName.decoratedName) // **TOM**
println(myName.decoratedLowercaseName) // **tom**
myName.decoratedName = "Marie"
myName.decoratedLowercaseName = "Marie"
println(myName.decoratedName) // **MARIE**
println(myName.decoratedLowercaseName) // **marie**
}
クラスを1つにまとめることができた上、コードの量も減って随分とスッキリしましたね!
さらに、『文字列を受け取って、文字列を返す』という関数であれば、大文字や小文字に変換する他にも、様々な関数を定義してDelegateクラスに渡すことができるため、コードの柔軟性も高まったと言えます。
ちなみに、Delegate(name) { ~ } という書き方になっているのは、Delegateクラスの最後のパラメータが関数だからです。
最後のパラメータが関数の場合、ラムダ式で { } を使って記述できるというのは、Kotlinの基本でしたよね。
実際のAndrodアプリ開発でも、by remember { ~ }
や、by lazy { ~ }
という形で良く出てきます。
下に今回の解説で使用したサンプルの全体的なコードを掲載しますので、参考になれば幸いです!
最初から全てを一度で理解・把握できる人間はいません。
そもそも、最初はデリゲート元を自分で作成することもあまりないかと思います。
あり得るとしたら、remember
やlazy
など予め用意されたデリゲート元を利用するぐらいです。
なので、まずは次のようにざっくりと理解するだけでも十分かと思います。
『by』が出てきたら何らかの設定を他のclassや関数に任せている ➔ だからコードがスッキリ書けて便利!
こういったシンプルな理解があるだけで、『今自分がどんなコードを書いているか』を把握しやすくなります。
少なくとも、『デリゲートって何がなんだか全然わからないけど、とりあえずコードの書き方を丸暗記しとこう』というやり方よりは、断然スキルが身につきやすくなると言えるでしょう。
POINT!
- ・KProperty APIを利用してプロパティをデリゲートできる!
- ・プロパティをデリゲートすることで、コードがスッキリシンプルにまとまる!
- ・まずはざっくりと大雑把に理解することから始めよう!