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 50: The only truth

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.

The only truth

Suppose a class called UserModel is defined as the data model for user accounts.

class UserModel(
    val userId: UserId
    val accountType: AccountType,
    val displayName: String,
    val profileImageUri: Uri,
    val onlineStatus: OnlineStatus,
    val moodMessage: String
)

Among these properties, accountType, displayName, and profileImageUri are widely used within the application, and the timing of their value updates is limited. On the other hand, onlineStatus and moodMessage are used only in specific features (e.g., user detail screen) and are frequently updated.

When there is a variation in the lifecycle of values and the scope of code that depends on those values, separating the data model can lead to robust and clear code. In this case, the direction of dependencies should be as follows:

  • Feature-specific code depends on generic code
  • Values with a short lifecycle depend on values with a long lifecycle

(For more on the direction of dependencies, refer to https://speakerdeck.com/munetoshi/code-readability-session-7-ver-2-ja?slide=4 slide=35 and the "6-3 Dependency Direction" section in Guidelines for Readable Code.)

In this case, it would be good to separate the three properties accountType, displayName, profileImageUri from the two properties onlineStatus, moodMessage, with the latter depending on the former.

Therefore, a developer applied "aggregation" and rewrote UserModel as follows. However, there are improvements to be made in this code. What are they?

class UserState(
    val userId: UserId
    val onlineStatus: OnlineStatus,
    val moodMessage: String,
    val accountModel: AccountModel
)

class AccountModel(
    val userId: UserId,
    val accountType: AccountType,
    val displayName: String,
    val profileImageUri: Uri
)

False truth

The issue with this code is that UserState holds an AccountModel, yet both UserState and AccountModel have their own userId. This configuration allows for the easy creation of "invalid" instances, as shown below:

UserState(
    UserId("userA")
    ...,
    ...,
    AccountModel(
        UserId("userB"),
        ...
    )
)

When applying aggregation or composition, it is preferable to avoid value duplication. If UserState holds an instance of AccountModel, UserState should not have an externally provided userId but should use AccountModel.userId. You can simply remove UserState.userId or make UserState.userId determined by AccountModel.userId.

// Option 1: Delete `userId`
class UserState(
    val onlineStatus: OnlineStatus,
    val moodMessage: String,
    val accountModel: AccountModel
)

// Option 2: Decide `userId` by `AccountModel`
class UserState(
    val onlineStatus: OnlineStatus,
    val moodMessage: String,
    val accountModel: AccountModel
) {
    val userId: UserId get() = accountModel.userId
    // or, `val userId: UserId = accountModel.userId` if it's immutable.
}

Such value duplication can occur not only due to dependency and non-dependency relationships but also between properties. The possibility of invalid value combinations can be eliminated with factory functions or failable initializers. The factory function of in the following ProfileModel creates an instance by combining AccountModel and UserGroupRelation, but returns null if the userId does not match.

class AccountModel(
    val userId: UserId,
    val displayName: String
)

class UserGroupRelation(
    val userId: UserId,
    val groups: Set<GroupId>
)

class ProfileModel private constructor(
    val userId: UserId,
    val displayName: String,
    val groups: Set<GroupId>
) {

    companion object {
        fun of(
            accountModel: AccountModel,
            userGroupRelation: UserGroupRelation
        ): ProfileModel? {
            if (accountModel.userId != userGroupRelation.userId) {
                return null
            }

            return ProfileModel(
                accountModel.userId,
                accountModel.displayName
                userGroupRelation.groups
            )
        }
    }
}

In a nutshell

When applying aggregation or composition, be cautious of invalid states due to value duplication.

Keywords: aggregation, value domain, property duplication

List of articles on techniques for improving code quality