LoginSignup
11
0

More than 1 year has passed since last update.

ScalaのバッチプログラムにZIOを導入した話

Last updated at Posted at 2022-12-15

この記事は ウェブクルー Advent Calendar 2022 16日目の記事です。
昨日は @wc_tanaka_rin さんの「 Looker Studio(旧Data Portal)でBigQueryからデータ取るときに注意するポイント 」でした。

今年後半は外部の経理担当者用Webサービスと社内の売り上げ管理システムとのデータ連携Scalaバッチの開発をメインに行ってきました。
そのScalaバッチの開発の過程でZIOの良さに気づき導入した話になります。

導入背景

Quillの導入に伴ったZIOの導入だったのですが、全体のコードデザインの方針が立ちやすくなり、また関数型プログラミングを行う上で楽になったり、DIを導入出来たりといったメリットがあり良かったと考えています!

  1. バッチ開発をScala3で始める
  2. slickを導入しようとしたもののScala3には正式には未対応
  3. Scala3に対応している同様のデータアクセスライブラリQuillの導入を決める
  4. QuillがZIOとの同時使用を推奨していたためZIOを導入することに決める

ZIO導入のメリット

関数型プログラミング

例えば以下のような関数を考えた場合、4割る2といった計算であれば特に問題ないのですが、0での割り算の場合は実行時例外が発生します。

def divide(a: Int, b: Int): Int = a / b
divide(5, 0)

// java.lang.ArithmeticException: / by zero

この場合の問題点として以下の点が挙げられます。

  • この関数は例外を返すことがあり、返り値を正確に表しておらず、関数型プログラミングの作法としてはあまり良くないということになります。
  • 例外を返すことが関数の型から事前にわかっていないと、例外の発生を想定した処理の実装を忘れるといったことがあり、プログラムが意図しない挙動をすることがあります。

このような関数の返り値について返り値をOption型等にするといった方法もありますが、以下のようなZIO型を使うことで失敗する可能性があることを示すことが出来ます。

ZIO [-R, +E, +A]

このZIO型の意味は以下の様になっています。

R => Either[E, A]

各型の意味は以下の様になっています。

  • 実行には型Rのコンテクストが必要である。(コンテクストはデータベースコネクション、RESTクライアント、設定に関するオブジェクトといったものになります。)
  • 型Eのエラーで失敗する可能性や型Aを返して処理が成功する可能性がある。

DIパターン

関数型プログラミングでのZIO型の記載のようにR型がDependencyとなります。
実行時にはこのDependencyを注入することになるのですが、このようにDIパターンを使うことでテスタビリティが向上し、バグが減るという効果が見込まれます。

サービスパターン

上記2つのZIO導入のメリットが得られると個人的に考えている公式のZIOを使ったサービスパターンについて紹介しようと思います。

The Five Elements of Service Pattern

サービスを定義(Service Definition)

この記事ではドキュメントのレポジトリに対する操作を対象にこのデザインパターンを紹介しています。
まず、savereadといった操作の関数の定義をDocRepという名前のtraitに対して行います。

サービスの実装(Service Implementation)

「サービスを定義」で作成したtraitの実装としてDocRepoImplという名前のcase classを作成します。

サービスの依存性(Service Dependencies)

サービスの依存性について注入できるようにコンストラクタを作っています。
本パターンではコンストラクタインジェクションにより依存性を注入します。
それと同時に具体的な実装を行っています。

ZLayer(ZLayer (Constructor))

依存性を注入されたDocRepを返すlayerを定義します。
ZIOでは一般的には依存性はZLayerでラップし、他のサービスなどへの依存性注入に使います。
そのため、ここでも返り値はZLayer型となっています。

アクセサメソッド(Accessor Methods)

単純に例えばDocRepo.saveといったように呼び出せるようにコンパニオンオブジェクトを使いアクセサメソッドを作成します。

単体テスト

最後に上記サービスパターンの関数についての単体テストについて1例を載せます。

SomeTestSpec.scala
import zio.*
import zio.test.{Spec, TestConsole, ZIOSpecDefault, assertTrue}

import java.text.SimpleDateFormat
import java.util.Date

object SomeTestSpec extends ZIOSpecDefault {
  def spec: Spec[Any, Any] =
    suite("単体テスト例")(test("日付表示に関するテスト") {
      for {
        _ <- TestService.consoleTest("Trump")
        questionVector <- TestConsole.output
        q1 = questionVector(0)
      } yield assertTrue(q1 == "Hello Trump !! Today is 2022/07/04\n")
    }.provide(ZLayer.fromZIO(ZIO.attempt {
      import java.text.SimpleDateFormat
      // 任意の日付文字列// 任意の日付文字列
      val inpDateStr = "2022/07/04 15:20:00"

      // 取り扱う日付の形にフォーマット設定
      val sdformat = new SimpleDateFormat("yyyy/MM/dd hh:mm:ss")

      // Date型に変換( DateFromatクラスのparse() )
      sdformat.parse(inpDateStr)
    }), TestServiceImpl.layer))
}
TestService.scala
import zio.ZIO

trait TestService {
  def consoleTest(somePerson: String): ZIO[Any, Throwable, Unit]
}

object TestService {
  def consoleTest(somePerson: String): ZIO[TestService, Throwable, Unit] =
    ZIO.serviceWithZIO[TestService](_.consoleTest(somePerson))
}
TestServiceImpl.scala
import zio.{Console, ZIO, ZLayer}

import java.text.SimpleDateFormat
import java.util.Date

case class TestServiceImpl(currentDate: Date) extends TestService {
  override def consoleTest(somePerson: String): ZIO[Any, Throwable, Unit] =
    Console.printLine(
      s"Hello $somePerson !! Today is ${new SimpleDateFormat("yyyy/MM/dd").format(currentDate)}"
    )
}

object TestServiceImpl {
  val layer: ZLayer[Date, Nothing, TestService] =
    ZLayer {
      for {
        currentDate <- ZIO.service[Date]
      } yield TestServiceImpl(currentDate)
    }
}

テスト実行の前の準備

TestService.scala TestServiceImpl.scalaについて

サービスパターンに沿って作成したZIO型を返す関数の各種ファイルになります。
コンソールに引数で受け取った人名と依存しているDate型をyyyy/MM/dd形式にフォーマットした文字列を出力するのみの関数です。

SomeTestSpec.scalaについて

単体テストを実行するためにはZIOSpecDefaultを継承します。
TestServiceImplがDate型に依存しているため、任意の日付のDateを注入し、テストを実行しています。
実行した結果テストは成功しました。

終わりに

ZIOの紹介記事ということで今回はサービスパターンと単体テストに焦点を絞り記事を書きました!
まだZIOについては並列処理を担うFiberの機能を有効活用できていないため、こちらの機能も使いパフォーマンスの最適化を図るといったことも行っていきたいと考えています。
間違い等ありましたらコメントでご教授いただければ幸いです。

明日は、@yuko-tsutsui さんになります。

参考記事

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