Help us understand the problem. What is going on with this article?

何となくScalaでミドルウェアを書いてみる

はじめに

所謂UseCase層・アプリケーション層・サービス層のロギングに統一されたミドルウェア的なものでも置いて楽できないかと思ったので書いてみた。

何のこともないScalaなコードなので解説は端折り気味にします。

環境

  • scala 2.13

ミドルウェア

middlewares.scala
import scala.concurrent.{ExecutionContext, Future}

trait Middleware {

  def run[A](cb: () => Future[A])()(implicit ec: ExecutionContext): Future[A]
}

object LoggerMiddleware1 extends Middleware {

  def run[A](
      cb: () => Future[A]
  )()(implicit ec: ExecutionContext): Future[A] = {
    println("Logger1 Start")

    cb().map { v =>
      println(s"Logger1 End: $v")
      v
    }
  }
}

object LoggerMiddleware2 extends Middleware {

  def run[A](
      cb: () => Future[A]
  )()(implicit ec: ExecutionContext): Future[A] = {
    println("Logger2 Start")

    cb().map { v =>
      println(s"Logger2 End: $v")
      v
    }
  }
}

最初は無依存(同期的に)で書きたかったけど思いつかなかったのでFutureに依存することにした。
こっからモナドチックに振りたかったらmonix.Taskなりcats.effect.IOなり突っ込めばいいと思う。

ミドルウェア実行用トレイト

WithMiddleware.scala
import scala.concurrent.{ExecutionContext, Future}

trait WithMiddleware {

  @scala.annotation.tailrec
  private def recursiveRun[A](
      runners: List[Middleware],
      cb: () => Future[A]
  )(implicit ec: ExecutionContext): Future[A] =
    runners match {
      case Nil =>
        cb()

      case head :: tail =>
        recursiveRun(tail, head.run(cb))
    }

  def middlewares: List[Middleware]

  protected def withMiddleware[A](cb: () => Future[A])(
      implicit ec: ExecutionContext
  ): Future[A] = recursiveRun(middlewares, cb)
}

withMiddlewareで登録されたミドルウェア(middlewares)を再帰で走らせます。

用例とテスト

UserApplication.scala
import scala.concurrent.{ExecutionContext, Future}

class UserApplication extends WithMiddleware {

  implicit val executionContext: ExecutionContext = ExecutionContext.global

  override val middlewares: List[Middleware] =
    List(LoggerMiddleware1, LoggerMiddleware2)

  def create(name: String): Future[Int] = withMiddleware { () =>
    println(s"Inside Application: $name")
    Future(1)
  }
}
UserApplicationSpec.scala
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AsyncWordSpec

class UserApplicationSpec extends AsyncWordSpec with Matchers {

  private val userApplication = new UserApplication()

  "user application" should {
    "create user" in {
      userApplication.create("Same").map { v =>
        v shouldBe 1
      }
    }
  }
}

テストを実行すると...

Logger2 Start
Logger1 Start
Inside Application: Same
Logger1 End: 1
Logger2 End: 1

とプリントされるはずです。

おわりに

久しぶりに標準Future使ってみたけどAPIがシンプルでこれはこれで悪くないなぁと思った。

追記

何となく猫版

middlewares.scala
import cats.effect.IO
import cats.implicits._

final case class Runner[F[_], A](run: () => F[A])

abstract class Middleware[F[_]] {

  def runner[A](runner: Runner[F, A]): Runner[F, A]
}

object LoggerMiddlewareIO_1 extends Middleware[IO] {

  override def runner[A](runner: Runner[IO, A]): Runner[IO, A] = Runner {
    println("Logger1 Start")

    runner.run.map { v =>
      println("Logger1 End")

      v
    }
  }
}

object LoggerMiddlewareIO_2 extends Middleware[IO] {

  override def runner[A](runner: Runner[IO, A]): Runner[IO, A] = Runner {
    println("Logger2 Start")

    runner.run.map { v =>
      println("Logger2 End")

      v
    }
  }
}
WithMiddleware.scala
trait WithMiddleware[F[_]] {

  @scala.annotation.tailrec
  private def recursiveRun[A](
      ms: List[Middleware[F]],
      runner: Runner[F, A]
  ): F[A] =
    ms match {
      case Nil =>
        runner.run()

      case head :: tail =>
        recursiveRun(tail, head.runner(runner))
    }

  def middlewares: List[Middleware[F]]

  protected def withMiddleware[A](cb: () => F[A]): F[A] =
    recursiveRun(middlewares, Runner(cb))
}
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした