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を一つだけ引数として持たせることも可能です。
「セカンダリコンストラクタ」を使用したことで、柔軟なクラスになりました。
「プライマリコンストラクタ」と「セカンダリコンストラクタ」
をうまく使ったクラス設計を行えば、簡潔なコードを書ける可能性が広がりそうです。
おわり
コメント