こんにちは。コミュニケーションアプリ「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
)
こうすることで、 MessageCard
を data 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