Kotlinの「セカンダリコンストラクタ」を使ったリファクタリング例

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)
    }
}

サンプルコードですが、これだけではイマイチ使い方を深く理解しづらい点がありました。

ということで、「セカンダリコンストラクタ」を使った簡単なリファクタリングコードを書いてみました。

サンプルコード

こんな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追加」と「共存の条件」が増えることによってコードの見通しが更に悪くなりそうです。

これを「セカンダリコンストラクタ」を使用してリファクタリングしてみます。

セカンダリコンストラクタを使用してみる

「セカンダリコンストラクタ」使用の前に軽くリファクタリングします。

「各具材の親」となる抽象クラスを作ります。

その後作成した「抽象クラスのリスト」を引数に取る「セカンダリコンストラクタ」を作っていきます。

各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を一つだけ引数として持たせることも可能です。

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

「プライマリコンストラクタ」と「セカンダリコンストラクタ」をうまく使ったクラス設計を行えば、簡潔なコードを書ける可能性が広がりそうです。

おわり