前提
このエントリはClean Architecture自体はすでに知識がある前提になりますので、なにそれ?という人は以下ののエントリを読まれると良いかと思います。
クリーンアーキテクチャ(The Clean Architecture翻訳) | blog.tai2.net
まだMVC,MVP,MVVMで消耗してるの? iOS Clean Architectureについて - Qiita
また、この内容は以前のエントリの宿題としていた、 1) クリーンアーキテクチャの絵の右下にあるフロー図の理解。に対する自分なりの回答です。
はじめに
昨日今日とはDroidKaigi2018に参加してきました(DroidKaigi会場からポストしました)。Day1 Partyの寿司がケータリングじゃなくて会場で板前さんが握ってくれたり大画面でスト2やってたりして非常に楽しめました。セッションの方はアーキテクチャ関連を中心に回ってきました。そこで色々なアーキテクチャ話を聞いているうちに、以前からモヤモヤしていたClean Architectureについてもっとモヤモヤしてきたので整理をしてみたいと思います。
なににモヤモヤしているのか?
さて、Clean Architectureといえば日本のエンジニアの間ではkoutalouさんのエントリが一番有名なのではないかと思います。実際私もiOSの勉強会で初めて知った後何度も読みました。何度も読んだのですが、モヤモヤしたのは「Repositoryクラスの後ろにいるDataSourceやEntityはData Layerなのか?これでは今までのUI/UseCase/Dataのレイヤーに分けた3層構造と変わらないのではないか?」ということです。
なぜそんなことを気にしているかというと、本家の絵の円の図の左下には「DB」がいます。つまり、「DBへのデータ保存」はUIと同じ一番外側の層の「Frameworks & Drivers」にデータを預けることになるのでData Layerは「Frameworks & Drivers」層の一部と考えることができると思います。一方、「Entity」は円の一番中心にいます。円の中心ということはEntityはUseCaseの層、Domain Layerなのではないのでしょうか?
さらに、円の右下側の図、
この図では
- 「Use Case Interactor」は「Use Case Input Portインターフェース」を継承(=実装)し、「Use Case Output Port」インターフェイスに関連する(=使用する)
- Controllerは「Use Case Input Portインターフェース」に関連する(=使用する)
- Presenterは「Use Case Output Port」インターフェースを継承(=実装)する
とありますが、この右下の図をしっかり説明しているエントリがみつかりません。(私のGooglabilityのせいかもしれませんが)
以上がモヤモヤの原因です。整理すると以下のようになります。
1. 円の一番中心にいるEntityとはなんなのか?
2. DataSourceに紐づくEntityはData Layerなのか?
3. 右下のFlow of Controlとそれを実現するクラス構成がよくわからない
DroidKaigiの帰りに、もう一度本家のblogを読み返したところ色々わかってきたことがあったので順番に書いていくことにします。
ここから本家のblogとそれを日本語訳されたblogからいくつか引用しています。参考リンク*1として最後にまとめてあるので時間があるときに是非全文をよんでみてください。
1, 円の一番中心にいるEntityとはなんなのか?
まずEntityとは何か、ですが本家のブログには以下のようにあります。日本語訳エントリの記載も載せておきます。
Entities
Entities encapsulate Enterprise wide business rules. An entity can be an object with methods, or it can be a set of data structures and functions. It doesn’t matter so long as the entities could be used by many different applications in the enterprise.
If you don’t have an enterprise, and are just writing a single application, then these entities are the business objects of the application. They encapsulate the most general and high-level rules. They are the least likely to change when something external changes. For example, you would not expect these objects to be affected by a change to page navigation, or security. No operational change to any particular application should affect the entity layer.
エンティティーは、大規模プロジェクトレベルのビジネスルールをカプセル化する。エンティティは、メソッドを持ったオブジェクトかもしれない、あるいは、データ構造と関数の集合かもしれない。エンティティが、大規模プロジェクト内で、たくさんの異なるアプリケーションから使われるのであれば、どちらでも問題ない。
大規模プロジェクトではなく、ひとつのアプリケーションを書いているだけであれば、エンティティは、そのアプリケーションのビジネスオブジェクトである。それらは、もっとも一般的で高レベルなルールをカプセル化する。それらは、外側のなにかが変わっても、変わらなさそうなものだ。たとえば、それらのオブジェクトは、ページナビゲーションの変更やセキュリティからの影響を受けないことが期待できる。アプリケーションの動作への変更が、エンティティーレイヤーに影響を与えるべきではない
いかがでしょうか。自分はEntityとはインスタンスフィールドだけやgetter/setterのような「データを保持するだけのクラス」をイメージしていましたが、本家の説明では以下のようにかなり広い責務を持っています。
- ビジネスルールをカプセル化する
- メソッドをもったオブジェクト、データ構造と関数の集合かもしれない
- 場合によってはビジネスオブジェクトであり、最も一般的で高レベルなルールをカプセル化し、外側が変わっても不変なもの
ではEntityがこんなに責務を持つのであればUse Caseとは何なのか?もう一度もともとの説明を確認します。
Use Cases
The software in this layer contains application specific business rules. It encapsulates and implements all of the use cases of the system. These use cases orchestrate the flow of data to and from the entities, and direct those entities to use their enterprise wide business rules to achieve the goals of the use case.
We do not expect changes in this layer to affect the entities. We also do not expect this layer to be affected by changes to externalities such as the database, the UI, or any of the common frameworks. This layer is isolated from such concerns.
We do, however, expect that changes to the operation of the application will affect the use-cases and therefore the software in this layer. If the details of a use-case change, then some code in this layer will certainly be affected.
このレイヤーのソフトウェアは、アプリケーション固有のビジネスルールを含む。このレイヤーは、システムのユースケースすべてをカプセル化および実装する。これらのユースケースは、エンティティからの、あるいはエンティティーへのデータの流れを組み立てる。そして、エンティティ、プロジェクトレベルのビジネスルールを使って、ユースケースの目的を達成せよと指示する。
このレイヤーの変更は、エンティティーには影響を与えないことを期待する。このレイヤーが、データベース、UIあるいは、共通のフレームワークの変更から影響を受けないことも期待する。このレイヤーは、そういった関心からは隔離される。
しかしながら、アプリケーションの操作への変更は、ユースケースに、つまりは、このレイヤーのソフトウェアに影響することを期待する。ユースケースの詳細が変われば、このレイヤーのコードは、確実に影響を受ける。
日本語訳だけを読むと違いがよくわかりませんが、英文に注目してみると…
..., and direct those entities to use their enterprise wide business rules to achieve the goals of the use case.
Entity
ではなくEntities
とあります。複数形です。そして最後はthe goals of the use case
としています。Use Caseは単数形です。つまり、1つのUse Caseは複数のEntityを束ねてある処理を完結させることが責務となります。
ということで、本家のEntity
が意味するものはデータを保持するだけのクラスではなく、それなりのメソッドや関数を持つことを想定されたクラス、あるいはクラス群となります。値を保持するだけのクラスではありません。
追記) Droid Kaigi 2018のDay2セッションであんざいゆきさんの「All you need is isolating the domain (How to apply DDD to Android Application Development 2)」を拝聴したところ、このEntityはドメイン駆動設計のEntityであるのではないか、という思いに至りました。詳しく知りたい方はドメイン駆動設計について調べてみてください。
2, DataSourceに紐づくEntity 値を保持するだけのクラスはData Layerなのか?
1ですでにEntityはデータ保持するだけのクラスではない、ということを書いたので、ここでは「データを保持するだけのためのクラス」はどこにあるべきなのか、を考えてみます。これは以下のInterface Adapterの部分にヒントがあります。
Interface Adapters
The software in this layer is a set of adapters that convert data from the format most convenient for the use cases and entities, to the format most convenient for some external agency such as the Database or the Web. It is this layer, for example, that will wholly contain the MVC architecture of a GUI. The Presenters, Views, and Controllers all belong in here. The models are likely just data structures that are passed from the controllers to the use cases, and then back from the use cases to the presenters and views.
Similarly, data is converted, in this layer, from the form most convenient for entities and use cases, into the form most convenient for whatever persistence framework is being used. i.e. The Database. No code inward of this circle should know anything at all about the database. If the database is a SQL database, then all the SQL should be restricted to this layer, and in particular to the parts of this layer that have to do with the database.
Also in this layer is any other adapter necessary to convert data from some external form, such as an external service, to the internal form used by the use cases and entities.
このレイヤーのソフトウェアは、アダプターの集合だ。これは、ユースケースとエンティティにもっとも便利な形式から、データベースやウェブのような外部の機能にもっとも便利な形式に、データを変換する。たとえば、このレイヤーは、GUIのMVCアーキテクチャを完全に内包するだろう。プレゼンター、ビュー、そしてコントローラーは、すべてここに属す。モデルは、コントローラーからユースケースに渡され、そして、ユースケースからプレゼンターやビューに戻される、単なるデータ構造である可能性が高い。
同じように、データはこのレイヤーで、エンティティーやユースケースにもっとも便利な形から、どんな永続化フレームワークが使われているにしろ、それにとってもっとも便利な形に変換される。例えば、データベースなど。この円よりも内側のコードは、データベースについてなにも知るべきではない。もしこのデータベースがSQLデータベースであるならば、どんなSQLであれ、このレイヤーに、もっと言うと、このレイヤーの中のデータベースに関連した部分に、制限されるべきだ。
また、このレイヤーには、その他すべてのアダプターもある。それらは、外部の形式(たとえば外部サービス)から、ユースケースとエンティティーで使われる内部形式にデータを変換するために必要なものだ。
日本語訳にもある通りここではModelについて言及されており、モデルはController、Use Case、Presenterを渡り歩くデータ構造の可能性があるとあります。そして、データ構造やクラスは、それぞれの層によって適切な形が異なるため、各層で必要に応じて定義し、Interface Adapterでそれらの変換を行え、ということが提案されています。
例えば、Userの情報をUse CaseからUIに渡す場合、Interface AdapterがとUse Case内で使われていたデータ構造を「UI表示に適した」別の形(UI用であれば多言語化や地域ごとの時刻、単位表現の補正などでしょうか)に変換をして、UI側のFramework/Driverに渡します。また、同様にInterface AdapterはWebAPIなどの応答にあるJSON形式のUser情報をUserクラスに変換してからデータをUse Caseへ渡します。
特に、円の一番外側の層は「Frameworks & Drivers」とある通り、UI用のFramework, DB用のFramework, ネットワーク通信用のFrameworkのようにそれぞれClean Architectureとは無関係のFrameworkが存在するべき(or してもよい)世界になります。それらFrameworkの中にはデータ保持だけを担当するクラスがあることは全く普通のことであるという点は理解できると思います。
従って、データ保持を担当するクラスは必要に応じて各層に存在してよい、と判断できます。
3, 右下のFlow of Controlとそれを実現するクラス構成がよくわからない
元々の円の図にも描いてある通り、依存関係は必ず外側から内側に、という原則があります。しかし
元々の図が円になっていたのに、クラス構成を従来の縦や横のレイヤーで区切ると正しい依存となっているのか分かりにくくなってしまいます。Clean Architectureのクラス構成を書くときは円をベースにしたレイヤーをもって説明することで依存関係がクリアになるのではないか、と思います。
この点を踏まえて、クラス図を描いてみたものが以下です。これであれば必ず外側から内側に依存をしつつ、さらに、View -> Data source, Data source -> Viewどちらが起点になっても本家の図の右下にあった「Flow of Control」に一致する流れを作り出せます。
2019/8/24追記:下記図で以前はInterfaceAdapterからEntityに依存関係が伸びていましたが、書籍Clean Architecture 22章において
境界線を超えて渡すのは単純なデータ構造であることが重要だ。エンティティオブジェクトやデータベースの行をそのまま渡すようなズルはしたくない。
と明記されていたためOutputDataを追加して依存関係を修正しました。
なお、この図ではUsecase Interactor/Input Port/Output PortはX, Yが、Interface AdapterはA,B,C,Dがそれぞれ別々に存在していますが、それぞれのプロジェクトにおける処理と粒度設計の中で統合するか分離するかを考えれば良いと思います。(設計者の腕の見せ所です)
また、View FrameworkとDatasource Frameworkから「依存関係を逆転させておく」という参照矢印がInterface Adapterに伸びています。これは実際の処理としてInterface AdapterがUsecaseの結果をView/Datasource Frameworkに受け渡す時になんらかの呼び出しをしないといけないのですが、単純な呼び出しだと「内側から外側」への依存が発生してしまうので、依存関係の逆転を組み込む必要があるためです。古典的な方法であればCallbackやFactoryパターンを使うことになると思います。
問題点
1. クラス多くね?
多いです。依存関係の逆転のためにはInterfaceが必要になったり、各層に対する適切なデータ変換のための「箱」クラスなど、画面->DB->画面と一往復させるだけでもいろいろなインターフェース、クラスが必要になってしまいます。上記の図は全て自作するイメージのクラス図です。従って何らかのライブラリやテクニックを使えば、少しずつクラス数を削減できるのではないかとおもいます。例えばEntityを全部の層から参照できるようにしつつ、所定の層からはReadOnlyになるようにアクセス制御することができれば「箱」クラスは多少削減できます。まぁ、これはまた改めて。。。
2. 同期処理で取得できるFramework/Driver層の結果もいちいち別のInterface Adapterに流さないといけないの?
わかります。面倒ですよね。でも、Clean Architectureに則るならば、いちいち別のInterface Adapterに流さないといけないはずです。でも、面倒ですよね。この点についてはEvent Bus, Rxあたりが役に立ちそうです。特にDroid Kaigi 2018のセッションでBenoitさんがプレゼンしたModel-View-Intentパターンの考え方が使えるのではないかと思っています。が、これもまた別途。。。
最後に
問題点についてはまた改めて時間を見つけて書ければと思います。が、自分がモヤモヤしていた3点については一旦整理することができました。このエントリがどなたかの頭の整理に役立てば幸いです。
参考リンク *1
本家:
EN: The Clean Architecture | 8th Light
JP: クリーンアーキテクチャ(The Clean Architecture翻訳) | blog.tai2.net