Site cover image

fhhm’s blog

型パラメータによる共通化

型パラメータを使ってロジックなどを共通化する機会があった。備忘のためにまとめてみる。

前提

既存実装として下記がある。

LoginEmailは外部からのインスタンス化はさせずに、バリデーションを通してからインスタンス化させたいとする。

⚠️
簡単のために他の要件などを省略しているので、requireでは不十分とする
/** ログインに使用するメールアドレス */
case class LoginEmail private (value: Email)

object LoginEmail {

  sealed trait ValidationResult
  
  object ValidationResult {
    case class Failure(reason: String) extends ValidationResult
    case class Success(value: LoginEmail) extends ValidationResult
  }

  private def checkLength(input: String): Either[ValidationResult.Failure, String] = ???
  private def checkFormat(input: String): Either[ValidationResult.Failure, String] = ???

  def validate(input: String): ValidationResult = {
    val result = for {
      _ <- checkLength(input)
      _ <- checkFormat(input)
    } yield ValidationResult.Success(LoginEmail(input))
    result.fold(identity, identity)
  }
}

バリデーションロジックの共通化

次に、連絡用メールアドレスを表すContactEmailを追加する。ドメイン上はLoginEmailと別概念だが、バリデーション内容は同じである。

まずはロジックを共通化してみる。

object EmailValidation {

  trait ValidationResult
  
  object ValidationResult {
    case class Failure(reason: String) extends ValidationResult
  }
  
  def checkLength(input: String): Either[ValidationResult.Failure, String] = ???
  def checkFormat(input: String): Either[ValidationResult.Failure, String] = ???
}
/** ログインに使用するメールアドレス */
case class LoginEmail private (value: Email)

object LoginEmail {

  case class Success(value: LoginEmail) extends ValidationResult
  
  def validate(input: String): ValidationResult = {
    val result = for {
      _ <- EmailValidation.checkLength(input)
      _ <- EmailValidation.checkFormat(input)
    } yield ValidationResult.Success(LoginEmail(input))
    result.fold(identity, identity)
  }
}
/** 連絡用メールアドレス */
case class ContactEmail private (value: Email)

object ContactEmail {

  case class Success(value: ContactEmail) extends ValidationResult
  
  def validate(input: String): ValidationResult = {
    val result = for {
      _ <- EmailValidation.checkLength(input)
      _ <- EmailValidation.checkFormat(input)
    } yield ValidationResult.Success(ContactEmail(input))
    result.fold(identity, identity)
  }
}

バリデーションロジックは共通化できた。ただし、下記の問題が残る。

  • Successをバリューオブジェクトごとに定義する必要がある
  • ValidationResultsealedになっていないので、exhaustiveかチェックができない

型パラメータによる共通化

生成責務をFactory[T]として分離し、成功時にどの型を生成するかだけを外から与えるように実装してみた。

object EmailValidation {

  trait Factory[T] {
    def create(input: String): T
  }

  sealed trait ValidationResult[T]
  
  object ValidationResult {
    case class Failure[T](reason: String) extends ValidationResult[T]
    case class Success[T](value: T) extends ValidationResult[T]
  }
  
  private def checkLength[T](input: String): Either[ValidationResult.Failure[T], String] = ???
  private def checkFormat[T](input: String): Either[ValidationResult.Failure[T], String] = ???
  
  def validate[T](input: String)(using factory: Factory[T]): ValidationResult[T] = {
    val validated = for {
      _ <- checkLength[T](input)
      _ <- checkFormat[T](input)
    } yield input
    validated match {
      case Right(validated) => Success(factory.create(validated))
      case Left(failure)    => failure
    }
  }
}
/** ログインに使用するメールアドレス */
case class LoginEmail private (value: Email)

object LoginEmail {
  given factory: Factory[LoginEmail] = { input =>
    LoginEmail(input)
  }
}
/** 連絡用メールアドレス */
case class ContactEmail private (value: Email)

object ContactEmail {
  given factory: Factory[ContactEmail] = { input =>
    ContactEmail(input)
  }
}

バリデーションロジックや結果を表現するクラスが共通化できた。

ちなみに呼び出し側はこんな感じで、バリデーション成功時の型が静的に評価されるようになった。

EmailValidation.validate[LoginEmail] match {
  case ValidationResult.Success(value) => ???
  case ValidationResult.Failure(value) => ???
}

EmailValidation.validate[ContactEmail] match {
  case ValidationResult.Success(value) => ???
  case ValidationResult.Failure(value) => ???
}