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.
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 theinit
ofBazUiComponent
. Screen
needs to know aboutBazUiComponent
, 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.
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.
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