LINEヤフー Tech Blog

LINEヤフー株式会社のサービスを支える、技術・開発文化を発信しています。

コード品質向上のテクニック:第64回 プライマリコンストラクタはシンプルにする

こんにちは。コミュニケーションアプリ「LINE」のモバイルクライアントを開発している大石です。

この記事は、毎週木曜の定期連載 "Weekly Report" 共有の第 64 回です。 LINEヤフー社内には、高い開発生産性を維持するための Review Committee という活動があります。ここで集まった知見を、Weekly Report と称して毎週社内に共有しており、その一部を本ブログ上でも公開しています。(Weekly Report の詳細については、過去の記事一覧を参照してください)

プライマリコンストラクタはシンプルにする

以下のような MessageCard クラスがあるとします。

class MessageCard(userId: String, addressBook: AddressBook, messageTemplate: String) {

    val name: String = addressBook.getContact(userId)?.name ?: ""

    val birthDay: LocalDate? = addressBook.getContact(userId)?.birthDay

    val message: String = messageTemplate.replace(NAME_PLACEHOLDER, name)

    companion object {
        private const val NAME_PLACEHOLDER: String = "%NAME%"
    }
}

このコードには何か問題がありますか?

プライマリコンストラクタのパラメータはクラスのプロパティに一致させる

MessageCard クラスのプロパティは name, birthDay, message の3つです。一方で、このクラスのプライマリコンストラクタのパラメータは userId, addressBook, messageTemplate ですが、これらはプロパティの値を導出するためだけに用いられています。このような実装はあまり好ましくありません。

一般的に、プライマリコンストラクタの中で「パース」や「変換」のような処理をするのは避けるべきです。そして、プライマリコンストラクタのパラメータは、そのクラスのプロパティとできる限り一致するようにするのが好ましいです。

つまり、この MessageCard クラスの実装は以下のようにするのが良いでしょう。

data class MessageCard(
    val name: String,
    val birthDay: LocalDate?,
    val message: String
)

こうすることで、 MessageCarddata class として宣言することができるというオマケも付いてきます。

ただし、このままでは元のコードにあった userId, addressBook, messageTemplate から MessageCard のインスタンスを作る機能がありません。そのような処理はセカンダリコンストラクタやファクトリ関数に書くのがよいでしょう。

「変換」のためのセカンダリコンストラクタ

単純な変換のような処理はセカンダリコンストラクタとして記述するのが良いでしょう。たとえば仮に、 MessageCard のインスタンスが Contact オブジェクトと messageTemplate から生成されるのであれば、以下のようなコードになるでしょう。

data class MessageCard(
    val name: String,
    val birthDay: LocalDate?,
    val message: String
) {
    constructor(contact: Contact, messageTemplate: String) : this(
        contact.name,
        contact.birthDay,
        messageTemplate.replace(NAME_PLACEHOLDER, contact.name)
    )

    companion object {
        private const val NAME_PLACEHOLDER: String = "%NAME%"
    }
}

しかし、セカンダリコンストラクタは最初のコードのような複雑な処理を記述するのには向いていません。

柔軟なオブジェクト生成のためのファクトリ関数

オブジェクト生成をより柔軟な形で記述するにはファクトリ関数を使いましょう。Kotlin では、ファクトリ関数は companion object 内に記述することが多いです。

data class MessageCard(
    val name: String,
    val birthDay: LocalDate?,
    val message: String
) {
    companion object {
        fun buildMessageCardOrNull(
            userId: String,
            addressBook: AddressBook,
            messageTemplate: String
        ): MessageCard? {
            val contact = addressBook.getContact(userId) ?: return null
            val message = messageTemplate.replace(NAME_PLACEHOLDER, contact.name)
            return MessageCard(contact.name, contact.birthDay, message)
        }

        private const val NAME_PLACEHOLDER: String = "%NAME%"
    }
}

ファクトリ関数は少し冗長なコードになりますが、代わりに以下のような長所があります。

  • オブジェクトの生成方法を関数名の形で記述できる。
  • オブジェクト生成がエラーとなったことを null などの値で返すことができる。
  • 対象のクラスのインスタンスそのものではなく、そのサブクラスのインスタンスを生成して返すことができる。

Constructor-like なファクトリ関数

さらに特別な例として、クラス名やインターフェース名と同じ名前の top-level 関数をファクトリ関数として用いることがあります。これは、コンストラクタ呼び出しと同じような見た目で使用できるので constructor-like なファクトリ関数と呼ばれます。

interface Foo {
    val name: String
    fun doSomething()
}

// Constructor-like factory function
fun Foo(): Foo {
    return FooImpl()
}

internal class FooImpl : Foo {
    override val name: String
        get() = TODO()

    override fun doSomething() {
        TODO()
    }
}


val foo: Foo = Foo() // This constructs an instance of `FooImpl`.

Constructor-like なファクトリ関数は kotlinx.coroutines のようなライブラリでインターフェースとその実装クラスを分離するためによく用いられます(例: kotlinx.coroutines.Job)。しかし逆に、コンストラクタのように見えるのに実際はコンストラクタではないという点が紛らわしいと感じられることもあるので、アプリケーションコード内では使用しないほうが無難です。

一言まとめ

プライマリコンストラクタ内でパースや変換といった処理を行わない。代わりにセカンダリコンストラクタやファクトリ関数を実装する。

キーワード: constructor, factory function, data class

コード品質向上のテクニックの他の記事を読む

コード品質向上のテクニックの記事一覧