この記事について
こちらの記事はクリーンアーキテクチャの Java 実装による解説記事です。
MVC フレームワークに組み込むために一部変更している部分もあります。
それをふまえてご覧ください。
講演内容が @IT さまに記事にしていただけました。
あわせてご参照ください。
https://www.atmarkit.co.jp/ait/articles/1907/08/news002.html
クリーンアーキテクチャよりも軽量で無理なく導入しやすいアプリケーションアーキテクチャパターンを考案しました。
https://nrslib.com/adop/
スライド
JJUG CCC 2019 Spring での発表資料です。
この発表をするにあたって記事を書くことにしました。
YouTube
YouTube でこちらの解説を行いました。
その他解説もしています。もしよろしければチャンネル登録をお願いいたします。
はじめに
みなさんクリーンアーキテクチャをご存知でしょうか。クリーンアーキテクチャは Rovert C. Martin (通称ボブおじさん)が提唱するアーキテクチャです。
クリーンアーキテクチャは次の同心円の図が有名です。
The Clean Architecture: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
この図をぱっと見ただけでは概念図にしかみえないかもしれません。実はこれは詳細な実装のことも考慮された図であると言ったら驚くでしょうか。
実装についてヒントがあるなら実装してみたい。そう思いませんか。
この記事は実装のヒントがあるのなら、それに従って実際に実装してみよう、という記事です。
なお、Java 以外の言語による実装サンプルもあります。
もしご興味あればご覧ください。
# 実践クリーンアーキテクチャ(C#)
https://nrslib.com/clean-architecture/
# 実装クリーンアーキテクチャ(C#)
https://qiita.com/nrslib/items/a5f902c4defc83bd46b8
# Laravelで実践クリーンアーキテクチャ(PHP
https://qiita.com/nrslib/items/aa49d10dd2bcb3110f22
# Laravelでクリーンアーキテクチャ(PHP)
https://qiita.com/nrslib/items/eaf39be65b2ebe5ccf08
これらの記事の中で実「践」になっているものは原典から一部改変があります。もちろんそれには理由があります。その理由についてはこの記事で触れているので、そちらをご参照頂ければと考えます。
サンプルコード
https://github.com/nrslib/play-clean-java
Java で MVC フレームワークである Play Framework に組み込んだサンプルです。
機能はユーザの CRUD 機能とログイン機能があります。
Play Framework を選んだ理由は MVC フレームワークとして一般的なものに組み込みたかったためです。
クリーンアーキテクチャは特定のフレームワークに縛られない発想なので、もちろん Play Framework 以外にも適用できます。
同心円の図
ソフトウェアは変化に常に晒されています。
ユーザエクスペリエンスの向上のためにインターフェースが変化することもあれば、新しいバージョンのデータベースへの載せ替えのために変化することもあります。
変化するのは大変なことですが、ソフトウェアという性質上、すべての変化を拒否することは叶いません。
拒否を続けたソフトウェアは近い将来使われなくなっていくでしょう。
使われないソフトウェアに価値はありません。
であれば開発者は常に変化する可能性に怯えているのが正しい姿であるかというと、それも違うでしょう。
私たち開発者は変化に怯えるのではなく、変化を受け入れる術を見出すべきです。
変化を受け入れるにはどうすればよいか。
それは変化しやすいものに依存することを止めて、変化しづらいものにソフトウェアの重大な意思決定の主導権を渡すことです。
伝統的なソフトウェア開発においては、抽象が詳細に依存するように開発を行っていました。
たとえばビジネスロジックがデータベース操作のアレやコレに終始しているロジックを見たことはないでしょうか。
まさにこれこそ抽象が詳細に依存している例です。
この作りはビジネスロジックが特定のデータストアにロックインすることを促します。
ビジネスロジックは技術的なコードに支配されます。データストアがEOLを迎えたときにビジネスロジックは死を迎えます。これは明らかに間違った姿です。
ビジネスにおいて最も重要なのはビジネスロジック、すなわち抽象であるはずです。
データベースという詳細に依存し、変化の主導権を本質的でない詳細に握らせるのは正しい開発手順ではないでしょう。
変化の主導権は高次元な抽象にあるべきです。
クリーンアーキテクチャはまさにそれを推し進めた形です。
この同心円の図はビジネスを中心に見据え、それ以外の詳細を隅に追いやっています。
矢印は依存の方向です。詳細はビジネスに依存するということを示唆しています。
ここにあるのは、高次元な概念であるビジネスが詳細に依存すべきでないという至極単純なルールだけです。
そう考えるととても簡単な図に見えてきませんか。
さて、同心円のコンセプトについて理解をすると、今度は右下の図に目が移ります。
これは何でしょうか。
矢印をよく観察してみるとそこには二種類の矢印が存在していることが分かります。
ひとつは通常の矢印で、ひとつは白抜きの矢印。
この矢印をどこかで見たことがありませんか。
そうです。UML です。
これは UML の依存と汎化を表現しています。
では Flow of control という紫色の矢印は何なのかというと、これは処理の流れを意味しています。
Controller はインターフェースである Use Case Input Port を呼び出します。
Use Case Input Port は自身の実装クラスにあたる Use Case Interactor に処理を移譲します。
Use Case Interactor は結果を伝える先であるインターフェースの Use Case Output Port に結果を伝えます。
Use Case Output port は自身の実装クラスにあたる Presenter に処理を移譲します。
Presenter は結果を表示するために加工して表示をします。
この流れを矢印でなぞるとちょうど Flow of control と同じになることに気づきます。
Flow of control はそのまま「処理の流れ」を示していたのです。
以上がこの図の解説です。
同心円の図はコンセプトを語り、右下の図は実際の実装例を示しています。
さぁ、クリーンアーキテクチャの図に対して理解が深まったところですが、今度はこの図を取り巻く歴史的な経緯を少し垣間見ておきましょう。
といってもひとつのアーキテクチャのコンセプトについて学ぶだけです。そう身構える必要はありません。
クリーンアーキテクチャと同じようなコンセプトを持つものにヘキサゴナルアーキテクチャというものがあります。
これはビジネスを中心に見据え、それ以外の詳細を差し替え可能にしようというアイデアです。
ゲーム機を思い浮かべてください。
コントローラのプラグがゲーム機のソケットに合いさえすれば、コントローラを差し替えてもゲームはプレイできるでしょう。
モニターもそうです。ゲーム機は通常のモニターであればメーカーなどの違いは関係なくゲーム画面を映します。
保存媒体は? HDD などの記憶媒体を使うこともあれば、クラウドに保存したりすることもできます。
ゲーム機以外のすべての詳細はアダプタ(プラグ)とポートが合えば差し替え可能です。
ヘキサゴナルアーキテクチャのコンセプトはこのようにプラガプルにモジュールを付け替えしようというものです。
アプリケーションはポートを公開し、UIや永続化装置などはアダプタのように形さえ合えば差し替え可能であるようにします。
ヘキサゴナルアーキテクチャとクリーンアーキテクチャはコンセプトが同じです。
唯一異なる点はその実装方法について、ヘキサゴナルアーキテクチャはあまり詳しく定義していないという点です。
そう考えると、ヘキサゴナルアーキテクチャの詳しい実装方法について述べたのがクリーンアーキテクチャでないのかと私は考えます。
であれば、クリーンアーキテクチャがクリーンアーキテクチャらしいところは、どこかといえばその実装方法です。
この記事ではその具体的な実装についてを解説していきます。
レイヤーの解説
クリーンアーキテクチャには四つのレイヤーがあります。
これは四つであることに固執する必要はありません。
プロジェクトによりレイヤーの数が変わることは許可されます。
Enterprise Business Rules
このレイヤーには Entities という登場人物が描かれています。
これはビジネスルールをカプセル化したオブジェクトのことです。
ドメイン駆動設計のエンティティと似ていますが、少し広い意味を持ちます。
このことに関する詳しい説明は以下 URL をどうぞ。
ドメイン駆動設計のエンティティとクリーンアーキテクチャのエンティティ: https://nrslib.com/clean-ddd-entity/
Application Business Rules
アプリケーションレイヤーに相当します。
Entierprise Business Rules はいわゆるドメイン層にあたり、あくまでもドメインの概念の表現にとどまります。
表現するだけでは肝心の問題解決にいたることは叶いません。
アプリケーションレイヤーはドメインオブジェクトを協調させ問題を解決するレイヤーです。
具体的にはドメインオブジェクトの直接のクライアントとなるオブジェクトが登場します。
Interface Adapters
ヘキサゴナルアーキテクチャのポートとアダプターに相当するものです。
ユーザの入力を解釈しアプリケーションに伝えたり、永続・再構築の手順をアプリケーションに公開して、アプリケーションが外界と接する手段を提供します。
Frameworks & Drivers
詳細なコードです。
こういったギークなコードにビジネスロジックが縛られないようにするために隅に追いやっています。
実装解説
実装をするにあたり、図を参考にしたいのですが、実をいうと同心円の図よりも具体的な図があります。
この図は登場人物の名前こそ違えど、右下の図を詳細にしたものです。
ふたつの図の登場人物の対応関係は次の通りです。
- Controller : Controller
- Use Case Input Port : Input Boundary
- Use Case Interactor : Use Case Interactor
- Use Case Output Port : Output Boundary
- Presenter : Presenter
お互いの図の登場人物や矢印によく目を凝らしてみれば、その関係性がどちらの図においても同じことが分かるでしょう。
ここからはより詳細な図にそって実装を確認していきます。
Controller
Controllerはユーザからの入力をアプリケーションが求める入力データに変換します。
まさにゲームのコントローラと同じことをしています。
ゲームのコントローラはボタンを押した事実をゲーム機に伝えはしません。
ボタンが押された事実を信号に変換し、ゲーム機に伝えているのです。
たとえばフロントの都合で日付を文字列として送るしかなかったとしましょう。
アプリケーションが日付型を欲するときコントローラは文字列を日付型への変換します。
Input Data
Input Data には <DS> という記号がついています。
これは Data Structure をあらわしています。
つまり入力データを渡すためのオブジェクトです。
Input Boundary
Input Boundary は <I> と記述されているとおりインターフェースです。
ユースケースが必要とする入力データを明示します。
Use Case Interactor
Use Case Interactor はユースケースを実行するスクリプトです。
ドメイン駆動設計でいうアプリケーションサービスにあたるものです。
ドメインオブジェクトの力を束ねあげ、問題を解決していく処理が記述されます。
このためドメインオブジェクトの直接のクライアントとなるオブジェクトでもあります。
Data Access Interface
データ永続化を担当するオブジェクトのインターフェースです。
インターフェースを絡めることによりモックで動作させる可能性を残します。
Data Access
Data Access はデータの具体的な永続化を担うオブジェクトです。
サンプルは EBean を用いてデータを保存しています。
Entities
Entities はドメインの概念を表現したドメインモデルを実装したドメインオブジェクトです。
サンプルではデータモデルは別に準備しています。
これはすなわち、データモデルとは異なるということを強調しています。
また同じ言葉で表現されるので少し紛らわしいのですが、ドメイン駆動設計のエンティティよりは少し広い意味です。
詳しくは次の記事をどうぞ。
ドメイン駆動設計のエンティティとクリーンアーキテクチャのエンティティ: https://nrslib.com/clean-ddd-entity/
Output Data
Output Data は Input Data と同じくデータ構造体です。
結果の出力用に使われます。
Output Boundary
Output Boundary は Interactor が結果を伝える先となるオブジェクトです。
Presenter が受け取る出力データの定義をしています。
Presenter
Presenter は OutputData を View のためのデータに変換する責務を担います。
たとえばアプリケーションが Double 型のデータを結果としていた時、丸め誤差を考慮してフロントに String 型として引き渡したいときは Presenter がその変換をします。
ViewModel
View Model は View のためのデータ構造体です。
View
View は View Model をユーザが分かるようにレンダリングします。
処理の流れ
処理の流れはスライドで確認するとわかりやすいです。
次のスライドを追ってみてください。(187ページまで)
メリットとデメリット
実装について把握したところで、このようにするメリットとデメリットについて考えてみましょう。
まずはメリットからです。
メリット
たとえばビジネスロジックを実装しなくてもフロントを動作させることができます。
フロントでうまいこと仕組みを作ることも可能ですが、実際にローカルサーバーに接続して動作させたりすることができるのはとても強みでしょう。
つぎにデータベースに実際に接続せずメモリをデータベースに見立ててビジネスロジックを組み立てることもできます。
EBean はデフォルトでインメモリでの動作をサポートしているので EBean を利用している限りはこの恩恵を受けることはありませんが、たとえば SQL を組み立てて実行しているシステムでは有効に働きます。
特定のデータストアに接続しなくてもビジネスロジックを動作させるようにすることはとても大切です。
データストアが準備されていない段階でロジックを記述することができますし、なんといってもテストを行うことが容易になります。
気軽にテストができるというのは開発者が自信を養うのに貢献します。
ほかには、ユースケースごとに確実に独立するので疎結合になり、コンフリクトが起きないなどのメリットもあります。
また、オブジェクトの責務がはっきりと分かれているおかげで、どこにどういったロジックを書くべきかというのもはっきりとしています。
やるべきことがわかっているので迷うことは少ないです。
デメリット
ではデメリットについてはどうでしょうか。
MVC フレームワークにこれを適用する前提で考えてみます。
まず問題になるのは Presenter です。
そもそも破綻しています。
通常の MVC フレームワークではアクションは戻り値が必要になるため Presenter がうまく動作しません。
次にコントローラのフィールドがユースケースが増えるにしたがって増えることが予想されます。
MVC フレームワークでは CRUD は同じコントローラに記載することが多いでしょう。
ユースケースの増減に従いコントローラのフィールドが増減するのはあまりよい方策には思えません。
最後の問題はある種もっとも深刻です。
つまり定義するファイルが多すぎるということです。
通常ビジネスロジックを実装するためのオブジェクトを抜きにしても Input Boundary, Input Data, Use Case Interactor, OutputData, Output Boundary, Presenter, View Model といったオブジェクトを定義しなくてはいけないというのはいささか面倒が過ぎます。
はたしてこれほど深刻なデメリットがつきまとうクリーンアーキテクチャは使い物にならないのでしょうか。
理想と現実のはざまに
アーキテクチャは理想です。
開発は現実で行われます。
理想が現実に即していないとき、とるべきアプローチはふた通りです。
すなわち他の方法を検討するか、理想と現実の折り合いをつけるか、です。
望まずとも技術的負債の返済を幾度となく経験した開発者であれば、クリーンアーキテクチャがもたらす恩恵は喉から手が出るほど欲してやまないものです。
であればここでは理想と現実の折り合いをつける方向で考えてみましょう。
まずは Presenter についてですが、これは捨てましょう。
そもそも Output Data という DTO のようなオブジェクトがあります。
もっとも大事なのは Presenter を使うことに固執することではなく、レイヤー間を疎結合にすることです。
アプリケーションレイヤーとプレゼンテーションレイヤーの境界をまたぐのに Output Data という DTO を経由していれば、レイヤー間は疎結合に保たれます。
一点だけ気にするのは Controller に出力のためのデータ変換処理が記述されることですが、許容範囲ともいえるレベルでしょう。
(もちろんiOS/Android開発などではPresenterといった考えはマッチします。天びんにかけた結果使わない方法を選びました。)
次にフィールドが増えすぎる問題についてですが、これは Message Bus というパターンを使います。
Message Bus がどういうものかは次のスライドを確認したほうが分かりやすいでしょう。
要するに処理系統をあらかじめ登録しておいて、引き渡されたデータによってそれぞれ処理すべき処理系統が処理を行うというパターンです。
Message Bus のパターンに沿った UseCaseBus は次のコードです。
Controller は InputData を UseCaseBus に引き渡すだけで、あとは UseCaseBus に登録された Interactor が処理を行い OutputData を返却してくれます。
つまりフィールドが UseCaseBus だけで済むようになります。
最後の定義ファイルが多すぎるという問題の解決は至極簡単です。
面倒なことはプログラムに任せてしまえばいいのです。
つまりスキャフォールディングツールを作ります。
これは工数がある程度かかりますが、大規模な開発であればあるこそ効果を発揮するでしょう。
説得の仕方にもよりますが、クリーンアーキテクチャのメリットについてお話し、「ツールを一度作ればその後も使いまわせる」ということをアピールすれば工数を確保できたりするのではないでしょうか。(ちなみに自分は自宅でちゃちゃっとある程度の仕組みを作っちゃいました。)
特殊な処理の解説
サンプルコードにはいくつか特殊な処理があります。
それについての解説もしておきます。
com.nrslib.clArc
‘Cl’ean’Arc’hitecture 用パッケージです。
UseCaseBus などのモジュールが含まれています。
com.nrslib.clArc.inject.ServiceCollection
ServiceCollection は依存関係の登録処理用インターフェースです。
UseCaseBus は IoC Container と連携します。
IoC Container は Guice に限らないためインターフェースを用意しています。
com.nrslib.clArc.inject.ServiceProvider
ServiceProvider は依存関係を解決するときに呼ばれるインターフェースです。
IoC Container からインスタンスを取り出して返却します。
com.nrslib.clArc.UseCaseBusBuilder
UseCaseBusBuilder は UseCaseBus を生成するオブジェクトです。
UseCaseBusBuilder に実施する UseCase を登録して build メソッドで UseCaseBus を生成します。
com.nrslib.clArc.UseCaseBus
UseCaseBus は引き渡された InputData から対応する Interactor を見つけ出し、UseCaseInvoker 経由で呼び出します。
UseCaseInvoker はキャッシュされているので最初だけインスタンス生成されます。
com.nrslib.clArc.invoke.UseCaseInvoker
UseCaseBus は Interactor を直接実行しません。
UseCaseInvoker というインターフェース経由で呼び出します。
UseCaseInvoker を実装することで Interactor の実行前と実行後に処理を挟める可能性を残しています。
com.nrslib.domain
いわゆるドメイン層です。
com.nrslib.domain.context.UserContext
ドメイン層ではセッションなどは扱いません。
もしもログインしているときのユーザー情報が欲しいのであれば、このように欲しい情報をインターフェースで公開して DI するように促します。
com.nrslib.lib
ちょっとしたユーティリティのパッケージです。
com.nrslib.lib.json.objectLoader.JsonsLoader
JsonsLoader はデバッグ実行用です。
デバッグ実行は完全なローカルで動作し、Interactor はすべて Stub で動作します。
Stub の Interactor の実装はすべて次のような実装です。
処理は JsonsLoader に移譲されます。
JsonsLoader は引き渡された OutputData のクラス名に基づき、設定されているディレクトリから .jsons ファイルを検索します。
.jsons ファイルは json 形式で返却すべきデータを複数記述できるファイルです。
JsonsLoader はこのファイルのデータから OutputData を生成してデータを返却します。
つまりこのファイルをいじるだけで任意のデータが返却できます。
JsonsLoader の詳しい実装は次の通りです。
lib.forClArc
clArc のインターフェースを実装したモジュール群です。
lib.forClArc.PlayUseCaseInvoker
UseCaseInvoker の実装クラスです。
UseCaseBus から呼び出されます。
クラスに定義されている @Transactional アノテーションを確認してトランザクションを開始しています。
おわりに
以上で解説は終了です。
本来プログラミングというのは自由なものです。
しかし自由というのは開発者にある種の自制心を強く求めます。
複数人で開発するときにすべての開発者が同じ熟練度であれば問題はそうそう起きませんが、そのような幸運が訪れることは稀です。
自由がもたらすのはもしかしたら無秩序かもしれません。
自由がもたらす混沌がもっとも恐るべきことであることは皆さんもよくご存知でしょう。
アーキテクチャを用意することはレールを敷くことに似ています。
ある程度の不自由さを課す代わりに、安全で快適な旅を提供します。
レールを走るのはいろいろな種類の列車です。
すべての列車に合わせたレールを用意するのは難しいかもしれません
それでも可能な限りすべての列車が効率的に走れるレールを用意するのがアーキテクトの役目です。
列車は開発者のオマージュです。
分業が発達した昨今では開発・運用・保守のフェーズで開発者が分かれていることもあります。
このときどれかに偏ってはいけません。どれかに負担を強いすぎてはいけません。
アーキテクチャを採用するときはさまざまな視点を考慮して、妥協するのか貫き通すのか、それとも迂回路をつくるのか、常に検討を怠らないことが肝要です。
この記事で学んだことを鵜呑みにしないでください。
あなたが行った行動に対する結果はすべてあなたに降りかかります。
そのときに後悔しないように、あなた自身のバイアスをかけて、この記事に対する答えを見出してください。
以上でこの記事は終わりです。
ここまでお付き合いいただきありがとうございました。
それでは皆さん快適な旅路を!