こんにちは。コミュニケーションアプリ「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