DIについて
DIについての説明とメリットについては以下のページに記載がありますが、先の章でお伝えした通り、このDIの仕組みがZIOには元々備わっています。
DIを導入することでテスト時に単体テスト用のモックオブジェクト(ダミーオブジェクト)に切り替えることなどができ、単体テストを行いやすくなります。
サービスパターン
ZIOのDIの使用の流れを実際に知るため、クリーンアーキテクチャとも関係が深いオニオンアーキテクチャにも適用可能な公式が提唱しているサービスパターンを先の章のHello, World!
プログラムに導入します。
-
オニオンアーキテクチャでは階層構造を円で表現する。オニオン(=玉ねぎ)という名称は、おそらくこの円構造からきているものと思われる。
全ての依存関係は円の中心の層に対して向かう。一方で、中心の層から外側の層へは依存しない。アプリケーションのコア(核)になる層の数は変化しても良いが、ドメインモデル層は常に中心に配置する。
ZIOのサービスパターンは関数型プログラミングとオブジェクト指向プログラミング両方の利点が得られるパターンであるとされています。
2023/07/25 Hello, World!
というように日付も出力されるように修正します。
Date
型をDIの対象とします。
サービスを定義(Service Definition)
最初に大元となるconsoleOutput
といった文字列出力関数の定義をApplicationService
という名前のtraitに追加します。
trait ApplicationService {
def consoleOutput(): ZIO[Any, Throwable, Unit]
}
サービスの実装(Service Implementation)
「サービスを定義」で作成したtraitの実装としてApplicationServiceImpl
という名前のcase classを作成します。
case class ApplicationServiceImpl(currentDate: Date) extends ApplicationService {
override def consoleOutput(): ZIO[Any, Throwable, Unit] = ???
}
サービスの依存性(Service Dependencies)
サービスの依存性について注入できるようにコンストラクタを作っています。
本パターンではコンストラクタインジェクションにより依存性を注入します。
それと同時に具体的な実装を行っています。
case class ApplicationServiceImpl(currentDate: Date) extends ApplicationService {
override def consoleOutput(): ZIO[Any, Throwable, Unit] =
Console.printLine(
s"${new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(currentDate)} Hello, World!"
)
}
ZLayer(ZLayer (Constructor))
依存性を注入されたApplicationService
を返すlayer
を定義します。
ZIOでは一般的には依存性はZLayer
でラップし、他のサービスなどへの依存性注入に使います。
そのため、ここでも返り値はZLayer
型となっています。
object ApplicationServiceImpl {
val layer: ZLayer[Date, Nothing, ApplicationService] =
ZLayer {
for {
currentDate <- ZIO.service[Date]
} yield ApplicationServiceImpl(currentDate)
}
}
アクセサメソッド(Accessor Methods)
単純に例えばApplicationService.consoleOutput
といったように呼び出せるようにコンパニオンオブジェクトを使いアクセサメソッドを作成します。
object ApplicationService {
def consoleOutput(): ZIO[ApplicationService, Throwable, Unit] =
ZIO.serviceWithZIO[ApplicationService](_.consoleOutput())
}
依存性の注入と関数の使用
アプリケーションの本体となるサービスが完成したので、実際に使っていきます。
ApplicationService.consoleOutput
の返り値はZIO[ApplicationService, Throwable, Unit]
となっていることからこの関数はApplicationService
型に依存していることがわかります。
ApplicationService
型については、その実装型であるApplicationServiceImpl.layer
を注入することで解決できます。
しかしながら、ApplicationServiceImpl.layer
の返り値がZLayer[Date, Nothing, ApplicationService]
であることから、さらにApplicationServiceImpl.layer
はDate
型に依存していることがわかります(Zlayer
型の解釈についてはZIO
型とほぼ同等であると考えてください)。
以上からApplicationService.consoleOutput
を使うためには最終的に以下の2つを依存性として注入する必要があることがわかりました。
ApplicationServiceImpl.layer
-
Date
型
run
関数からのApplicationService.consoleOutput
の呼び出しは以下のようになります。
def run: ZIO[Any, Throwable, Unit] = ApplicationService.consoleOutput().provide(ZLayer.fromZIO(ZIO.attempt {
import java.text.SimpleDateFormat
// 任意の日付文字列
val inpDateStr = "2023/07/25 17:46:00"
// 取り扱う日付の形にフォーマット設定
val sdformat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss")
// Date型に変換( DateFromatクラスのparse() )
sdformat.parse(inpDateStr)
}), ApplicationServiceImpl.layer)
provide
関数を使い、依存性を注入しています。
このprovide
関数は引数の順序に依らず、型に応じて自動的に依存性を解決してくれる関数です。
また、ここではZLayer.fromZIO
を使い、日時を自由に指定できるようにDate
型を作成しています。
終わりに
新しい関数や型が多く登場しましたが、まとめるとZIOの基本的なDIの使い方は以下の流れになります。
- サービスパターンにより依存性を持つ関数を作成する(他に依存しない関数の作成も可能です)。
-
provide
関数などで依存性を解決した上でrun
関数に渡し、実行する。
また、上記の通りZIOの依存性の解決については型により決定されるという特徴があることも分かったかと思います。
次に次章ではZIO
型に関係するZIOの例外処理について詳しく見ていきます。
前章:Hello, World!
次章:ZIOのエラー処理
演習
- 今回のサービスパターン、DIパターンを導入した
Hello, World!
プログラムを実際に動かしてください。- 解答例は以下になります。
https://github.com/hatuda/zio-practice/tree/CHAPTER2
- 解答例は以下になります。