こんにちは。コミュニケーションアプリ「LINE」のモバイルクライアントを開発している石川です。
この記事は、毎週木曜の定期連載 "Weekly Report" 共有の第 6 回です。Weekly Report については、第 1 回の記事を参照してください。
乱切りか角切りか
以下のコードは、「もし ContactModel
が Person
タイプかつ "friend" 状態なら表示名 (displayName
) を取得する」というものです。
fun ...(contact: ContactModel): ReturnValue? {
val friendName = (contact as?
ContactModel.Person)?.takeIf {
it.isFriend
}?.let { normalizeEmoji(it.displayName) } ?: return null
// snip...
// snip...
}
なお、let
は Kotlin の標準の関数で、このコードの場合は以下のように振る舞います。
?.takeIf
の戻り値が null の場合: 何もせず null を返す?.takeIf
の戻り値が非 null の場合: 戻り値をit
としてnormalizeEmoji(it.displayName)
を呼び、その結果を返す
このコードで改善できる点はなにかありますか?
切る前にナイフを研ごう
上記のコードは不適切な改行によって、読みにくいものになっています。ロジックには全く手を加えずとも、改行の位置を変えるだけでも読みやすくすることができます。
val friendName = (contact as? ContactModel.Person)
?.takeIf { it.isFriend }
?.let { normalizeEmoji(it.displayName) }
?: return null
基本的には「意味が大きく区切られる」場所で改行するとよいでしょう。しかし、適切な改行位置を選ぶのが難しいときもあります。そのような場合は、コードを自然言語(英語や日本語など)に翻訳してみるのも一つの手段です。そうすることで、どこに大きな区切りがあるかが明確になることもあります。
上記のコードは以下のように翻訳できます。
If a contact is a "Person" and the person is a friend, take the name with normalizing; otherwise this returns null.
ここで、意味が大きく区切られる場所にスラッシュを挿入すると、以下のようになります。
If a contact is a "Person" / and the person is a friend, / take the name with normalizing; / otherwise this returns null.
そしてこれは、改善したコードの改行位置に対応します。
一方で、最初の読みにくい例の改行位置に対応するスラッシュを挿入すると、以下のようになります。
If a contact is a / "Person" and the person is / a friend / , take the name with normalizing; otherwise this returns null.
正しい位置に改行を入れることで、より発展的なリファクタリングが行えるようにもなります。1 行目の as? ContactModel.Person
と 2 行目の .takeIf
はフィルタの役割を果たしていて、4 行目の ?: return null
がそれに対応する結果になっています。そうすると、3 行目の normalizeEmoji(it.displayName)
はメソッドチェーンの外に移動してもよさそうということに気づけます。
val friend = (contact as? ContactModel.Person)
?.takeIf { it.isFriend }
?: return null
val friendName = normalizeEmoji(friend.displayName)
様々な 切り方
メソッドチェーン / フォールバックチェーン
ドット演算子 .
やセーフコール演算子 ?.
などによるメソッドチェーンや、?:
などによるフォールバックチェーンを使う場合、コードの詳細な部分よりも、ロジックの構造や流れの方が重要なことが多いです。その場合は、.
・?.
・?:
演算子の直前に改行を入れるとよいでしょう。
val ... = someCollection
.filterIsInstance<SomeModel>()
.filter { ... }
.map { ... }
.toSet()
val ... = nullable?.value
?: fallback.value
?: another.fallback(value)
もし、実引数が長くなる場合(ラムダを含む)は、引数が短くなるように補助的な関数や拡張関数などを使うと改行位置を整えることができる場合があります。
val ... = nullable?.value
?: fallback.shortcut
?: another.fallback(value)
...
private val Fallback.shortcut: ...? get() =
value.with(long.long.long.long.long.long.long.argument)
演算子の優先順位
多くの場合、演算子の優先順位は意味の繋がりの強さに一致します。 例えば、以下の 2 つのコードを比較した場合、==
で改行を行う方が、+
や -
で改行するよりも理解しやすいコードになります。
valueWithLongName1 - valueWithLongName2 ==
valueWithLongName3 + valueWithLongName4
valueWithLongName1 -
valueWithLongName2 == valueWithLongName3 +
valueWithLongName4
式に ()
を使う場合は、()
で括られた部分はより強い繋がりを持ちます。
valueWithLongName1 *
(valueWithLongName2 + valueWithLongName3)
エルヴィスリターン ?: return
もし、あるコードの影響が局所部分に閉じない場合は、その影響が強調されるように改行位置を工夫するべきです。return
や throw
はその代表例と言えるでしょう。return
や throw
を使う場合、コードの左側に置くことでそれらを強調できます。
したがってエルヴィスリターン ?: return
や ?: throw
、?: error(...)
を使う場合は、その直前に改行を入れると良いでしょう。
val nonNullValue = some.nullable.value.with(parameter)
?: return someReturnValue
一言まとめ
コードの改行を行う際は、意味の区切りを意識する。
キーワード: line-break
, code chunk
, operator precedence