クリーンアーキテクチャをミニマルで実装可能な形にした Simple Clean Architecture という考え方


これは、ソフトウェアアーキテクチャの一つである Clean Architectureを、開発現場での実運用(特にC#, Unityでの開発)に合わせて調整した Simple Clean Architecture の解説記事です。(英語版はこちら)

なぜアーキテクチャが必要なの?

ソフトウェアは、どんな実装でもできます。あなたが個人開発をする限り、そしてあなたがコードのすべてを把握できている限り、どんな実装をするのも自由です。ただし、実際にビジネスの現場で、複数人で大規模なコードをみんなで作り上げていくときに、問題が出てきます。あなたの書いた天才的なコードは、他の人から見ると理解に時間がかかり、バグを誘発しやすい難解なものになっているかもしれません。アーキテクチャの使命は、ソフトウェアの構造にルールを課すことで、コードに一貫性をもたせ、これにより開発スピードとクオリティを上げることです。

特に Simple Clean Architectureは実際のビジネス、とくにスタートアップのような不確実性が高い中で最速で結果を出していく必要がある開発現場のため、下記をゴールとして考えて設計しています。
・複数のメンバーが参加しても、全員が似たような実装になるような規則を用いることで既読性/メンテナンス性を維持する
・複数のメンバーが参加しても、疎結合で相互依存の少ない実装にすることでお互いの変更が干渉しあわないようにする

Simple Clean Architecturetとは?

Simple Clean Architecture (SCA) は、Clean Architectureの考え方をベースに、実際の実装方法のレベルまでルールを具体化させたものです。Clean Architecture自体は「設計指針」に近いため、実際の実装の方法は1つではありません。SCAは、Clean Architectureのエッセンスを利用しながら、冗長な部分は省き、また、実装上必要になる「Dependency Injection (依存注入)」と「Reactive Programming」の考え方を足すことで「実装ルールに落とし込んだアーキテクチャ」の形になっています。

とはいえ、Clean Architectureの考え方はコンポーネントを疎結合に保つために非常に重要な基本概念です。下記のようなページを参照することをおすすめします。
https://qiita.com/kz_12/items/bc79102247b86626fc72
https://gist.github.com/mpppk/609d592f25cab9312654b39f1b357c60

また、前述の通り、SCAは Dependency Injection (DI) と Reactive Programming の考え方を取り入れています。これらはソフトウェアアーキテクチャというより、アーキテクチャを実際の実装に落としたときの実現手段(デザインパターン)の1つ、ツールのような立ち位置です。

これらも、SCA特有のものではなく、ソフトウェア構成として一般的なものですので、基礎知識を調べてみることをおすすめします。
・Dependency Injection
https://qiita.com/hshimo/items/1136087e1c6e5c5b0d9f

・Reactive Programming
イベント駆動型プログラミングが考え方としては近いです。
https://gist.github.com/staltz/868e7e9bc2a7b8c1f754
https://ninjinkun.hatenablog.com/entry/introrxja

おさらいすると、Simple Clean Architecture (SCA) とは
・Clean Architectureで概念として示されているものを、必要最小限のエッセンスを取り出して実装可能なルールに落とし込んだもの
・実装のためのツールとして、Dependency Injection(DI) と、Reactive Programming(RP)を導入している
というものになります。

Simple Clean Architectureの構成

下記の図は、Clean Architectureで調べるとまず出てくる図です。Uncle Bobの原典で使われている図です。

Clean Architectureはあくまでコード・コンポーネント間の依存関係を少なくすることでプロジェクトをきれいに保つための行動指針ですので、「この実装じゃなきゃダメ!」というものはありません。だからこそ人によって理解や実装形態が千差万別になっています。
SCAではClean ArchitectureのSOLID原則を元に、特に下記の考え方を重要と考えます。
・外側のレイヤーから1つ内側のレイヤにのみしか依存してはいけない
・依存関係は必ずインターフェイスを通じて行い、DIによって依存解決する
・内側から外側への情報の伝播はイベント駆動で行うことで、依存関係を無くす

また、Clean Architectureではいろいろな登場人物が登場しますが、SCAではその中から下記のみを取り出して使用します。
・Entity
・Usecase
・Gateway
・Presenter
・View

それぞれの特徴を見ていきましょう。

Entity

Clean Architectureでは最も中心のレイヤーにある要素です。なので、上位のレイヤーのどれにも依存関係を持ってもいけません。Entityは、「最も基本となるビジネスルールのカプセル化」を担当します。オブジェクトとして扱われる最小単位の要素や、ビジネスルールの特徴上必要になる計算処理などが含まれます。
「基本となる要素のクラスの定義をするレイヤーで、他の上位レイヤーに対して依存してはいけないんだな」と考えてもらえれば十分です。
DDD(ドメイン駆動設計)を知っている方には、「EntityはDDDにおける Entity, ValueObject, Domainserviceをまとめたもの」とも言えます。

Usecase

Clean Architectureでは中心から二番目のレイヤー、「Application Business Rules」にある要素です。「アプリケーション固有のビジネスルール(いわゆるビジネスロジック)」を記述する役割をもちます。UsecaseはEntityの操作をすることが役割です。ただし、Entityを直接操作するだけでなく、「データベースからEnityとして定義されたデータをGatewayを通じて取得し、処理する」といった処理部分もUsecaseの担当となります。そのため、UsecaseはGatewayのInterfaceに依存することができます。また、UsecaseはInterfaceを通じてPresenterから呼ばれることになります。
DDDにおけるUsecaseと同じ考え方が適応できます。こちらの記事ではUsecaseとEntity(DDDのDomainService)の切り分け方を解説されているので、参考になるかもしれません。

Gateway

Clean Architectureでは中心から三番目のレイヤー、「Interface Adapters」にある要素です。Interface Adaptersは「ビジネスルールとUIやDBなどのアプリの外の世界とをつなげる」役割をもったコンポーネントが所属するレイヤーです。Gatewayはこのうち、DBや外部ライブラリとのインタラクションを担当するコンポーネントです。
例えばデータベースからEntityで定義したデータを取得する IDBGateway というインターフェイスを定義したとします。

public interface IDBGateway{
    int GetValue(string key);
    void SetValue(string key, int value);
}

このとき、実際の実装方法としてはDBとしてMySQLを使っても、PostgreSQLを使っても、それは実装者の自由です。これらの実装方法の違いは、例えば IDBGateway を継承した MySqlGateway : IDBGateway や、 PostgreSqlGateway : IDBGateway として実装できます。重要なのは、この実体の修正が他のレイヤーに影響を与えないことです。DIがインスタンスする対象を MySQLGateway から PostgreSqlGateway に変えるだけでこの変更ができることになります。このDIの考え方は他のコンポーネント(UsecaseとPresenter)でも同じです。

Presenter

Clean Architectureでは中心から三番目のレイヤーで、「ビジネスルールと外の世界をつなげる」役割をもった「Interface Adapters」にある要素です。Gatewayと同じレイヤーに所属し、UI (View) とのインタラクションを担当します。Presenterはインターフェイスを通じてUsecaseを参照することができます。Viewからの入力をUsecaseに伝えてビジネスロジックを駆動させ、UsecaseからのEntityの更新をViewに伝えます。ここで「Viewに伝える」ための方法として、Reactive Programmingを使用します。PresenterがEntityの更新をPublishするReactive Propertyを備えることで、Viewへの依存なしにUIを更新することができます。

View

Clean Architectureでは一番外側の「Frameworks & Drivers」レイヤーに属します。UIの操作、すなわちUIを介したアクションのInputと、更新されたEntityのOutputを担当します。
ViewはPresenterにインターフェイスを介して依存することができます。ViewからPresenterへのアクションはPresenterの関数を呼ぶことで実現します。一方、Viewの更新は、ViewがPresenterが提供するReactive Propertyを購読することで実現します。Presenterの値の更新をイベントとしてコールバックで取得することで、Viewがコールバックを通して表示を更新する。という動作を実現します。これにより、ViewとPresenterが密結合になることを防ぎ、新しいViewの追加を容易にします。Viewはレイヤーの最外にいるため、他の要素に依存することはできません。

以上を図にまとめると、コンポーネント間の関係は下記のようになります。

UnityでのSimple Clean Architectureの実装

SCAは特定の開発言語、プラットフォームに限定されたアーキテクチャではありません。大規模開発でコンポーネント間を疎結合に保ちたい場合、SCAの考え方を適用することはできると考えます。
一方で、最も実践上で相性が良いのがUnityでのプロジェクトに対する使用です。Unity上でのアプリケーション開発は、アーキテクチャ無しの場合、下記のような悪夢を容易に実現できます。
・とあるMonobehaviourが別のMonobehaviourに依存し、どれかを修正したらすべて動かなくなる
・Monobehaviourへの依存関係 (変数の代入) を public や [serializefield] を使い、Inspectorから代入して行っていて、誰かの変更ですべての参照が吹っ飛んだ
・新しいオブジェクトを追加するためにシーンファイル (.unity) を複数人で編集し、gitでマージする際にコンフリクトした

これらは、Unityのエディタからの操作やMonobehaviourに依存しすぎた結果起きるものです。できるだけC#の世界でコードを整理しておき、シーンへのコンポーネントの追加やMonobehaviourの参照を少なくすることで、大規模開発での混乱を少なくすることができます。

具体的には、UnityでのSCAの実装は下記のようになります。
・SCAに基づき、コンポーネントをレイヤーごとに分ける
・Monobehaviourを継承できるのは「View と Presenter のみ」に限定する
・UniRXを使ってReactive Programmingを実現する
・Extenjectを使って依存注入し、Unityシーンをできるだけ編集せずに機能拡張を実現する

下記はコンポーネント間のデータの流れを示します。

ここでは Gatewayは割愛しています。GatewayはUnityに依存しない純粋なC#の世界のクラスのためです。重要なのは「ViewはPresenterしか参照せず、PresenterはUsecaseしか参照しない」ことです。これを実現するために、 PresenterからViewのような逆方向の情報伝播はReactive Programmingを積極的に使い、外側のレイヤーが情報をイベントドリブンに取る実装になります。

SCA for Unityの各コンポーネントにおけるルールをまとめると、下記のようになります。
・View
 ・Viewはインターフェイスを通じてPresenterに依存できる
 ・Viewは他のViewに依存できない
 ・ViewはUsecase, Gatewayに依存できない
 ・ViewはMonobihaviourを継承できる

・Presenter
 ・Presenterはインターフェイスを通じてUsecaseに依存できる
 ・PresenterはView, Gatewayに依存できない
 ・PresenterはMonobihaviourを継承できる
 ・Presenterは他のPresenterにインターフェイスを通じて依存してもよい(が、できるだけ避ける)

・Usecase
 ・Usecaseはインターフェイスを通じてGatewayに依存できる
 ・UsecaseはView, Presenterに依存できない
 ・UsecaseはMonobihaviourを継承できない
 ・Usecaseは他Usecaseにインターフェイスを通じて依存してもよい(が、できるだけ避ける)

・Gateway
 ・Gatewayはインターフェイスを通じGatewayに依存できる
 ・GatewayはView, Presenter, Usecaseに依存できない
 ・GatewayはMonobihaviourを継承できない
 ・Gatewayは他Gatewayにインターフェイスを通じて依存してもよい(が、できるだけ避ける)

SCA for Unityの実装サンプルをgithubで公開しているので、参考にしてみてください。

他のアーキテクチャとの比較

「なんでMVPやMVVMではダメで、SCAを推奨するの?」に対して回答します。それは「MVPやMVVMだとモデルが肥大化するから」です。MVPやMVVMが悪いと言っている訳ではありません。ソフトウェアアーキテクチャは実際のアプリケーションに最適なものを使うべきなので、利点が欠点を上回ればそれで良いと思います。一方で、MVPやMVVMは、前述の通り、役割をきれいに分類していくとビジネスロジック部、つまりモデルのコードサイズが大きくなっていきます。モデルの全体像が把握できているうちは問題ありませんが、プロジェクトが大きくなっていくと「Model間での循環参照、密結合」が顕在化してきます。SCAではモデルに相当する部分はGateway、Usecase、Entityに分かれているので、この「モデルの肥大化」が起きにくなっている点が違いです。言い換えると、「MVPやMVVMのモデルの定義をもう少し細分化したもの」がSCAになります。
「MVPだとコードが整理しきれないけど、Clean Architectureを真面目にやると、RepositoryとかUsecase input portとか、Data Accessとか、細かすぎて逆に複雑になっちゃうよ~」という人達に向けた、「Clean Architectureを簡略化して実装ルールに落とし込んだアーキテクチャ」が Simple Clean Arcitectureになります。