LINEヤフー Tech Blog

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

コード品質向上のテクニック:第43回 値の帰還

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

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

値の帰還

以下の registerNewUser は、その名の通り新しいユーザを登録するための関数です。この関数は、2 つのステップで構成されています。

  1. userAttributes を使って、新しいユーザアカウントを作成する
  2. 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
        }
    }

この関数は少し大きいため、先述の 2 つのステップがあることが一目では分かりにくいです。そこで、ステップが 2 つあることを強調するために、ある開発者が以下のように補助的な関数を作成しました。requestCreatingAccount でユーザアカウントを作成し、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
        }
    }

このようにすることで、registerNewUser が 2 つのステップで構成されることを強調できます。しかしながら、逆に分かりにくくなってしまっている点もあります。それはどこでしょうか?

「王」の責任はどこか

抽出後のコードは、「最終的な戻り値 (RegistrationResult) が registerNewUser を読んだだけでは分からない」という問題を抱えています。条件によって、requestCreatingAccountrequestUploadingProfileImage のどちらが戻り値を決めるかが変わります。これは、戻り値に関する知識が補助的な関数に分散されてしまっているとも言えるでしょう。今後、戻り値の仕様を変えたくなった場合に、変更する範囲が大きくなり、ミスも発生しやすくなります。

多くの場合 戻り値に関する知識は分散させず、一ヶ所にまとめたほうが良い でしょう。今回の場合、RegistrationResult を知っているのは registerNewUser だけで十分で、他の関数の戻り値型は異なっても問題ありません。以下の実装では、requestCreatingAccountrequestUploadingProfileImagenull をエラーの値として使っており、registerNewUser でそれを 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)
    }

このようにすることで、registerNewUser の流れを明確にしつつ、他の関数を読まずとも戻り値について理解可能なコードにできます。RegistrationResult の仕様を変える場合でも、直接は requestCreatingAccountrequestUploadingProfileImage が影響を受ける可能性も低くなります。

一言まとめ

戻り値を決めるコードは、一ヶ所にまとめた方が良いときもある。

キーワード: return value, function responsibility, specification knowledge

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

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