LY Corporation Tech Blog

We are promoting the technology and development culture that supports the services of LY Corporation and LY Corporation Group (LINE Plus, LINE Taiwan and LINE Vietnam).

This post is also available in the following languages. Japanese

Improving code quality - Session 49: Dependency between reality and illusion

Hello, I'm Munetoshi Ishikawa, a mobile client developer for the LINE messaging app.

This article is the latest installment of our weekly series "Improving code quality". For more information about the Weekly Report, please see the first article.

Dependency between reality and illusion

Let's assume the implementation of a UI called Screen as follows. Screen has two layouts, FooLayout and BarLayout, each containing several UI components. Among them, a UI component called BazUiComponent is used in both layouts.

Structure of Screen. FooLayout and BarLayout each have a BazUiComponent

Since the logic of BazUiComponent is the same in both FooLayout and BarLayout, a developer implemented it to be used as an aggregate as follows. Each layout class holds an instance of BazUiComponent as a constructor parameter. Conversely, BazUiComponent holds a mapping of layouts and UI elements as uiElementBinding to identify which layout's UI element it is.

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) { ... }
    }
}

To display "Foo" in BazUiComponent within FooLayout and "Bar" within BarLayout, you would write the following code in Screen.

fooLayout.updateBazText("Foo")
barLayout.updateBazText("Bar")

Through updateBazText, the UI elements within fooLayout and barLayout are independently updated.

Are there any improvements to be made to this code?

Dependency on illusion or reality

Regarding BazUiComponent, only the logic is shared between FooLayout and BarLayout, not the state. However, the problem is that the instance of BazUiComponent is shared between these two layouts. If it's only for logic sharing, there's no need to share the instance.

This leads to the following issues:

  • The implementation of BazUiComponent becomes complex, requiring circular dependencies with the layouts.
  • Instances of UiElementHolder cannot be created in the init of BazUiComponent.
  • Screen needs to know about BazUiComponent, which is a detail within the layout.

Such issues can arise when confusing dependency as a type with dependency as an object.

The dependency as a type between Screen, Foo/BarLayout, and BazUiComponent can be illustrated as follows.

Diagram of class dependencies. FooLayout and BarLayout both depend on BazUiComponent

Since FooLayout and BarLayout display unique strings, there is no need to share the state. In this case, sharing the type is sufficient, and the object should not have been shared. The dependency as an object should be as follows.

Diagram of instance dependencies. FooLayout and BarLayout each depend on different BazUiComponent instances

An implementation example would be as follows.

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)
    }

    ...
}

When saying "share", "commonize", or "extract", it is necessary to be aware of whether the target is a type or an object. If state sharing is needed, a common object should be created, but for logic sharing, sharing the type is sufficient. However, if the object is stateless or single-state, sharing it solely for logic commonization is less likely to cause problems.

In a nutshell

Be aware of the difference between dependency as a type and dependency as an object.

Keywords: dependency, aggregation, object reference

List of articles on techniques for improving code quality