「Clean Architectureで実装するバッチ処理」についての勉強
本業はバックエンドエンジニアで、興味のほとんどもバックエンドにあります。
ここでいうバックエンドはWebサーバ等を持ってるAPIサーバのことではなく、
常にタスクを処理して計算し続けているようなソフトウェアです。
そういったソフトウェアの設計をするときに、レイヤードアーキテクチャを参考にしようとするのですが、
検索で手に入るのはWebAPIのCRUDを実装する例ばかり。
外部から叩かれて動くプログラムの記事から、内部から自発的に動くプログラムの設計をするのは無理だなーと常日頃から感じていたので、どういったサイトの何を参考にして、自分なりのアーキテクチャを構築したかをまとめます。
要件
スケジューラでタスクをコントロールする常駐アプリケーションの開発
Webサーバなどを持たず、UIがない
ただし外部からのデータ要求などがあるかもなので、その際にはgRPC等を使って通信の受付をする
gRPCとWebAPIで外部からデータをもらう
gRPCはServer StreamでClientであるこのアプリケーションはデータを受け取り続ける
WebAPIはRESTFullなので、必要な時に必要なリクエストを発行する
各タスクは非同期に実行される
実行タイミングはスケジューラがコントロールする
実行のトリガーは時間のみで、このタスクが終わったらこのタスクをするよみたいな逐次処理は一つのタスクとみなす
なぜClean Architectureなのか
レイヤードアーキテクチャの代表としてはDDDが強いと思います。ドメイン駆動開発ってやつですね。
ですが、ドメイン駆動の開発をしたいわけじゃないんです。僕が作るバッチ処理には検証・研究の意味合いが強い物も少なくなく、まずは最低限のプログラムで期待する結果が得られるかを検証します。
そこでいい結果が得られた場合にのみ、保守可能なように再構築します。
といった感じで、開発時にはドメインがないんですよね。
じゃあドメイン駆動できへんわ。ってことで、DDD以外の駆動と関係のないアーキテクチャを使いたかったんです。
そこで比較的やりたいことが分かりやすいClean Architectureを採用したいと考えました。
正直なところ、Clean Architectureはかなり大きなアーキテクチャだと思います。
例えばDBコネクション・コントロールまで抽象化して、外側から注入することになります。
RDBからNoSQLに変えましょう!っていうプロジェクトがどれくらいあるのかという話ですよね。
じゃあモデルはDBに依存してもいいんじゃね?と感じてしまいますが、Clean Architectureではそれは許されません。
技術的関心事はすべて外に追い出します。メール送信もそうですし、WebAPIを叩くのもそう、gRPCのコネクションもそう。
この辺がまー大変なんです。ソース量が激増しちゃうんです。
それでも抽象化するメリットは大きいです。
DBが変わることは少なかったとしても、例えばWebAPIのレスポンスが変わることや、WebAPIのエンドポイントが変わることはあり得ますし、この辺は使う側ではコントロールできないものです。
モデルがWebAPIのレスポンスに直接依存していたら、DBにも変更が必要になるかもしれません。
DB全く関係なくても、DBに影響が及ぶってめんどくさいですよね。
ってことで、WebAPIのレスポンスをパースしたオブジェクトと、ドメインのオブジェクト(Entity)は無関係にしておきたいです。
そういう思惑もあって、Clean Architectureをなるべく真正面から実装してみたいと感じているわけです。
参考にしたサイト
レイヤーの責務
参考にしたサイトを見ていただければ十分ですが、頭の中の整理のために書き出します。
https://blog.cleancoder.com/uncle-bob/images/2012-08-13-the-clean-architecture/CleanArchitecture.jpg
外側から順に
Frameworks & Drivers
Frameworkをどう外に出すのかはいったん置いといて、Driversについてのみ
Driversの代表としてはWebとDBだと思います。Webから受けたリクエストによってDBからデータを出したり入れたり
ってことで、Webのリクエストを処理して内側にいれるのも、DBからデータを取り出すのも、DBにデータをいれるのも、実装はいちばん外側にします
ここで注意したいのが、データをどのように加工してどのように入れるかとかはここには関係ない
ここではデータの受渡が仕事であって、システムのために都合のいいような処理は属さない
Interface Adapters
Adapterってことで、つなぎの部分。
何と何をつなぐかというと、内側と外側をつなぐ。
Webリクエストも、DBとのやりとりも、目的の計算や永続化だけをすることを考えると不要な情報が多かったり、必要な形になってなかったりする
ってことで、外側から入ってきたものの中から必要なものを必要な形で内側に伝えるのと、内側から出てきたものには外に出すのに不足しているものを追加して返す
Application Business Rules
アプリケーションのビジネスロジックはUse caseしかない
Use caseをGoogle翻訳にいれると「使用事例」ってのが返ってきた。言葉の通りで、こんな使われ方するでーってのを書くところってこと
よく言われるビジネスロジックはここかとおもう。人が手であれしてこれしてーってのを書くところじゃないかなーと
イベントステップとか言われるみたいやけど、要は手順のことじゃないかな
Enterprice Business Rules
ここに含まれるのはEntityだけ
ビジネスロジックの内側にあるから、当然ながらビジネスロジックではある
じゃあビジネスロジックとは何が違うかっていうと、他のシステムでも使える処理かどうかってところやとおもう
UseCaseがイベントステップなら、Entityはイベント
ただのデータオブジェクトなこともあれば、小さくまとまった処理かも
Entityの範囲がまーひろい。UseCaseの具象もここに入るし、データオブジェクトもここに入る
UseCaseとEntityの切り分けがむずかしーよー!!!って人は、UseCaseへの変更の影響はEntityには関係ないということを念頭におくといいのかも
「UseCaseではinterfaceだけを用意して、実装はEntityで」みたいなことが書かれている記事もありますが、例えinterfaceをダックタイピングしていたとしても外側に依存しちゃってる気がするので、UseCaseの実装をするためにEntityを使うのは間違いでしょう
複数のUseCaseで同じ目的で同じような処理を書く場合に、Entityで共通化は合っていると思います
UseCaseとEntityのストーリーが難しいので、ちょっと例を考えてみました。
WebAPIがってその中の複数のエンドポイントを叩くにあたって、共通の認証用のキー(文字列)があったとします。
そのキー発行からは30分有効で、30分経過したら何がなんでも無効になります。期限を延長する方法はありません。
さらにこのキーの新規発行はそれなりにコストの伴う処理です。いっぱい計算してやっと作ることのできるキーなので、可能な限り作り直したくないです。
ということで、期限がきれるまでの間は同じキーを使い続け、キーが切れたら誰かが代表してキーを新たに作り、同時に複数のキーをつくることがないようにしたいです
とあったとき、シングルトンパターンを使うのがいいと思います。このシングルトンのオブジェクトはどのレイヤーに所属すべきでしょうか、どこがシングルトンオブジェクトからキーを取り出すべきでしょうか。
依存の解決
依存の方向は一方向で、常に依存は外側が内側に依存する
どういうことかというと、EntityはDBに依存することなく動く。
DBがRDBでもNoSQLでもEntityの形は変わらないってのが理想で、守らなくちゃいけないルール
これが守れないと、DBの変更でEntityが変わったり、UseCaseが変わったり、Adapterが変わったりしてしまう。
そうなると影響がいろんなところに及びまくって、保守・変更が大変になる。
ここで誤解したくないのが、依存とは何かということ。
引数を受け取れる関数が、外から渡されたデータを使用するのはここでいう依存ではない。
例えばデータをDBに永続化するときに、DBのコネクションを張ったり開いたりするのはDBに依存した行為になる。
DBがPostgresからMySQLに変わるとコネクションの張り方が少し変わるかもしれないし、PostgresからMongoDBに変わったらコネクションを張るライブラリから変わるはず。
何かが変わったら一緒に影響を受けてしまうのを依存と呼ぶのが正しいと思う。
WebとかDBが一番外側にある以上、何でもかんでも外に依存するのは仕方がないことですが、それをしないようにしたい。
ってことで、外部注入を使う。
何かっていうと、一番外から中で使うものを渡しちゃおうって考え方。
引数に渡すんじゃなくて、オブジェクトに入れこんじゃうってのが一般的だと思う。
この方法を使えば、内側で依存するものは外から与えられるという構図ができて、外への依存は外がコントロールできるようになる。
つまり、外が変わっても注入するものが変わっていなければ内側は何も変わらなくてもよくなる。
でも注入されるものがオブジェクトなら、型が制限されて変更が難しい。
そこで使うのがInterface。抽象オブジェクトを外から中に入れるようにしておけば、正しく実装されたオブジェクトであれば何でも放り込めるようになる。
これで中から外に依存するために出ていくということをしなくてよくなる。
実際に作ってみる
ソースはこちら
今回作るシステムは下記の機能をもっているとします
一定間隔で特定のWebサーバにリクエストを投げて結果が変わっているかを監視し、変更があったら通知する機能
カブコムのメンテナンス情報が欲しいっていう個人的な事情もあって、ここの情報を定期的に取ってみようと思う 機能をもうちょっと細分化
一定間隔で特定のWebサーバにリクエストを投げて結果が変わっているかを監視し、変更があったら通知する機能
スケジューラ機能 : 一定間隔で処理を実行する
HTTPリクエスト機能 : 指定したURLにリクエストを投げ、その結果を受け取る
結果保持機能 : 前回実行した際の結果を保持する
保持結果取り出し機能 : 保持されている結果を取り出す機能
結果比較機能 : 前回実行した際に取得した結果と、今回実行した際に取得した結果を比較する
通知機能 : 結果が変わっていた場合に、結果が変わったことをSlackのWebhookで通知する機能
着手順序
定期的に実行するというスケジューラ機能や、HTTPリクエストを投げる機能、Slackにメッセージを送る機能と、技術的な検証を行いながら進めなくてはいけない機能がいくつかあります。
これらは間違いなく技術的な関心事であるはずなので、いちばん外側のレイヤーにおきたいです。
こんな機能が必要だなーという想像ができているので、ビジネスロジックの詳細であるいちばん内側のレイヤーも作れます。
この時に気を付けないといけないのが、ビジネスロジックに登場するデータはDBなどに永続化するデータとは違うもとのです。
例えばデータベースではよく使われる、データの作成日時、更新日時、論理削除のフラグなどがありますが、これはビジネスロジックとは無関係です。
今回の要件で言うと、HTTPレスポンスで返ってきたBodyと確認した日時がビジネスロジックで必要なデータで、それ以外のデータは必要ありません。
ビジネスロジックの中のデータオブジェクトがイメージで来たら、それを使う処理を考えます。
今回でいうと、Webサーバにリクエストを送って返ってきたデータを、保存していたデータと比較して一致しているかどうかを見ます。
そのあとに通知するという機能もあります。可能であれば通知するなどまでまとめて考えられたほうがいいとは思いますが、まずはコアとなるロジックだけを考えてみようと思います。
Webサーバへリクエストをおくり、返ってきたデータを受け取る
保存していたデータを取り出す
比較する
新しいデータを保存する
あとは外部とのやりとりである、HTTPレクエストやデータの保存先とのやりとりをrepositoryにinterfaceで定義すればいちばん内側のレイヤーはOKです。
ユースケースはビジネスロジックをステップとし、それをまとめてフローにするのが仕事です。
Webサーバからデータをとってきて、保存していたデータもとってきて、比較し、保存する
これがひとつのフローで、ユースケースです
アダプターは外から中が呼ばれるときにのみ使われるものです。
例えばAPIサーバならHTTPリクエストをビジネスロジックで使う形にし、結果をHTTPレスポンスにします
スケジューラから叩かれた場合は、パラメータがあればそれを含めてユースケースを叩き、結果を返します
いちばん外側はビジネスロジックとは隔離されていますが、逆にどこからでも呼べるような形にしなくてはいけません
開発中も必要な処理をいちばん外側に追加しながら、内側の処理が動くようにする必要があるとおもいます
内側から間接的に呼ばれる外側は内側を作りながらになりますが、外側から内側を呼ぶ処理はadapterができてからなので後の方になります
ユースケースとドメインを分離するための基準
色んな記事に、こう分けます!!みたいなのが載っていますが、どれもしっくりこかなかったので、自分なりの解釈に基づいた分け方をしています。
ユースケースは手順をフローとして扱うためのもの
ドメインはユースケースの各ステップを必要な範囲にしぼって実行するためのもの
何かしら新規の注文を受け付けることを考えた時、注文を受け付けるというユースケースを作ります
注文を受け付けるには、注文の妥当性をチェックしたり、注文を永続化したりする必要があるはずです
この、注文の妥当性チェックや注文の永続かがドメインになります。
ドメインがドメインに依存しなくてはいけないような状況になるなら、ユースケースが間に立って依存範囲を限定することが出来るようになると思います。
ユースケースからユースケースへの依存はどうなの?みたいなのもあるので、一概にこれでOKとは言いにくいんですが、ドメインをより細かい粒度に砕いて結合することが容易になります
最終更新日 : 2019/12/09