LoginSignup
0

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-02-20

はじめに

所謂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))
}

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
0