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 43: Return of values

The original article was published on September 26, 2024.

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.

Return of values

The following registerNewUser function is designed to register a new user, as its name suggests. This function consists of two steps.

  1. Create a new user account using userAttributes
  2. Upload a profile image using userAttributes.profileImageUri
    fun registerNewUser(userAttributes: UserAttributes): RegistrationResult {
        val accountCreationRequestParams = AccountCreationRequestParams(
            userName = userAttributes.userName,
            accountType = userAttributes.accountType,
            referrer = userAttributes.referrer,
            creationTimestamp = currentTimeProvider()
        )
        val creationRequestResult = serviceApi.requestCreatingAccount(accountCreationRequestParams)
        if (creationRequestResult !is AccountCreationRequestResult.Success) {
            return RegistrationResult.ERROR
        }

        val profileImageBitmap = userAttributes.profileImageUri
            ?.let(resourceResolver::getSource)
            ?.let(ImageDecoder::decode)
        val isProfileImageUploaded = profileImageBitmap != null &&
                serviceApi.uploadProfileImage(
                    creationRequestResult.userId,
                    profileImageBitmap
                )

        return if (isProfileImageUploaded) {
            RegistrationResult.REGISTERED_WITH_CUSTOMIZED_IMAGE
        } else {
            RegistrationResult.REGISTERED_WITH_DEFAULT_IMAGE
        }
    }

This function is somewhat large, making it difficult to immediately see that there are two steps involved. To emphasize the two steps, a developer created the following auxiliary functions. The user account is created with requestCreatingAccount, and the profile image is uploaded with requestUploadingProfileImage.

    sealed interface AccountCreationResult {
        class Success(val userId: String): AccountCreationResult
        class Failure(val error: RegistrationResult): AccountCreationResult
    }

    fun registerNewUser(userAttributes: UserAttributes): RegistrationResult {
        val creationResult = requestCreatingAccount(userAttributes)
        if (creationResult is AccountCreationResult.Failure) {
            return creationResult.error
        }

        val userId = (creationResult as? AccountCreationResult.Success)?.userId
        return requestUploadingProfileImage(userId, userAttributes)
    }

    private fun requestCreatingAccount(userAttributes: UserAttributes): AccountCreationResult {
        val requestParams = AccountCreationRequestParams(
            userName = userAttributes.userName,
            accountType = userAttributes.accountType,
            referrer = userAttributes.referrer,
            creationTimestamp = currentTimeProvider()
        )
        val creationRequestResult = serviceApi.requestCreatingAccount(requestParams)
        return if (creationRequestResult is AccountCreationRequestResult.Success) {
            AccountCreationResult.Success(creationRequestResult.userId)
        } else {
            AccountCreationResult.Failure(RegistrationResult.ERROR)
        }
    }

    private fun requestUploadingProfileImage(
        userId: String?,
        userAttributes: UserAttributes,
    ): RegistrationResult {
        if (userId == null) {
            return RegistrationResult.REGISTERED_WITH_DEFAULT_IMAGE
        }

        val profileImageBitmap = userAttributes.profileImageUri
            ?.let(resourceResolver::getSource)
            ?.let(ImageDecoder::decode)
        val isProfileImageUploaded = profileImageBitmap != null &&
                serviceApi.uploadProfileImage(userId, profileImageBitmap)

        return if (isProfileImageUploaded) {
            RegistrationResult.REGISTERED_WITH_CUSTOMIZED_IMAGE
        } else {
            RegistrationResult.REGISTERED_WITH_DEFAULT_IMAGE
        }
    }

By doing this, it emphasizes that registerNewUser consists of two steps. However, it also introduces a point of confusion. Where is that?

Where is the king's responsibility?

The extracted code has a problem: "The final return value (RegistrationResult) is not clear just by reading registerNewUser." Depending on the conditions, either requestCreatingAccount or requestUploadingProfileImage determines the return value. This means that knowledge about the return value is dispersed across auxiliary functions. If you want to change the return value specification in the future, the scope of changes will be larger, and mistakes are more likely to occur.

In many cases, it is better not to disperse knowledge about the return value and to consolidate it in one place. In this case, only registerNewUser needs to know about RegistrationResult, and it is not a problem if the return types of other functions are different. In the following implementation, requestCreatingAccount and requestUploadingProfileImage use null as the error value, and registerNewUser converts it to RegistrationResult.

    fun registerNewUser(userAttributes: UserAttributes): RegistrationResult {
        val userId = requestCreatingAccount(userAttributes)
            ?: return RegistrationResult.ERROR
        
        val hasProfileImage = requestUploadingProfileImage(userId, userAttributes.profileImageUri)
        return if (hasProfileImage) {
            RegistrationResult.REGISTERED_WITH_CUSTOMIZED_IMAGE
        } else {
            RegistrationResult.REGISTERED_WITH_DEFAULT_IMAGE
        }
    }

    /**
     * Sends a request to create a new account with ...
     * This returns the user ID if a new user was successfully created, otherwise returns null.
     */
    private fun requestCreatingAccount(userAttributes: UserAttributes): String? {
        val requestParams = AccountCreationRequestParams(
            userName = userAttributes.userName,
            accountType = userAttributes.accountType,
            referrer = userAttributes.referrer,
            creationTimestamp = currentTimeProvider()
        )
        val creationRequestResult = serviceApi.requestCreatingAccount(requestParams)
        return (creationRequestResult as? AccountCreationRequestResult.Success)?.userId
    }

    /**
     * Uploads  the profile image for the new account with ...
     * Then, returns true if the upload finishes successfully; false otherwise.
     */
    private fun requestUploadingProfileImage(
        userId: String,
        profileImageUri: Uri?,
    ): Boolean {
        val profileImageBitmap = profileImageUri
            ?.let(resourceResolver::getSource)
            ?.let(ImageDecoder::decode)
        return profileImageBitmap != null && serviceApi.uploadProfileImage(userId, profileImageBitmap)
    }

By doing this, you can clarify the flow of registerNewUser and understand the return value without reading other functions. Even if you want to change the specification of RegistrationResult, the direct impact on requestCreatingAccount or requestUploadingProfileImage is likely to be minimal.

In a nutshell

Sometimes it is better to consolidate the code that determines the return value in one place.

Keywords: return value, function responsibility, specification knowledge

List of articles on techniques for improving code quality