はじめに
こんにちは。プロダクト開発部の荒川です。 これまで最年少を謳っていましたが、ついに新卒の子にその座を奪われてしまいました。とても残念です。
さて今回のテーマは、皆さんお馴染みクリーンアーキテクチャ(Clean Architecture)です。
クリーンアーキテクチャは一時期流行し、その流れに乗って私もある程度の理解はしていました。 しかし、それはあくまでも感覚的な理解であって、他人に説明や良さを語れるレベルまで自分の中で落としこめていませんでした。 そこでより具体性のあるソースコードを読み込むことで、アーキテクチャへの理解を深めたいと思います。
クリーンアーキテクチャとは?
クリーンアーキテクチャの定義や解説に関しては、ネット上にいくらでも公開されているので、このエントリでは詳しく話しません。 私自身が勉強に使った書籍やサイトを記事末尾の「参照」に掲載しているので、そちらを参考にしてみてください。
これはクリーンアーキテクチャを語るうえで有名な図です。 (明らかに言葉足らずではありますが)簡単に言うと4層に分割したレイヤ構造でアプリケーションを構築するためのアーキテクチャです。
今回のサンプル
GitHubにクリーンアーキテクチャを採用したデモアプリがいくつか公開されており、そのなかでも以下のリポジトリを参考にしたいと思います。 ソースコードはC#(ASP.NET)ですが、このエントリではC#特有の表現は避けて説明をするので特段の事前知識は必要ありません。
中身について軽く説明をしておくと、従業員、顧客の売上を管理するWebアプリケーションです。 全体的にミニマムな実装なので、顧客リストや従業員リストを確認したり、どの従業員がどの顧客にどれだけ売上を上げたかを参照・保存したりするアプリだという理解さえあればこのアプリを読み解くには十分です。
全体像を把握する
早速リポジトリを見ながらクリーンアーキテクチャの構造を理解していきましょう。
ありがたいことにこのリポジトリの /Diagrams
に全体像を見渡すための画像が含まれています。
各用語はアプリの各ディレクトリと直接対応しており、図には依存関係が示されています。
/Presentation
に含まれるのは、Webサイト(HTML, CSS, JavaScript)と対応付けられるコントローラです。
/Service
ではWebAPIを提供しています。この二つのディレクトリはクライアントとの接点になります。
次に /Application
に含まれるのは、(ここではWebとAPIから)共通して呼ばれるロジックが含まれています。
例えば売上を計上する、顧客のリストを取得する、などが該当し、一連の業務の処理手順を表現しているといえます。
/Domain
ではいわゆるエンティティが含まれています。例えば「顧客」だったり「売上」だったり、業務に関連するクラスそのものが格納されています。
/Persistance
はデータベースへのアクセスと永続化が責務です。/Application
から受け取ったデータでクエリを発行したり、永続化することが役割です。デモアプリでは、 Entity FrameworkというO/Rマッパーを利用しています。
/Infrastructure
は外部のデータソースを扱っており、このアプリでは外部のAPIに対してPOST送信を行っています。
以上が大まかな責務の解説でした。 この一枚の画像からもクリーンアーキテクチャに関する重要な知見が得られます。
- 各レイヤは責務を明確に分離していること
- 各レイヤで依存性が分離されていること
- 依存関係の流れが一方向であること
クリーンアーキテクチャの用語の整理
いまだにソースコードが出てきませんが、もう少しだけ事前知識の共有をさせてください。
アプリケーションのアーキテクチャ全体像を理解できたと思いますが、 今一度クリーンアーキテクチャ図を眺めてみると、アプリケーションで用いられている言葉と乖離があることに気づくと思います。
唐突ですがクリーンアーキテクチャと類似したオニオンアーキテクチャという手法が存在します。そのオニオンアーキテクチャの図がこれです。
ディレクトリ名にも登場する言葉(Application, Domain...)がいくつか見受けられるので、少しはデモアプリとの対応関係が掴めてきたのではないでしょうか。 実はオニオンアーキテクチャなどの優れたアーキテクチャの共通点を集約して汎用化したモデルがクリーンアーキテクチャであり、2013年にC. Martin氏が提唱しました。
今回使うデモアプリのリポジトリ名はclean-architecture-demo
とありますが、ご覧の通りオニオンアーキテクチャに近い構造と用語だと感じるので、今後はオニオンアーキテクチャの用語と図をベースに話を進めたいと思います。オニオンアーキテクチャの核心を理解すれば、クリーンアーキテクチャのエッセンスを感じることができるはずです。
ただし、注意しなければならないのは、それでもオニオンアーキテクチャとデモアプリの関係が1対1で対応付けられないということです。
例えばですが、図中のApplication ServiceとDomain Serviceに関しては、デモアプリでは/Application
として集約されているようにも見受けられます*1。アーキテクチャ設計の概念や用語をどのように使うか、どこまで厳密に区別するかは、アプリごとの設計に依ります*2。
オニオンアーキテクチャのレイヤ構造
さてオニオンアーキテクチャには、以下の4つのレイヤが存在しています。
- User Interface / Infrastructure / Tests(外側)
- Application Services
- Domain Services
- Domain Model(内側)
ここで確実に理解しておくべきことは依存の方向と処理の流れです。
依存の方向は外側から内側へと流れます。つまり外側のレイヤから内側のレイヤへの参照は許されていますが、逆は依存関係を狂わせてしまうので基本的に違反行為です。
そして処理の流れは外側から内側を通ったのち最終的には外側へと流れます。プログラムの実行パスといっても差し支えないと思います。例えばHTTPリクエストを受け取り、そのパラメータをデータベースに保存するといった典型的な処理の場合、初めにUser Interfaceから処理が始まって、Application Servicesなどの内側を通りながらInfrastructureを出口に流れていきます*3。
引用元:[DDD]ドメイン駆動 + オニオンアーキテクチャ概略 - Qiita
これらが理解できればソースコードを見る準備が整いました。オニオンアーキテクチャの用語とデモアプリのディレクトリを結び付けながら各レイヤについて解説していきます。
User Interface
外側のレイヤでクライアントとの接点を持つレイヤです。先述したようにディレクトリとしては/Presentation
と/Service
が該当します。
責務としてはプレゼンテーション層と言えるでしょう。
/Presentation
まずは顧客リストのWebサイトを表示するためのビュー(HTML)が以下の通りです。
<h2>Customers</h2> <table class="table table-striped"> <thead> <tr> <th>ID</th> <th>Name</th> </tr> </thead> <tbody> @foreach (var customer in Model) { <tr> <td>@customer.Id</td> <td>@customer.Name</td> </tr> } </tbody> </table>
独特の文法がありますがおそらく問題ないと思います。そしてこのビューに対応するコントローラが存在します。
public class CustomersController : Controller { private readonly IGetCustomersListQuery _query; public CustomersController(IGetCustomersListQuery query) { _query = query; } public ViewResult Index() { var customers = _query.Execute(); return View(customers); } }
これも雰囲気で伝わるレベルだと思いますが、顧客リストを何らかの方法で取得して、HTML上に表示する責務を持つクラスです。
ここでのポイントは、やはりIGetCustomersListQuery
というフィールドでしょう。この型はインターフェースであり、実装は/Application
のなかに含まれています。この具象クラスはインスタンス生成時にインジェクションされます。
コントローラでは顧客リストの取得を行いたいのであって、具体的なデータソースは指定する必要がありません。つまり顧客リストはデータベースから取得されるかもしれないし、キャッシュから取得されるかもしれないし、ファイルから取得されるかもしれないです。それを/Presentation
では意識する必要がないように抽象化されています。
/Service
ではHTTPリクエストを受け取ってJSONを返却しますが、ロジック自体はほぼ同様のため割愛します。ここで理解すべきは複数のクライアントから/Application
を呼び出していることです。
Application Services / Domain Services
User Interface(プレゼンテーション層)から利用される共通ロジックを提供します。 特定の業務の流れもしくはユースケースともとらえることができます。
/Application
デモアプリではそもそもApplication ServiceとDomain Serviceという言葉を使っておらず、それらに該当するクラスは/Application
にまとめて含まれています*4。
このディレクトリには、先ほどの顧客リスト取得の実装の詳細が含まれています。
public class GetCustomersListQuery : IGetCustomersListQuery { private readonly IDatabaseService _database; public GetCustomersListQuery(IDatabaseService database) { _database = database; } public List<CustomerModel> Execute() { var customers = _database.Customers .Select(p => new CustomerModel() { Id = p.Id, Name = p.Name }); return customers.ToList(); }
先ほどと似たような構成になっていますが、実装の詳細に踏み入ったことを理解してください。
ちなみにフィールドであるIDatabaseService
は/Application
に、その実装クラスであるDatabaseService
は/Persistence
に存在しています。
そしてUser Interface(プレゼンテーション層)側から見たときに、このレイヤの責務が共通ロジックを提供していることは重要です。逆に言い換えると、/Application
から見たときには、この顧客リスト取得の処理がHTMLの要素のために使われようが、API(JSON)のために使われようが、テストのために使われようが、まったく関係がないロジックである、ということです。これぞまさしく責務の分割であり、依存が分離されています。
そしてExecuteメソッドの返り値であるCustomerModel
は/Application
に存在しています。
そして、まだ登場していませんが、/Domain
に存在するCustomer
クラスを直接利用していません。
// Application public class CustomerModel { public int Id { get; set; } public string Name { get; set; } }
// Domain public class Customer : IEntity { public int Id { get; set; } public string Name { get; set; } }
上記の二つはディレクトリが異なるだけで、ほぼ同じクラスなのですが、なぜこの二つのクラスを分離するのでしょう?
一つの答えは/Application
ではドメインの整合性を担保することが責務だからです*5。
このとき、/Domain
に属するCustomer
は、状況によって不整合が生じている可能性があります。
例えば、インスタンス生成直後はすべてのフィールドがnullかもしれません。
User Interface(プレゼンテーション層)に渡した後、別の値に書き換えられてしまうかもしれません。
一方でCustomerModel
は、外側のレイヤにインターフェースとして渡すガワのような存在です。
そのインターフェースに前述したような不整合な値が渡ったらどうなるでしょうか。
/Presentation
でも/Service
でも似たようなチェックを行う必要性があり、呼び出し側が増えるにつれてコードが膨らんでいきます。
したがって、Customer
を整合性のある静的な状態で渡すため、CustomerModel
とクラスが分離されていると言えるでしょう。
Executeメソッド内でCustomerModel
へとマッピングを行っている箇所があります。ただし今回の場合はシンプルすぎてただ渡しているだけですが... *6
var customers = _database.Customers
.Select(p => new CustomerModel()
{
Id = p.Id,
Name = p.Name
});
さて、これまではいわゆる取得処理を見てきましたが、もう一つのユースケースである売上を作成する処理を見てみましょう。
public class CreateSaleCommand : ICreateSaleCommand { private readonly IDateService _dateService; private readonly IDatabaseService _database; private readonly ISaleFactory _factory; private readonly IInventoryService _inventory; public CreateSaleCommand( IDateService dateService, IDatabaseService database, ISaleFactory factory, IInventoryService inventory) { _dateService = dateService; _database = database; _factory = factory; _inventory = inventory; } public void Execute(CreateSaleModel model) { var date = _dateService.GetDate(); var customer = _database.Customers .Single(p => p.Id == model.CustomerId); var employee = _database.Employees .Single(p => p.Id == model.EmployeeId); var product = _database.Products .Single(p => p.Id == model.ProductId); var quantity = model.Quantity; var sale = _factory.Create( date, customer, employee, product, quantity); _database.Sales.Add(sale); _database.Save(); _inventory.NotifySaleOcurred(product.Id, quantity); } }
Executeメソッドにて引数であるCreateSaleModel
を利用して、顧客や製品の情報を取得して、それらを統合してデータベースに保存しています。また売上が発生したことを通知(_inventory.NotifySaleOcurred
)しているようです。この一連の流れを整合性を取りながら実行することが/Application
の責務です。
最後にもう一つクリーンアーキテクチャとは直接関係がないのですが、このデモアプリでは「データ取得」「データ更新」の責務を持つクラスが分離されていました。具体的には売上の取得処理は/Application/Sales/Query
に、売上の作成処理は/Application/Sales/Command
に置かれています。この戦略はCQRS(コマンドクエリ責務分離)と呼ばれます。詳しくはDDDで設計するならCQRSの利用を検討すべき - Qiitaを参考にされるとよいでしょう。
Domain Model
エンティティのことであり、業務を表現したモデルのクラスを提供します。
/Domain
デモアプリではModelという用語が抜けて単にDomainと表現されています。一例として挙げたCustomer
もここに定義されています。
public class Customer : IEntity { public int Id { get; set; } public string Name { get; set; } }
ドメインによってはメソッドを持っている場合もあります。また他のドメインを内部のフィールドとして持っていることもあります。
public class Sale : IEntity { private int _quantity; private decimal _totalPrice; private decimal _unitPrice; public int Id { get; set; } public DateTime Date { get; set; } public Customer Customer { get; set; } public Employee Employee { get; set; } public Product Product { get; set; } public decimal UnitPrice { get { return _unitPrice; } set { _unitPrice = value; UpdateTotalPrice(); } } public int Quantity { get { return _quantity; } set { _quantity = value; UpdateTotalPrice(); } } public decimal TotalPrice { get { return _totalPrice; } private set { _totalPrice = value; } } private void UpdateTotalPrice() { _totalPrice = _unitPrice * _quantity; } }
/Domain
は最も内部に属するクラスなので、基本ほかのレイヤを参照することはありません。言い換えると/Application
を参照したり、/Presentation
を参照したりすることはありません。あくまでも使われる側です。ここを破るとあっという間に一方向が崩れて循環参照となってしまいます。
Infrastructure
外側のレイヤであるInfrastructure
は外部との通信を責務として持っています。
/Persistence
データベースへの永続化レイヤです。これはあまり説明することもないですね。
public class DatabaseService : DbContext, IDatabaseService { public IDbSet<Customer> Customers { get; set; } public IDbSet<Employee> Employees { get; set; } public IDbSet<Product> Products { get; set; } public IDbSet<Sale> Sales { get; set; } public DatabaseService() : base("CleanArchitecture") { Database.SetInitializer(new DatabaseInitializer()); } public void Save() { this.SaveChanges(); } ...
やっとのことでデータベースにアクセスする実装クラスまでたどり着きました。これほど幾つものレイヤを重ねてデータソースへのアクセスを隠ぺい化してきました。この設計のおかげで変更に強い状態であるといえます。ここまでくるとEntity Frameworkというライブラリ(もしくはフレームワークなど)、そしてデータベース自体に依存することになります。例えば、Entity Frameworkを辞めて別のライブラリを使うことになったとしましょう。もしもInfrastructureのレイヤ以外にもEntity Frameworkに依存していた場合、ソースコードの修正やリグレッションテストの範囲が大きく広がってしまいます。ライブラリやフレームワークへの依存を上手に隠ぺい化することで影響範囲を特定のレイヤで閉じることができます。
/Infrastructure
オニオンアーキテクチャ自体の用語と被るのですが、デモアプリにおいては外部APIを呼ぶ責務を持つディレクトリです。
public class InventoryService : IInventoryService { // Note: these are hard coded to keep the demo simple private const string AddressTemplate = "http://abc123.com/inventory/products/{0}/notifysaleoccured/"; private const string JsonTemplate = "{{\"quantity\": {0}}}"; private readonly IWebClientWrapper _client; public InventoryService(IWebClientWrapper client) { _client = client; } public void NotifySaleOcurred(int productId, int quantity) { var address = string.Format(AddressTemplate, productId); var json = string.Format(JsonTemplate, quantity); _client.Post(address, json); }
実行しているもの自体は至ってシンプルで、外部のAPIに対してPOSTリクエストを投げてクライアントに通知を送っています。
データベースへの永続化(/Persistence
)と外部APIへのPOST送信(/Infrastructure
)が、同じレイヤに存在することが若干不思議に思われるかもしれません。しかしあくまでも処理の出口という観点であって、アプリケーションにとって必ずしも永続化をするだけがゴールではありません。
Tests
アプリケーションコードに対して、テストを実行するレイヤです。これまでレイヤを分割する際に、インターフェースを挟んでいたと思います。このおかげでモックを挟むことができるため、レイヤごとにテストが可能になりアプリケーションのテスタビリティが向上します。
これは/Application
の顧客リスト取得処理のテストコードで、データベースをモック化することで単体のレイヤでのテストを可能にしています。
[TestFixture] public class GetCustomersListQueryTests { private GetCustomersListQuery _query; private AutoMoqer _mocker; private Customer _customer; private const int Id = 1; private const string Name = "Customer 1"; [SetUp] public void SetUp() { _mocker = new AutoMoqer(); _customer = new Customer() { Id = Id, Name = Name }; _mocker.GetMock<IDbSet<Customer>>() .SetUpDbSet(new List<Customer> { _customer }); _mocker.GetMock<IDatabaseService>() .Setup(p => p.Customers) .Returns(_mocker.GetMock<IDbSet<Customer>>().Object); _query = _mocker.Create<GetCustomersListQuery>(); } [Test] public void TestExecuteShouldReturnListOfCustomers() { var results = _query.Execute(); var result = results.Single(); Assert.That(result.Id, Is.EqualTo(Id)); Assert.That(result.Name, Is.EqualTo(Name)); } }
ちなみにデモアプリではテストプロジェクトは分離されておらず、テスト対象のクラスと同様のパスに配置されています。この戦略は小規模のアプリなので、分離する必要性が薄いと判断されたのだと思われます。大規模になるにつれて他のディレクトリと同じように分離することになるはずですが、今回の場合は小さいので全然アリですね。
まとめ
ソースコードを参考にクリーンアーキテクチャ(オニオンアーキテクチャ)を見てきましたが、改めてこのアーキテクチャを採用することで何がよかったのでしょうか?本家サイトであるClean Coder Blogにて、クリーンアーキテクチャを採用することで以下が達成できるとの記述があります。
- フレームワークからの分離
- テスタビリティの確保
- UIからの分離
- データベースからの分離
- 外部機能(エージェント)からの分離
ソースコードと一緒に読み合せたからこそ、これらの意味を理解できるようになっているのではないでしょうか! ぜひアプリケーションにクリーンアーキテクチャを採用して、メリットを享受してみてはいかがでしょうか。
おわりに
長文駄文だったと思いますが、おつかれさまでした! 少しでもクリーンアーキテクチャについての理解が深まってもらえるとすごく嬉しいです。
もちろんこのエントリはアプリケーション開発におけるアーキテクチャの答えではありません。私なりの解釈を含んでいる箇所も多分にありますので、「この箇所は誤っている」や「こういう考えもある」というのはぜひ共有してもらえたらうれしいです。
ちなみにデモアプリですが、用語や細部は異なれどSansanのコードベースと非常に類似しているアーキテクチャだと感じました。 クリーンアーキテクチャのエッセンスがこの小さなアプリケーションに詰まっており、私もエントリを書いていて大変勉強になりました。
参照
- Clean Coder Blog
- Onion Architecture Is Interesting - DZone Java
- 実装クリーンアーキテクチャ - Qiita
- 実践クリーンアーキテクチャ │ nrslib
- オニオンアーキテクチャにておいて、ドメイン層とアプリケーション層の責務はどう違うのか[DDD] - little hands' lab
- DDDで設計するならCQRSの利用を検討すべき - Qiita
- Onion Architecture と Clean Architecture - 騒音のない世界 BLOG
- Clean Architecture 達人に学ぶソフトウェアの構造と設計【委託】 - 達人出版会