LINEヤフー Tech Blog

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

コード品質向上のテクニック:第49回 虚実皮膜の依存

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

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

虚実皮膜の依存

以下のような Screen という UI を実装することを仮定します。 Screen は 2 つのレイアウト FooLayoutBarLayout を持ち、それぞれいくつかの UI コンポーネントを持ちます。その中でも BazUiComponent という UI コンポーネントは、両方のレイアウトで共通して使われます。

Screenの構造。FooLayoutとBarLayoutがそれぞれBazUiComponentを持っている

BazUiComponent のロジックは FooLayoutBarLayout で同じになるため、ある開発者は以下のように 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 により、最終的には fooLayoutbarLayout 内の UI 要素が独立して更新されます。

このコードでなにか改善点はありますか?

虚への依存か、実への依存か

BazUiComponent について、FooLayoutBarLayout の間ではロジックだけを共有しているのですが、状態は共有していません。しかし、この 2 つのレイアウトで BazUiComponent のインスタンスを共有してしまっているのが問題です。ロジックの共通化のためだけであるならば、インスタンスまで共通にする必要はありません。

これにより、以下のような問題が引き起こされます。

  • BazUiComponent の実装が複雑になり、レイアウトとの循環依存が必要となる。
  • UiElementHolder のインスタンスを BazUiComponentinit で作成することができない。
  • Screen がレイアウト内の詳細である BazUiComponent についても知る必要がある。

このような問題は、型としての依存関係とオブジェクトとしての依存関係を混同したときに起きる可能性があります。

ScreenFoo/BarLayoutBazUiComponent 間の、型としての依存関係を図示すると以下のようになります。

クラスの依存関係の図。FooLayoutとBarLayoutが共通してBazUiComponentに依存している

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

インスタンスの依存関係の図。FooLayoutとBarLayoutがそれぞれ異なるBazUiComponentインスタンスに依存している

実装例としては、以下のようになります。

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

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

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