Webエンジニア

【Kotlin】分かりづらい「セカンダリコンストラクタ」を理解をしてみる

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でループしWhenisでクラスを判定。

最後にプロパティに値をセットしています。

 

こんな事が出来るんですね。

使用側の例

OdenResにListを渡すことが出来るようになったので、このような形で渡すことが出来るようになりました。

OdenRes(listOfNotNull(
    when (どの具材が返却可能か) {
        もちきんの場合 -> OdenRes(MotikinElement(madeIn = "Japan")
        ちくわぶの場合 -> OdenRes(TikuwabuElement(madeIn = "USA")
        だいこんの場合 -> OdenRes(DaikonElement(madeIn = "Brazil")
    },
    if(条件) MustardElement() else null
))

純粋にコードの見通しがよくなりましたね。

 

プライマリコンストラクタも残しているので

OdenRes(MustardElement())

のように各Elementを一つだけ引数として持たせることも可能です。

 

「セカンダリコンストラクタ」を使用したことで、柔軟なクラスになりました。

 

「プライマリコンストラクタ」と「セカンダリコンストラクタ」

をうまく使ったクラス設計を行えば、簡潔なコードを書ける可能性が広がりそうです。

 

 

おわり

コメント

タイトルとURLをコピーしました