LoginSignup
10
0

More than 1 year has passed since last update.

Scala + Play Frameworkでリクエストを値オブジェクトにする

Last updated at Posted at 2022-12-11

この記事は ウェブクルー Advent Calendar 2022 12日目の記事です。
昨日は @wc-fukuda さんの「 個人的にブラボーだったUI/UX3選 」でした。

はじめに

今年度はScala + Play Frameworkでコードを書くことが多く、値オブジェクトを積極的に導入しました。
この構成でリクエストを値オブジェクトに変換する実装が分からず詰まった経験があり、記事のネタとしてちょうどいいと思ったのでこの記事を書きました。

値オブジェクトとは

一言でいうと、業務で扱う最小単位をclassで表現したものです。
値オブジェクトはプリミティブ型をラッピングして個別に意味のある型として表現します。(値オブジェクトの詳細については興味があったらぜひ調べてみてください。)

例えば、アプリを作ってると「ID」は複数存在すると思います。ユーザーID、リクエストID、会社IDなど。これらはScalaだと基本的にLong型で扱うと思いますが、それぞれのIDをUserId型、RequestId型、CompanyId型で扱うという感じです。こうすることにより、変数の代入ミスを防げたりコードリーディングが簡単になったりします。

Scalaで値オブジェクトを実装する

Scalaの値オブジェクトもプリミティブ型のラッピングをして表現してます。AnyValを利用することでオブジェクトのメモリ割り当てを防ぎ、実行時には内部のプリミティブ型として扱ってくれる優れ機能です。

final case class UserId(value: Long) extends AnyVal

リクエストを値オブジェクトに変換して受け取る

今回はREST APIのリクエストを想定してます。
パス、クエリストリング、JSONから値を受け取るときの実装について書いてます。

パス

Play FrameworkのPathBindableを使うことで実装できます。
/users/3から取得することを想定してます。

import play.api.mvc.PathBindable

final case class UserId(value: Long) extends AnyVal

object UserId {
  implicit def pathBinder(implicit longBinder: PathBindable[Long]) = new PathBindable[UserId] {
    override def bind(key: String, value: String): Either[String, UserId] = {
      for {
        userId <- longBinder.bind(key, value)
      } yield UserId(userId)
    }
    override def unbind(key: String, userId: UserId): String = {
      userId.value.toString
    }
  }
}

クエリストリング

Play FrameworkのQueryStringBindableを使うことで実装できます。このドキュメントの実装だとクエリストリングが付与されていない場合エラーになってしまうので、少し改造してます。

/users/?from=1&to=10から取得することを想定してます。

import play.api.mvc.QueryStringBindable

final case class From(value: Long) extends AnyVal
final case class To(value: Long) extends AnyVal

case class UserRange(
  fromOpt: Option[From],
  toOpt: Option[To]
)

object UserRange {
  implicit def queryStringBindable(implicit longBinder: QueryStringBindable[Long]) = new QueryStringBindable[UserRange] {
    override def bind(key: String, params: Map[String, Seq[String]]): Option[Either[String, UserRange]] = {
      val from = longBinder.bind("from", params)
      val to = longBinder.bind("to", params)

      val result = (from, to) match {
        case (Some(Right(b)), Some(Right(t))) => Right(UserRange(Some(From(b)), Some(To(t))))
        case (Some(Right(b)), None) => Right(UserRange(Some(From(b)), None))
        case (None, Some(Right(t))) => Right(UserRange(None, Some(To(t))))
        case (_, _) => Right(UserRange(None, None))
      }

      Option(result)
    }

    override def unbind(key: String, userRange: UserRange): String = {
      longBinder.unbind("from", userRange.fromOpt.getOrElse(From(-1)).value) +
        "&" + longBinder.unbind("to", userRange.toOpt.getOrElse(To(-1)).value)
    }
  }
}

JSON

circeを利用して実装できます。以下ユーザーに関するJSONを想定してます。

{
	"id": 1,
	"name": "Scala太郎"
}
import io.circe.{ Decoder, Encoder, HCursor, Json }

final case class UserId(value: Long) extends AnyVal
final case class UserName(value: String) extends AnyVal

case class User(
  id: UserId,
  name: UserName
)

object User {
  implicit val decoder: Decoder[User] = new Decoder[User] {
    final def apply(c: HCursor): Decoder.Result[User] = 
      for {
        id <- c.downField("id").as[Long].map(UserId(_))
        name <- c.downField("name").as[String].map(UserName(_))
      } yield {
        new User(id, name)
      }
  }
}

circeは他にもいろんな実装ができるのでぜひドキュメントを覗いてみてください。

おわりに

皆さんも積極的に値オブジェクトを導入して型に強く守られましょう!!

明日は、@kentaro-arai さんです。
よろしくお願いします。

ウェブクルーでは一緒に働いていただける方を随時募集しております。
お気軽にエントリーくださいませ。
https://www.webcrew.co.jp/recruit/

10
0
0

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
10
0