LINEヤフー Tech Blog

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

コード品質向上のテクニック:第52回 分岐を伐り根を枯らす

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

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

分岐を伐り根を枯らす

あるサービスに、3 つのアカウントタイプ (無料、ビジネス、プレミアム) があるとしましょう。さらに、アカウントタイプに応じて、3 つの UI 要素 (背景、アイコン、テキスト) の表示を、以下のように切り替えたいとします。

UI element \ account typeFreeBusinessPremium
Background colorgrayblueyellow
Icon image👤👔👑
Type textFree...BusinessPremium!!

この 2 次元の表を実装するには、以下のように、アカウントタイプか UI 要素のどちらかでコードを分ける必要があります。

Option 1 縦軸で切る: アカウントタイプごとに関数やコードブロックを分ける。分けたコード内で、すべての UI 要素を設定する。

Option 2 横軸で切る: UI 要素ごとに関数やコードブロックを分ける。分けたコード内で、アカウントタイプに応じた要素の値を決定する。

可読性や頑健性の観点では、どちらのオプションが好ましいでしょうか?

分岐を閉じ込める

Option 2、つまり UI 要素ごとにコードを分割する方がよいケースが多いです。その理由を探るためにも、Option 1 と 2 それぞれの実装を見比べてみましょう。以下の実装ではどちらも、分割したコードを補助的な private 関数として抽出しています。

Option 1

/**
 * Updates UI to indicate a given account type.
 */
fun updateAccountTypeLayout(accountType: AccountType) {
    when (accountType) {
        AccountType.FREE -> updateLayoutForFree()
        AccountType.BUSINESS -> updateLayoutForBusiness()
        AccountType.PREMIUM -> updateLayoutForPremium()
    }
}

private fun updateLayoutForFree() {
    backgroundUiElement.color = Color.GRAY
    accountTypeIconUiElement.image = imageProvider.get(FREE_IMAGE_ID)
    accountTypeTextUiElement.text = "Free..."
}

private fun updateLayoutForBusiness() {
    backgroundUiElement.color = Color.BLUE
    accountTypeIconUiElement.image = imageProvider.get(BUSINESS_IMAGE_ID)
    accountTypeTextUiElement.text = "Business"
}

private fun updateLayoutForPremium() {
    backgroundUiElement.color = Color.YELLOW
    accountTypeIconUiElement.image = imageProvider.get(PREMIUM_IMAGE_ID)
    accountTypeTextUiElement.text = "Premium!!"
}

Option 2

/**
 * Updates UI (background color, icon, and text) to indicate
 * a given account type.
 */
fun updateAccountTypeLayout(accountType: AccountType) {
    updateBackgroundColor(accountType)
    updateIconImage(accountType)
    updateTypeText(accountType)
}

private fun updateBackgroundColor(accountType: AccountType) {
    backgroundUiElement.color = when (accountType) {
        AccountType.FREE -> Color.GRAY
        AccountType.BUSINESS -> Color.BLUE
        AccountType.PREMIUM -> Color.YELLOW
    }
}

private fun updateIconImage(accountType: AccountType) {
    val iconId = when (accountType) {
        AccountType.FREE -> FREE_IMAGE_ID
        AccountType.BUSINESS -> BUSINESS_IMAGE_ID
        AccountType.PREMIUM -> PREMIUM_IMAGE_ID
    }
    accountTypeIconUiElement.image = imageProvider.get(iconId)
}

private fun updateTypeText(accountType: AccountType) {
    accountTypeTextUiElement.text = when (accountType) {
        AccountType.FREE -> "Free..."
        AccountType.BUSINESS -> "Business"
        AccountType.PREMIUM -> "Premium!!"
    }
}

Option 2 には次のような 2 つの利点があります。

  1. 補助的な private 関数の詳細を読まずとも、主となる関数 (updateAccountTypeLayout) を読むだけで動作が想像できる: Option 1 では、updateAccountTypeLayout を読んだだけでは「アカウントタイプごとに UI 更新ロジックが異なる」としか分からない。一方で、Option 2 では「背景、アイコン、テキストの 3 つを更新する」ことが分かるようになっている。
  2. 新しい UI 要素やアカウントタイプを追加するときに、すべての組み合わせが実装されていることを保証できる: Option 1 では、新しいアカウントタイプ ULTIMATE を追加したとき、補助的な関数内で accountTypeTextUiElement の更新を忘れてしまっても、コンパイル時に検出できない。さらに、新しい UI 要素を追加したときは、すべての既存の関数を更新しなくてはならないが、それを忘れてもやはり検出できない。一方で Option 2 では、アカウントタイプと UI 要素の組み合わせに実装漏れがある場合は、when 式によってコンパイル時に検出できる。

また、Option 2 に対しては、補助的な関数に抽出するコードの範囲を最適化するといった、さらなるリファクタリングを行えます。Option 2-a では、副作用のあるコードを updateAccountTypeLayout に移すことで、補助的な関数を参照透過にしています。

Option 2-a

fun updateAccountTypeLayout(accountType: AccountType) {
    backgroundUiElement.color = getBackgroundColorInt(accountType)
    val iconId = getIconId(accountType)
    accountTypeIconUiElement.image = imageProvider.get(iconId)
    accountTypeTextUiElement.text = getTypeText(accountType)
}

private fun getBackgroundColorInt(accountType: AccountType): Int = when (accountType) {
    AccountType.FREE -> Color.GRAY
    AccountType.BUSINESS -> Color.BLUE
    AccountType.PREMIUM -> Color.YELLOW
}

これを更に発展させ、Option 2-b のように複数の値を 1 つのデータモデルにまとめてもよいでしょう。

Option 2-b

fun updateAccountTypeLayout(accountType: AccountType) {
    backgroundUiElement.color = accountType.backgroundColorInt
    accountTypeIconUiElement.image = imageProvider.get(accountType.iconId)
    accountTypeTextUiElement.text = accountType.typeText
}

// Case 1: Data model by an enum class
enum class AccountType(
    val backgroundColorInt: Int,
    val iconId: Int,
    val typeText: String
) {
   FREE(Color.GRAY, FREE_IMAGE_ID, "Free..."),
   ...
}

// Case 2: Data model by normal class
class AccountTypeLayoutModel(
    val backgroundColorInt: Int,
    val iconId: Int,
    val typeText: String
) {
    companion object {
        val FREE = AccountTypeLayoutModel(Color.GRAY, FREE_IMAGE_ID, "Free...")
        ...
    }
}

条件分岐については、 分岐が重複することを避けるよりも、分岐のスコープを小さくする方がよいことが多い です。似たような分岐があっても、when 式を使うなど、網羅性を保証することで更新漏れを防ぐことができます。特に、大規模なサービスやアプリケーションの場合、無理に分岐を一箇所にまとめようとしてしまうと、その分岐内に複数のモジュールやレイヤにまたがった処理が記述されてしまうことがあります。分岐のスコープを小さくすることで、各モジュールやレイヤに責任を閉じ込めやすくなります。

網羅性を保証できない場合

多くの動的型付けの言語など、使用している言語によっては、when 式のような網羅性を保証できる条件分岐がない場合もあります。Java も switch で enum の網羅性を保証できるようになったのは 14 以降であり、それまでは defaultnull のような無効な値を使ったり、デフォルト値にフォールバックしたり、IllegalArgumentException のような例外を使用する必要がありました。

@Nullable
Integer getBackgroundColorInt(@NonNull AccountType accountType) {
    switch(accountType) {
        case FREE:
            return Color.GRAY;
        ...
        default:
            return null;
}

このように、プロダクションコードで網羅性を保証できない場合は、網羅されていることを確認するテストコード を書くとよいでしょう。以下のテストでは、未知の accountType がないことを保証しています。新たな AccountType が追加されたにも関わらず getBackgroundColorInt の更新を忘れている場合は、assertNotNull が失敗します。

@Test
public void testBackgroundColorCompleteness() {
    for (AccountType type : AccountType.values()) {
        assertNotNull(
                testTarget.getBackgroundColorInt(type),
                "`getBackgroundColorInt` implementation is missing for the type " + type);
    }
}

一言まとめ

条件分岐の重複を避けるよりも、スコープを小さくすることを優先したほうがよいことが多い。

キーワード: conditional branch, completeness, extraction

次回は年明けから連載再開いたします。

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

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