LoginSignup
35
37

More than 5 years have passed since last update.

ScalaでどのタイミングでNoneが生じたか返すコードをEitherを使ってスッキリ書くリファクタリング

Last updated at Posted at 2015-02-10

Scalaでリレーショナルデータベースを扱う場合、関連をたどっていく中でどのタイミングで情報が取得できなかったのかを返さねばならないことがあります。

Noneを盲目的に処理するのであれば、flatMapやfor式をつかえば畳み込んでスッキリかけるのですが、関連を取得していくなかでどのタイミングでNoneが取得されてしまったのか返したい場合にはそうは行かず、結局match caseの深いネストになってしまいます。

例を挙げます。

ユーザーとアドレスがそれぞれデータベースに格納されており、ユーザーIDを利用してそのユーザーを検索し、ユーザーが持つアドレスIDでアドレスを検索し、さらにその郵便番号を取得するような場合を考えます。

失敗結果としては

  • ユーザーがみつからない
  • ユーザーがアドレスを持っていない
  • アドレスがみつからない
  • アドレスが郵便番号を持っていない

という4つの失敗パターンがあり、それらを結果オブジェクトとして返さなくてはなりません。

以下のようなコードになります。

MainBefore.scala
object MainBefore {

  case class Address(id: Int, name: String, postalCode: Option[String])
  case class User(id: Int, name: String, addressId: Option[Int])

  val userDatabase: Map[Int, User] = Map (
    1 -> User(1, "太郎", Some(1)),
    2 -> User(2, "二郎", Some(2)),
    3 -> User(3, "プー太郎", None)
  )

  val addressDatabase: Map[Int, Address] = Map (
    1 -> Address(1, "渋谷", Some("150-0002")),
    2 -> Address(2, "国際宇宙ステーション", None)
  )

  sealed abstract class PostalCodeResult
  case class Success(postalCode: String) extends PostalCodeResult
  abstract class Failure extends PostalCodeResult
  case class UserNotFound() extends Failure
  case class UserNotHasAddress() extends Failure
  case class AddressNotFound() extends Failure
  case class AddressNotHasPostalCode() extends Failure

  // どこでNoneが生じたか取得しようとするとfor式がつかえず地獄のようなネストになる
  def getPostalCodeResult(userId: Int): PostalCodeResult = {
    findUser(userId) match {
      case Some(user) =>
        user.addressId match {
          case Some(addressId) =>
            findAddress(addressId) match {
              case Some(address) =>
                address.postalCode match {
                  case Some(postalCode) => Success(postalCode)
                  case None => AddressNotHasPostalCode()
                }
              case None => AddressNotFound()
            }
          case None => UserNotHasAddress()
        }
      case None => UserNotFound()
    }
  }

  def findUser(userId: Int): Option[User] = {
    userDatabase.get(userId)
  }

  def findAddress(addressId: Int): Option[Address] = {
    addressDatabase.get(addressId)
  }

  def main(args: Array[String]): Unit = {
    Console.println(getPostalCodeResult(1)) // Success(150-0002)
    Console.println(getPostalCodeResult(2)) // AddressNotHasPostalCode()
    Console.println(getPostalCodeResult(3)) // UserNotHasAddress()
    Console.println(getPostalCodeResult(4)) // UserNotFound()
  }
}

getPostalCodeResultが鬼のようなmatch caseのネストになっていることがわかります。このような可読性の低いコードを、Eitherを使って書きなおすことができます。

以下のように全てのfindメソッドをEitherでFailureをLeftに、正常取得できた場合の値の型をRightにして書き直します。

findの各段階でFailureオブジェクトに引き換えるという動きをさせるわけです。

リファクタリングした結果は以下のようになります。

MainRefactored.scala
object MainRefactored {

  case class Address(id: Int, name: String, postalCode: Option[String])
  case class User(id: Int, name: String, addressId: Option[Int])

  val userDatabase: Map[Int, User] = Map (
    1 -> User(1, "太郎", Some(1)),
    2 -> User(2, "二郎", Some(2)),
    3 -> User(3, "プー太郎", None)
  )

  val addressDatabase: Map[Int, Address] = Map (
    1 -> Address(1, "渋谷", Some("150-0002")),
    2 -> Address(2, "国際宇宙ステーション", None)
  )

  sealed abstract class PostalCodeResult
  case class Success(postalCode: String) extends PostalCodeResult
  abstract class Failure extends PostalCodeResult
  case class UserNotFound() extends Failure
  case class UserNotHasAddress() extends Failure
  case class AddressNotFound() extends Failure
  case class AddressNotHasPostalCode() extends Failure

  // 本質的に何をしているかわかりやすくリファクタリング
  def getPostalCodeResult(userId: Int): PostalCodeResult = {
    (for {
      user <- findUser(userId).right
      address <- findAddress(user).right
      postalCode <- findPostalCode(address).right
    } yield Success(postalCode)).merge
  }

  def findUser(userId: Int): Either[Failure, User] = {
    userDatabase.get(userId).toRight(UserNotFound())
  }

  def findAddress(user: User): Either[Failure, Address] = {
    for {
      addressId <- user.addressId.toRight(UserNotHasAddress()).right
      address <- addressDatabase.get(addressId).toRight(AddressNotFound()).right
    } yield address
  }

  def findPostalCode(address: Address): Either[Failure, String] = {
    address.postalCode.toRight(AddressNotHasPostalCode())
  }

  def main(args: Array[String]): Unit = {
    Console.println(getPostalCodeResult(1)) // Success(150-0002)
    Console.println(getPostalCodeResult(2)) // AddressNotHasPostalCode()
    Console.println(getPostalCodeResult(3)) // UserNotHasAddress()
    Console.println(getPostalCodeResult(4)) // UserNotFound()
  }
}

以上のようになり、

  def getPostalCodeResult(userId: Int): PostalCodeResult = {
    (for {
      user <- findUser(userId).right
      address <- findAddress(user).right
      postalCode <- findPostalCode(address).right
    } yield Success(postalCode)).merge
  }

getPostalCodeResultが本質的に何をしているのかが非常にわかりやすいコードとなりました。何をしているかというと、Eitherではfor式を直接つかえないので.rightというメソッドで、RightProjectionという型にして、for式が利用できる形に変換しています。そのあと、margeメソッドにより中身を畳み込んで取得しています。

@xuwei_kさんからの指摘を頂いたので、リファクタ後もmatch caseで切り出したメソッド内でEitherにしていた部分をtoRightを使って表現するように修正しました。

なおこの内容は、
https://gist.github.com/rirakkumya/2382341
で元々議論されていたものをわかりやすく書きなおしたものになります。
ぜひ今後このようなリファクタリングにチャレンジしてみてください。

コード自体は、 https://github.com/sifue/right_projection にありますので、sbt runなどして動きを試してみてください。

35
37
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
35
37