Kotlinの「セカンダリコンストラクタ」を最近ようやく理解出来ました。
「実際にどういった場面で使用するか」を把握する事が、より一層理解を深めます。
今回はリファクタリング例を主に「セカンダリコンストラクタ」の使い方を紹介してみます。
環境情報
- Kotlin: 1.3.30
 
Kotlin のプライマリコンストラクタとセカンダリコンストラクタって何?
「プライマリコンストラクタ」の構文として
class Person(
    val firstName: String,
    val lastName: String,
    var age: Int
) {
    // 例...
}
というようなプロパティの宣言と初期化を行う形があります。
Kotlinでよく目にする書き方です。
Classes and Inheritance – Kotlin Programming Language
一方、「セカンダリコンストラクタ」の構文を見てみます。
class Person(val name: String) {
    constructor(name: String, parent: Person) : this(name) {
        parent.children.add(this)
    }
}
サンプルコードですが、これだけではイマイチ使い方を深く理解しづらい点がありました。
ということで、「セカンダリコンストラクタ」を使った簡単なリファクタリングコードを書いてみました。
Kotlin のセカンダリコンストラクタサンプルコード
こんなJSON返却用の「OdenRes」というクラスをサンプルコードとして作成しました。
class OdenRes(
    var motikin: MotikinElement? = null,
    var tikuwabu: TikuwabuElement? = null,
    var daikon:  DaikonElement? = null,
    var mustard: MustardElement? = null
)
class MotikinElement(
    val madeIn: String = ""
)
class TikuwabuElement(
    val madeIn: String = ""
)
class DaikonElement(
    val MadeIn: String = ""
)
class MustardElement(
    val MadeIn: String = ""
)
ここでの「プライマリコンストラクタ」の引数であるプロパティは
- Motikin(もちきんちゃく)
 - Tikuwabu(ちくわぶ)
 - Daikon(大根)
 - Mustard(からし)
 
この4つです。
このクラスのインスタンス化の条件として、
- 「もちきんちゃく」「ちくわぶ」「大根」の3つは共存出来ない
- 1つのクラスが存在して残り2つのクラスは必ず「null」になる。
 
 - 上記3つの具材と共存可能なのは「からし」だけ。
 
としておきます。
以上を前提に初期化しようとすると、こんなコードになりました。
when (どの具材が返却可能か) {
    もちきんの場合 -> OdenRes(MotikinElement(madeIn = "Japan"), mustard = if (条件) MustardElement() else null)
    ちくわぶの場合 -> OdenRes(TikuwabuElement(madeIn = "Japan"), mustard = if (条件) MustardElement() else null)
    だいこんの場合 -> OdenRes(DaikonElement(madeIn = "Japan"), mustard = if (条件) MustardElement() else null)
)
「どの具材が返却可能か」と「からしは返却可能か」
の条件が混在しており、冗長な記述のように見えます。
特に「からし」の条件分岐が冗長です。
また今後
「Element追加」と「共存の条件」
が増えることによってコードの見通しが更に悪くなりそうです。
これを「セカンダリコンストラクタ」を使用してリファクタリングしてみます。
Kotlin のセカンダリコンストラクタを使用してみる
「セカンダリコンストラクタ」使用の前に軽くリファクタリングします。
「各具材の親」となる抽象クラスを作ります。
その後作成した
「抽象クラスのリスト」を引数に取る「セカンダリコンストラクタ」を作っていきます。
各Elementをまとめる
abstract classで「OdenElementBase」というクラスを作りました。
abstract class OdenElementBase {
    val madeIn: String = ""
}
class MotikinElement: OdenElementBase()
class TikuwabuElement: OdenElementBase()
class DaikonElement: OdenElementBase()
class MustardElement: OdenElementBase()
これをセカンダリコンストラクタに渡します。
セカンダリコンストラクタ使用のリファクタリング完成コード
class OdenRes(
    motikin: MotikinElement? = null,
    tikuwabu: TikuwabuElement? = null,
    daikon: DaikonElement? = null,
    mustard: MustardElement? = null
) {
    var motikin: MotikinElement? = motikin
        private set
    var tikuwabu: TikuwabuElement? = tikuwabu
        private set
    var daikon:  DaikonElement? = daikon
        private set
    var mustard: MustardElement? = mustard
        private set
    constructor(list: List<OdenElementBase>) : this(){
        list.forEach {
            when(it){
                is MotikinElement -> motikin = it
                is TikuwabuElement -> tikuwabu = it
                is DaikonElement -> daikon = it
                is MustardElement -> mustard = it
            }
        }
    }
}
今回プライマリコンストラクタの部分は残しています。
また、プロパティのセッターもprivateにしています。
早速、本題のセカンダリコンストラクタの部分を詳しく見てみます。
セカンダリコンストラクタ作成例の解説
constructor(list: List<OdenElementBase>)
ここでListを受け取る「セカンダリコンストラクタ」を作成しています。
このthis()でプライマリコンストラクタを呼び出しています。ここの呼出は必須です。
: this()
ここで、クラスのインスタンス化とプロパティの初期化を行っています。
引数なしで呼べる理由は、プライマリコンストラクタで全てnullableな引数を取っており、かつデフォルト引数がnullなためです。
list.forEach {
    when(it){
        is MotikinElement -> motikin = it
        is TikuwabuElement -> tikuwabu = it
        is DaikonElement -> daikon = it
        is MustardElement -> mustard = it
    }
}
そして最後のセカンダリコンストラクタの部分。
受け取ったリストをforEachでループしWhenとisでクラスを判定。
最後にプロパティに値をセットしています。
こんな事が出来るんですね。
使用側の例
OdenResにListを渡すことが出来るようになったので、このような形で渡すことが出来るようになりました。
OdenRes(listOfNotNull(
    when (どの具材が返却可能か) {
        もちきんの場合 -> OdenRes(MotikinElement(madeIn = "Japan")
        ちくわぶの場合 -> OdenRes(TikuwabuElement(madeIn = "USA")
        だいこんの場合 -> OdenRes(DaikonElement(madeIn = "Brazil")
    },
    if(条件) MustardElement() else null
))
純粋にコードの見通しがよくなりましたね。
プライマリコンストラクタも残しているので
OdenRes(MustardElement())
のように各Elementを一つだけ引数として持たせることも可能です。
「セカンダリコンストラクタ」を使用したことで、柔軟なクラスになりました。
「プライマリコンストラクタ」と「セカンダリコンストラクタ」
をうまく使ったクラス設計を行えば、簡潔なコードを書ける可能性が広がりそうです。
おわり
  
  
  
  

コメント