はじめに
所謂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))
}