こんにちは。コミュニケーションアプリ「LINE」のモバイルクライアントを開発している石川です。
この記事は、毎週木曜の定期連載 "Weekly Report" 共有の第 49 回です。 LINEヤフー社内には、高い開発生産性を維持するための Review Committee という活動があります。ここで集まった知見を、Weekly Report と称して毎週社内に共有しており、その一部を本ブログ上でも公開しています。(Weekly Report の詳細については、過去の記事一覧を参照してください)
虚実皮膜の依存
以下のような Screen という UI を実装することを仮定します。 Screen は 2 つのレイアウト FooLayout と BarLayout を持ち、それぞれいくつかの UI コンポーネントを持ちます。その中でも BazUiComponent という UI コンポーネントは、両方のレイアウトで共通して使われます。

BazUiComponent のロジックは FooLayout と BarLayout で同じになるため、ある開発者は以下のように BazUiComponent を集約として使うように実装しました。それぞれのレイアウトクラスは、BazUiComponent のインスタンスをコンストラクタパラメータとして保持します。逆に BazUiComponent は、どちらのレイアウトの UI 要素 かを識別するために、レイアウトと UI 要素のマッピングを uiElementBinding として保持します。
class Screen(...) {
private val bazUiComponent: BazUiComponent = BazUiComponent()
private val fooLayout: FooLayout = FooLayout(fooLayoutContainer, ..., bazUiComponent)
private val barLayout: BarLayout = BarLayout(barLayoutContainer, ..., bazUiComponent)
...
}
class FooLayout(
private val container: UiElementContainer,
...,
private val bazUiComponent: BazUiComponent
) {
init {
...
val bazUiElement = ...
bazUiComponent.initUiElements(this, bazUiElement)
}
...
fun updateBazText(text: String) {
bazUiComponent.setText(this, text)
}
}
class BarLayout(
private val container: UiElementContainer,
...,
private val bazUiComponent: BazUiComponent
) {
init {
...
val bazUiElement = ...
bazUiComponent.initUiElements(this, bazUiElement)
}
...
fun updateBazText(text: String) {
bazUiComponent.setText(this, text)
}
}
class BazUiComponent {
private val uiElementBinding: MutableMap<Any, UiElementHolder> = mutableMapOf()
fun initUiElements(key: Any, element: BazUiElement) {
uiElementBinding[key] = UiElementHolder(element)
...
}
fun setText(key: Any, text: String) {
uiElementBinding[key]?.setText(text)
}
...
private class UiElementHolder(private val element: BazUiElement) {
...
fun setText(text: String) { ... }
}
}
FooLayout 内の BazUiComponent で "Foo" と表示し、BarLayout 内 で "Bar" と表示するには、Screen で以下のようなコードを書きます。
fooLayout.updateBazText("Foo")
barLayout.updateBazText("Bar")
updateBazText により、最終的には fooLayout と barLayout 内の UI 要素が独立して更新されます。
このコードでなにか改善点はありますか?
虚への依存か、実への依存か
BazUiComponent について、FooLayout と BarLayout の間ではロジックだけを共有しているのですが、状態は共有していません。しかし、この 2 つのレイアウトで BazUiComponent のインスタンスを共有してしまっているのが問題です。ロジックの共通化のためだけであるならば、インスタンスまで共通にする必要はありません。
これにより、以下のような問題が引き起こされます。
BazUiComponentの実装が複雑になり、レイアウトとの循環依存が必要となる。UiElementHolderのインスタンスをBazUiComponentのinitで作成することができない。Screenがレイアウト内の詳細であるBazUiComponentについても知る必要がある。
このような問題は、型としての依存関係とオブジェクトとしての依存関係を混同したときに起きる可能性があります。
Screen、Foo/BarLayout、BazUiComponent 間の、型としての依存関係を図示すると以下のようになります。

FooLayout と BarLayout ではそれぞれ固有の文字列を表示するため、状態を共有する必要はありません。その場合は、型を共有すれば十分で、オブジェクトは共有すべきでなかったと言えます。オブジェクトとしての依存関係は、以下のようにすると良いでしょう。

実装例としては、以下のようになります。
class Screen(...) {
private val fooLayout: FooLayout = FooLayout(fooLayoutContainer)
private val barLayout: BarLayout = BarLayout(barLayoutContainer)
...
}
class FooLayout(private val container: UiElementContainer) {
private val bazUiComponent: BazUiComponent
init {
...
val bazUiElement = ...
bazUiComponent = BazUiComponent(bazUiElement)
}
...
fun updateBazText(text: String) {
bazUiComponent.setText(text)
}
}
class BarLayout(private val container: UiElementContainer) {
private val bazUiComponent: BazUiComponent
init {
...
val bazUiElement = ...
bazUiComponent = BazUiComponent(bazUiElement)
}
...
fun updateBazText(text: String) {
bazUiComponent.setText(text)
}
}
class BazUiComponent(private val bazUiElement: BazUiElement) {
fun setText(text: String) {
bazUiElement.setText(text)
}
...
}
「共有する」・「共通化する」・「抽出する」と言った場合、その対象が型なのかオブジェクトなのかを意識する必要があります。状態の共有が必要なら共通のオブジェクトを作る必要がありますが、ロジックの共通化なら型だけを共通化すれば十分です。ただし、ステートレス、もしくはシングルステートなオブジェクトならば、ロジックの共通化のためだけに共有しても問題は起きにくいです。
一言まとめ
型としての依存関係とオブジェクトとしての依存関係の違いを意識する。
キーワード: dependency, aggregation, object reference