LINEヤフー Tech Blog

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

コード品質向上のテクニック: 第 10 回(起きば浮世の壱を見ん)

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

この記事は、毎週木曜の定期連載 "Weekly Report" 共有の第 10 回です。Weekly Report については、第 1 回の記事を参照してください。

起きば浮世の壱を見ん

ソートされた Item のリストを表示する UI を実装することを想定しましょう。この Item リストの上部には、Item の総数を表示するヘッダーがあるとします。以下は、リストの表示例です。

Items: 3
---------
 <item1>
---------
 <item2>
---------
 <item3>

さらに、リストと総数表示には次のような仕様があるとします。

  • リストで表示できる Item 数は先頭 100 個まで
  • 100 個を超える Item がある場合は、個数表示は "100+" となる

総数が 100 を超える場合の表示形式は以下のようになります。

Items: 100+
---------
 <item1>
---------
 <item2>
---------
... (省略) ...
---------
 <item100>

この仕様を実現するために、モデルクラス Item/SortedItems と Repository を次のように定義しました。

// Model classes

const val ITEM_LIST_MAX_COUNT = 100

class Item(...)
class StoredItems(val items: List<Item>)
// In the repository layer
class ItemRepository(...) {
    suspend fun getItemList(): StoredItems {
        // `+ 1` is for showing "+" on the UI.
        val items = itemDao.selectItems(ITEM_QUERY_MAX_COUNT + 1)
        return StoredItems(items)
    }
}

一方、UI 表示側で総数のテキストを決めるロジックでは、要素数が ITEM_LIST_MAX_COUNT を超えるかどうかで分岐しています。

val itemCount = storedItems.items.size
countTextView.text =
    if (itemCount <= ITEM_LIST_MAX_COUNT) itemCount.toString() else "$ITEM_LIST_MAX_COUNT+"

itemListAdapter.items = storedItems.items.take(ITEM_LIST_MAX_COUNT)

このコードに問題点はあるでしょうか。

+ 1 の距離を保つ

このコードでは、UI とレポジトリレイヤの間で「暗黙的な知識」を共有しているため、仕様を変更したときにバグが生じやすい構造になっています。コードを複数のレイヤやコンポーネントに分ける場合、「どのレイヤ/コンポーネントが何の知識を持つべきか」 を考慮して設計しましょう。

レポジトリレイヤは UI の詳細について知るべきないのですが、ItemRepository に書かれた // `+ 1` is for showing "+" on the UI. というコメントはそれに反していることを示しています。逆に UI 側も、レポジトリレイヤの詳細に依存してます。UI 側の countTextView.text を求めるロジックが、レポジトリレイヤの「ITEM_LIST_MAX_COUNT を超える Item をもつ場合は ITEM_LIST_MAX_COUNT より大きいリストを返す」という挙動に依存しているからです。

この問題を解決するためには、以下のように「リストに入りきれていない Item があるか」を示すプロパティを StoredItems に追加するとよいでしょう。

class StoredItems(val items: List<Item>, val hasMoreItems: Boolean)

このようにすることで、レポジトリレイヤは UI の詳細を意識することなく、モデルクラスのインスタンスの作成に焦点を当てることができます。

private const val ITEM_LIST_MAX_COUNT = 100

class ItemRepository(...) {
    suspend fun getItemList(): StoredItems {
        // `+ 1` is for deciding `hasMoreItems`.
        val items = itemDao.selectItems(ITEM_QUERY_MAX_COUNT + 1)
        return StoredItems(
            items.take(ITEM_QUERY_MAX_COUNT),
            items.size > ITEM_QUERY_MAX_COUNT
        )
    }
}

さらに UI のレイヤは、ITEM_QUERY_MAX_COUNT の知識を持たなくて良くなります。ついて知る必要がなくなります。

val countText = storedItems.items.size.toString()
countTextView.text = if (storedItems.hasMoreItems) "$countText+" else countText

itemListAdapter.items = storedItems.items

ええと、でも ITEM_LIST_MAX_COUNT はレポジトリレイヤでいいの?

ITEM_LIST_MAX_COUNT の知識をどこで持つかには、いくつかの選択肢があります。例えば、ビジネスロジックのレイヤを設け、そこに ITEM_LIST_MAX_COUNT を定義しても良いでしょう。ビジネスロジックのレイヤは、採用するアーキテクチャによってドメイン、サービス、ユースケースなど様々な形態がありえます。

もし、ビジネスロジックのレイヤを設けるのがオーバーキルである場合は、モデルクラスにこの知識を持たせるのも一つの選択肢です。

class StoredItems(storedItemList: List<Items>) {
    val items: List<Item> = storedItemList.take(MAX_ITEM_COUNT)
    val hasMoreItems: Boolean = storedItemList.size > MAX_ITEM_COUNT

    companion object {
        private const val MAX_ITEM_COUNT = 100
        const val ITEM_COUNT_FOR_QUERY = MAX_ITEM_COUNT + 1
    }
}

ただし、この方法では「アルゴリズム -> データ構造」という依存関係の方向が曖昧になるため注意が必要です。特に、機能固有のロジックを汎用的に使われるデータモデルに含めないように気をつける必要があります。


一言まとめ: 別のレイヤの詳細な動作に、暗黙的に依存するコードは避けるべき。

キーワード: implicit dependency, module structure, responsibility