こんにちは。コミュニケーションアプリ「LINE」のモバイルクライアントを開発している石川です。
この記事は、毎週木曜の定期連載 "Weekly Report" 共有の第 43 回です。 LINEヤフー社内には、高い開発生産性を維持するための Review Committee という活動があります。ここで集まった知見を、Weekly Report と称して毎週社内に共有しており、その一部を本ブログ上でも公開しています。(Weekly Report の詳細については、過去の記事一覧を参照してください)
値の帰還
以下の registerNewUser
は、その名の通り新しいユーザを登録するための関数です。この関数は 、2 つのステップで構成されています。
userAttributes
を使って、新しいユーザアカウントを作成する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
を読んだだけでは分からない」という問題を抱えています。条件によって、requestCreatingAccount
と requestUploadingProfileImage
のどちらが戻り値を決めるかが変わります。これは、戻り値に関する知識が補助的な関数に分散されてしまっているとも言えるでしょう。今後、戻り値の仕様を変えたくなった場合に、変更する範囲が大きくなり、ミスも発生しやすくなります。
多くの場合 戻り値に関する知識は分散させず、一ヶ所にまとめたほうが良い でしょう。今回の場合、RegistrationResult
を知っているのは registerNewUser
だけで十分で、他の関数の戻り値型は異なっても問題ありません。以下の実装では、requestCreatingAccount
と requestUploadingProfileImage
は null
をエラーの値として使っており、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
の仕様を変える場合でも、直接は requestCreatingAccount
や requestUploadingProfileImage
が影響を受ける可能性も低くなります。
一言まとめ
戻り値を決めるコードは、一ヶ所にまとめた方が良いときもある。
キーワード: return value
, function responsibility
, specification knowledge