2018年9月17日 URL 変更
Qiita 版
Qiita に CUI や GUI 向けのクリーンアーキテクチャの記事を書きました。
ボブおじさんのクラス図を模したものです。
Web とはまた異なった実装になるので、もしよければ合わせてご参照ください。
https://qiita.com/nrslib/items/a5f902c4defc83bd46b8
さらに PHP の Laravel 版も作ってみました。
https://qiita.com/nrslib/items/aa49d10dd2bcb3110f22
はじめに
クリーンアーキテクチャ(Clean Architecture)をご存知でしょうか。
Uncle Bob こと Rovert C. Martin が提唱した設計思想です。
発端となった記事はこちらです。
https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html
またこのクリーンアーキテクチャは書籍も出ており、日本語訳もあります。
これらの文献を参照すればクリーンアーキテクチャの思想などはわかります。
クリーンアーキテクチャはソフトウェアを作る上で守るべき事柄や実装の方針について語られており、模倣すべきものであると私は感じました。
しかし困ったことに、この設計思想を模したサンプルコードがありません。
その結果、サンプルコードがないために書籍や記事の内容がすんなり頭に入ってこないのです。
「とてもいいことを言っているとは思うけど実践の仕方がわからないなぁ。このアーキテクチャはまぁ参考程度かな」と考えてしまうのも止む無しと思います。
実際に私がそうでした。
当時は記事しかなくその記事を頼りに何度かスクラップビルドを繰り返しました。
最終的に Uncle Bob の提唱するクラス図に合致する形にたどり着くことができました。
そして思いました。
このコードが最初からあればよかったのに……。
最初からあれば記事の内容もすんなり頭に入ってきたのに……。
そんないきさつで、クリーンアーキテクチャの実装例を世に出したく新規プロダクトで採用したので発表したりしました。
Link: https://speakerdeck.com/nrslib/cleanarchitecture
(一応動画も https://www.youtube.com/watch?v=btJPK3TaJMg&feature=youtu.be&t=10830)
折角行った発表でしたから、それを基に文章にしてみようと思いこの記事を作りました。
ソース
https://github.com/nrslib/PracticeOfCleanArchitecture
エントリポイントをいくつか用意しています。
- ConsoleApp : CLI プログラム
- Domain.Tests : テストプロジェクト
- WebApplication : MVC フレームワーク (ASP.NET Core)
- WindowsFormsApp : GUI アプリケーション (Windows Form)
ロジックについては全く同じものを使っています。
レイヤー
すぐにでも実装を見ていきたいところですが、まずはクリーンアーキテクチャの図をご覧ください。
色々な単語が記載されており、どこから見ていいか迷ってしまうところですが、まずはレイヤーについて見ていくのがわかりやすいでしょう。
図では円がレイヤーを表しており、レイヤーの名前は右上に注釈が伸びている部分です。
一つずつ簡単に説明をします。今段階ではイメージが沸かないと思われるのでさらっと流し読みでも構いと思います。
Enterprise Business Rules
黄色のレイヤーは Enterprise Bussiness Rules とされており、ビジネスロジックを表現するレイヤーです。
ここはトランザクションスクリプトで構築されていても構いませんし、ドメイン駆動設計で構築されていても構いません。
ビジネスロジックがここに所属するということを覚えておけばよいでしょう。
Application Business Rules
赤いレイヤーは Application Bussiness Rules です。
このレイヤーは API のようにソフトウェアが何ができるのか、を表現します。
Enterprise Bussiness Rules がビジネスルールの表現であったのに対して、それらのビジネスルールを組み合わせてユースケースを達成します。
Interface Adapters
緑色レイヤーの Interface Adapters は入力、永続化、出力が所属します。
一般的な MVC フレームワークはここに内包されますし、単体テストクラスもこのレイヤーの住人でしょう。
Frameworks & Drivers
ここは Web フレームワークやデータベースのドライバーなどのギークなコードが所属します。
フロントエンドの UI などもここに所属します。
矢印の方向
円の左側に矢印がいくつかあります。
この矢印は依存の方向性を表しています。
依存とは具象クラスに対する依存のことで、例えば次のユーザ作成処理クラスは User Repository というクラスに依存しています(UserRepository がどういったものかについては理解していなくて問題ないです)。
// ユーザ作成処理クラス | |
public class CreateUser { | |
private readonly UserRepository userRepository; | |
public CreateUser(UserRepository userRepository){ | |
this.userRepository = userRepository; | |
} | |
public void Execute(string name){ | |
var user = new User(name); | |
this.userRepository.Save(user); | |
} | |
} | |
public class UserRepository{ | |
public void Save(User user){ | |
// User の保存処理 | |
} | |
} |
このユーザ作成処理クラスが User Repository クラスに依存しないようにする場合はインターフェースや抽象クラスなどを利用します。
// ユーザ作成処理クラス | |
public class CreateUser { | |
private readonly IUserRepository userRepository; | |
public CreateUser(IUserRepository userRepository){ | |
this.userRepository = userRepository; | |
} | |
public void Execute(string name){ | |
var user = new User(name); | |
this.userRepository.Save(user); | |
} | |
} | |
public interface IUserRepository{ | |
void Save(User user); | |
} |
ユーザ作成処理クラスは IUserRepository という抽象に依存していますが、具象クラスには依存していません。
「依存していない」と表現するときはつまり「(具象クラスに)依存していない」という意味なのです。
具象クラスに依存しないようにすると、処理はそのままに保存媒体を変更することができるようになります。
矢印の図に戻りますが、これは「外側の層は内側の層に依存してもよいが内側の層は外側の層に依存してはいけない」ということを表しています。
とにかくやってみよう
図だけでイメージは湧かないと思います。
「ユーザを作成する」というユースケースに従って処理を作る過程を体験して理解へと繋げてみましょう。
最初は Web サービスを想定して作ってみます(もちろんクリーンアーキテクチャは一般的な GUI にも適用できます)。
登場人物
図には書かれているけれども、まだ説明していないものが多くあります。
その中で最低限の処理を作る上で必要な登場人物を紹介を交えながら実装していきます。
登場人物は以下の四つです。
- UseCase
- Repository
- Interactor
- Controller
※Presenter についてはこの後解説します。
UseCase
UseCase は図では UseCases と記述されており Application Bussiness Rules レイヤーに所属します。
複数形なのは UseCase が一つではないことを示しています。
システムにはいくつものユースケースが存在しています。
今回作ろうとしている「ユーザ登録処理」はそのユースケースのうちの一つです。以後、単数形と複数形の違いについては同様の理由です。
UseCase では単一のユースケースを表現します(ややこしい)。
あくまでアプリケーションとして何が出来るのかのみを表現するだけなので、実装は持ちません。
ユーザを登録するという UseCase は次のようになります。
public interface IUserCreateUseCase { | |
UserCreateUseCaseResponse Handle(UserCreateUseCaseRequest request); | |
} |
ユーザを登録するとき、おそらくユーザ名を登録する必要があるでしょう。
つまりリクエストパラメータが必要です。
また作成したユーザを識別するために ID が必要になるときもあるでしょう。
その場合はレスポンスパラメータも必要です。
// ユーザ作成リクエスト | |
public class UserCreateRequest { | |
public UserCreateRequest(string userName){ | |
UserName = userName; | |
} | |
public string UserName { get; } | |
} | |
// ユーザ作成レスポンス | |
public class UserCreateResponse { | |
public UserCreateResponse(string userId){ | |
UserId = userId; | |
} | |
// 作成したユーザの ID | |
public string UserId { get; } | |
} |
リクエストやレスポンスは DTO (Data Transfer Object) として用意しましょう。
基本的に DTO で利用する値の型はプリミティブなものやプレーンな型で構成することをお勧めしますが、列挙型や値オブジェクトなどを利用したいこともあるかと思います。
public class ChangePermitRequest { | |
public ChangePermitRequest(string userId, Role role){ | |
UserId = userId; | |
Role = role; | |
} | |
public string UserId { get; } | |
public Role Role { get; } // <- このような enum 等 | |
} |
その場合は Application Bussiness Rules より内側のレイヤーにある Enterprise Bussiness Rules レイヤーの列挙型や値オブジェクトであれば参照しても問題ありません。
もし Application Bussiness Rules より外側のレイヤーにある型を含めてしまうと Application Bussiness Rules が外側に向かって依存の矢印を伸ばしてしまうことになります。
これはルールに反してしまい、外側のレイヤーで変化が起きると内側にまで修正が広がってしまいます。必ず避けましょう。
依存して良いのは内側のレイヤーのみです。
Repository
Repository は Interface Adapter のレイヤーに所属します。
図中では GateWays にあたります。
リポジトリパターンで知られ、特定のモデルのデータ永続化について抽象化したものです。
ユーザというモデルの永続化について抽象化した場合は次のようになります。
public interface IUserRespoitory{ | |
User FindByUserName(string userId); | |
void Save(User user); | |
} |
mysql を対象としてリポジトリを実装すると次のようになります。
public class UserRepository : IUserRepository { | |
public User FindByUserName(string username) { | |
using (var con = new MySqlConnection(Config.ConnectionString)) { | |
con.Open(); | |
using (var com = con.CreateCommand()) { | |
com.CommandText = "SELECT * FROM t_user WHERE username = @username"; | |
com.Parameters.Add(new MySqlParameter("@username", username)); | |
var reader = com.ExecuteReader(); | |
if (reader.Read()) { | |
var id = reader["id"] as string; | |
return new User( | |
id, | |
username | |
); | |
} else { | |
return null; | |
} | |
} | |
} | |
} | |
public void Save(User user) { | |
using (var con = new MySqlConnection(Config.ConnectionString)) { | |
con.Open(); | |
bool isExist; | |
using (var com = con.CreateCommand()) { | |
com.CommandText = "SELECT * FROM t_user WHERE id = @id"; | |
com.Parameters.Add(new MySqlParameter("@id", user.Id.Value)); | |
var reader = com.ExecuteReader(); | |
isExist = reader.Read(); | |
} | |
using (var command = con.CreateCommand()) { | |
command.CommandText = isExist | |
? "UPDATE t_user SET username = @username, firstname = @firstname, familyname = @familyname WHERE id = @id" | |
: "INSERT INTO t_user VALUES(@id, @username, @firstname, @familyname)"; | |
command.Parameters.Add(new MySqlParameter("@id", user.Id.Value)); | |
command.Parameters.Add(new MySqlParameter("@username", user.UserName.Value)); | |
command.Parameters.Add(new MySqlParameter("@firstname", user.Name.FirstName)); | |
command.Parameters.Add(new MySqlParameter("@familyname", user.Name.FamilyName)); | |
command.ExecuteNonQuery(); | |
} | |
} | |
} | |
} |
Interactor
UseCase を実装したクラスが Interactor です。
Interactor は少しわかりづらい場所にありますが右下にあります。
いわゆるビジネスロジックにあたる Enterprise Bussiness Rule に所属するオブジェクトを協調させユースケースを達成します。
ドメイン駆動設計ではアプリケーションサービスがこの Interactor に相当します。
リポジトリの interface をコンストラクタで受け取ることにより Interactor が GateWays (UserRepository) に依存しないようにします。
public class UserCreateInteractor : IUserCreateUseCase { | |
private readonly IUserRepository userRepository; | |
public UserCreateInteractor(IUserRepository userRepository){ | |
this.userRepository = userRepository; | |
} | |
public UserCreateResponse Handle(UserCreateRequest request){ | |
var username = request.UserName; | |
var duplicateUser = userRepository.FindByUserName(username); | |
if(duplicateUser != null){ | |
throw new Exception("duplicated"); | |
} | |
var user = new User(username); | |
userRepository.Save(user); | |
return new UserCreateResponse(user.Id); | |
} | |
} |
Controller
Controller はユーザの入力を解釈し、UseCase に伝える役割です。
ビジネスロジックは存在させません。もちろんモデルも扱いません。
テレビのリモートコントローラやゲームのコントローラなどと同じです。
テレビのリモートコントローラはボタンを押したという情報をテレビへの信号に変換しテレビに送ります。
ゲーム機のコントローラはボタンを押したという情報をゲーム機へ伝えます。
コントローラはそれらと同じように、ユーザの入力をユースケースのためのデータに変換しユースケースに伝えます。
今回の例は Web サービスにするので MVC フレームワークの Controller をコントローラとして実装してみます。
public class UserController : Controller { | |
private readonly IUserCreateUseCase usecase; | |
public UserController(IUserCreateUseCase usecase){ | |
this.usecase = usecase; | |
} | |
public IActionResult Create(UserCreateRequestViewModel viewModel){ | |
var userName = viewModel.UserName; | |
var request = new UserCreateRequest(userName); | |
var response = usecase.Handle(request); | |
var result = new UserCreateResponseViewModel(response.UserId); | |
return View(result); | |
} | |
} |
コンストラクタに渡されるものは
ここまで紹介したクラス群には一つだけ共通点があります。
コンストラクタでオブジェクトを受け取ってはいるものの、その実態(具象クラス)については記述されていません。
コンストラクタに渡される IUserRepository や IUserCreateUseCase は果たしてどの具象クラスが渡されるのでしょうか。
この interface にどの具象クラスを渡すかを設定する一般的な方法の一つに DIContainer というものがあります。
DIContainer は次のようなことを可能にします。
var serviceCollection = new ServiceCollection(); | |
// IUserRepository が要求されたら UserRepository を渡す | |
serviceCollection.AddTransient<IUserRepository, UserRepository>(); | |
// UserCreateInteractor をコンストラクタで UserCreateInteractor を生成して渡す | |
serviceCollection.AddTransient<IUserCreateUsecase, UserCreateInteractor>(); | |
var provider = serviceCollection.BuildServiceProvider(); | |
// IUserCreateUseCase に登録されているインスタンスを取得(UserInteractor が取得される) | |
var interactor = provider.GetService<IUserCreateUseCase>(); |
MVC フレームワークではこれを利用して、コントローラで指定された抽象クラスに具象クラスを渡すことができます。
以下は ASP.net Core の DI 設定です。
public class Startup{ | |
public void ConfigureServices(IServiceCollection services) | |
{ | |
services.AddMvc(); | |
serviceCollection.AddTransient<IUserRepository, UserRepository>(); | |
serviceCollection.AddTransient<IUserCreateUsecase, UserCreateInteractor>(); | |
} | |
} |
また IUserRepository を要求するコンストラクタ(UserCreateInteractor)には UserRepository が代入されます。
処理の流れ
準備が整いました。
Controller, UseCase, Interactor, Repository の四つを組み合わせたときの処理の流れを追ってみましょう。
まずユーザがシステムを利用するとその入力は Controller に伝えられます。
Controller では入力値を解釈して UseCase が必要とする値に変換を行います。
変換されたリクエストはそのまま UseCase に引き渡されます。
public class UserController : Controller { | |
private readonly IUserCreateUseCase usecase; | |
public UserController(IUserCreateUseCase usecase){ | |
this.usecase = usecase; | |
} | |
public IActionResult Create(UserCreateRequestViewModel viewModel){ | |
var userName = viewModel.UserName; | |
var request = new UserCreateRequest(userName); | |
var response = usecase.Handle(request); // UseCase に渡されます | |
var result = new UserCreateResponseViewModel(response.UserId); | |
return View(result); | |
} | |
} |
このとき、IUserCreateUseCase は UserCreateInteractor が代入されているので、UserCreateInteractor.Handle に処理が落ちていきます。
public class UserCreateInteractor : IUserCreateUseCase { | |
private readonly IUserRepository userRepository; | |
public UserCreateInteractor(IUserRepository userRepository){ | |
this.userRepository = userRepository; | |
} | |
public UserCreateResponse Handle(UserCreateRequest request){ // ← ここに処理が流れてくる | |
var username = request.UserName; | |
var duplicateUser = userRepository.FindByUserName(username); | |
if(duplicateUser != null){ | |
throw new Exception("duplicated"); | |
} | |
var user = new User(username); | |
userRepository.Save(user); | |
return new UserCreateResponse(user.Id); | |
} | |
} |
その後処理は userRepository.FindByUserName まで流れます。
public class UserCreateInteractor : IUserCreateUseCase { | |
public UserCreateResponse Handle(UserCreateRequest request){ | |
var username = request.UserName; | |
var duplicateUser = userRepository.FindByUserName(username); // ← リポジトリのメソッドが呼ばれる | |
if(duplicateUser != null){ | |
throw new Exception("duplicated"); | |
} | |
var user = new User(username); | |
userRepository.Save(user); | |
return new UserCreateResponse(user.Id); | |
} | |
} |
userRepository は IUserRepository が型ですが、こちらも DI により UserRepository が代入されていますので UserRepository.FindByUserName に処理が移ります。
public class UserRepository : IUserRepository { | |
public User FindByUserName(string username) { // ← 処理がここに流れてくる | |
using (var con = new MySqlConnection(Config.ConnectionString)) { | |
con.Open(); | |
using (var com = con.CreateCommand()) { | |
com.CommandText = "SELECT * FROM t_user WHERE username = @username"; | |
com.Parameters.Add(new MySqlParameter("@username", username)); | |
var reader = com.ExecuteReader(); | |
if (reader.Read()) { | |
var id = reader["id"] as string; | |
return new User( | |
id, | |
username | |
); | |
} else { | |
return null; | |
} | |
} | |
} | |
} | |
} |
結果としてデータベースからデータを読み取り、ユースケースが達成されます。
何が嬉しいか
ロジックが疎結合になります。
特定のインフラストラクチャに依存しないようにロジックを記述できます。
つまりテストができます。
フロントエンドのテスト
例えばバックエンドが出来上がっていないとき、バックエンドが完成するまで待つのでしょうか。
勿論他にやるべき作業があるのであればそれで構いません。
しかし作業を追い越したときバックエンドが完成しないからといって手持無沙汰になるのは少々勿体ないように思えます。
そんなときはフロント開発用のテスト用 Interactor を作れば解決します。
public class MockUserCreateInteractor : IUserCreateUseCase { | |
private static int id; | |
public UserCreateResponse Handle(UserCreateRequest request){ | |
var currentId = id++; | |
return new UserCreateResponse(currentId.ToString()); | |
} | |
} |
またそれ以外にもエラーのテストなども容易になります。
発生させるのが難しいエラーというのは世の中にはいくつもあると思います。
そういったエラーに対するフロントのハンドリングをしたものの、整合性のあるデータの準備が難しくテストをせずにリリースしてしまうようなこともあるかもしれません。
そういったときにもこの MockUserCreateInteractor で好きなデータを返却すればよいだけなので、エラーを表す Response を戻すことでテスト可能になります。
バックエンドのテスト
リポジトリが interface になっているのでデータベースに依存せずにビジネスロジックをテストできます。
データベースにいちいちデータを用意して開発するのはとても面倒ですので開発中はメモリ上で動作するリポジトリを利用して Interactor を組み立てます。
public class InMemoryUserRepository : IUserRepository { | |
private readonly Dictionary<string, User> data = new Dictionary<string, User>(); | |
public User FindByUserName(string username){ | |
var target = data.FirstOrDefault(x => x.UserName == username); | |
if(target != null){ | |
return target.Value; | |
}else{ | |
return null; | |
} | |
} | |
public void Save(User user){ | |
data[user.UserId] = user; | |
} | |
} |
開発中以外にも例えば「ロジック上ハンドリングはしているが到底起こりえないデータ」や「そもそも整合性がおかしい」ときのフェイルセーフの処理をテストができるようになります。
if 文を用意したはよいけどテストせずにリリースしたことはありませんか。
もしその経験があるならこの仕組みを活用すればテストをすることができます。
もうリリース直前に「本当に動くかな」と怯える必要はないのです。
Presenter
ところで図には存在するけど触れていない要素があります。
こちらの Presenter です。
これに敢えて触れなかった理由は MVC フレームワークとの相性が悪いからです。
表示のためのオブジェクト
Presenter は表示を司ります。
テレビゲームを思い浮かべてみましょう。
ユーザはゲームのコントローラで入力します。その入力されたデータがゲーム機で処理され、その結果がテレビに表示されます。
ゲームコントローラが Controller、ゲーム機が Interactor、テレビが Presenter です。
わかったようなわからないような例え話でイメージをつけたところで実際のコードで表現してみましょう。
例えば途中経過の進捗を表示したいときを例にします。
現在のコードではユーザ登録処理はその進捗状況を伝える術がありません。
途中経過の進捗状況を伝える場合は次のような Presenter を用意します。
public interface IUserCreatePresenter{ | |
void Progress(int percentage); | |
void Complete(UserCreateResponse response); | |
} |
この Presenter を利用した Interactor は次のようになります。
public class UserCreateInteractor : IUserCreateUseCase { | |
private readonly IUserRepository userRepository; | |
private readonly IUserCreatePresenter presenter; | |
public UserCreateInteractor( | |
IUserRepository userRepository, | |
IUserCreatePresenter presenter | |
){ | |
this.userRepository = userRepository; | |
this.presenter = presenter; | |
} | |
public void Handle(UserCreateRequest request){ | |
presenter.Progress(10); // 10% 経過 | |
var username = request.UserName; | |
var duplicateUser = userRepository.FindByUserName(username); | |
presenter.Progress(30); // 30% 経過 | |
if(duplicateUser != null){ | |
throw new Exception("duplicated"); | |
} | |
presenter.Progress(50); // 50% 経過 | |
var user = new User(username); | |
presenter.Progress(80); // 80% 経過 | |
userRepository.Save(user); | |
presenter.Complete(new UserCreateResponse(user.Id)); | |
} | |
} |
この Interactor をテストをしてみましょう。
テストをする場合にはリッチな UI は必要なく、コンソールでデータ表示をすれば十分に事足ります。
public class ConsoleUserCreatePresneter : IUserCreatePresenter{ | |
public void Progress(int percentage){ | |
Console.WriteLine("進捗 " + percentage + "% です"); | |
} | |
public void Complete(UserCreateResponse response){ | |
Console.WriteLine("完了しました"); | |
Console.WriteLine("作成されたユーザの ID は " + response.UserId + " です"); | |
} | |
} |
このクラスを利用したテストは次の通りです。
var repository = new UserRepository(); | |
var presenter = new ConsolePresenter(); | |
var interactor = new UserCreateInteractor(repository, presenter); | |
var request = new UserCreateRequest("TestUser"); | |
interactor.Handle(request); |
表示先を変更する場合は Presenter を実装して DI すればロジックに影響することなく表示先を変更することができます。
しかし、この Presenter を使うパターンを MVC フレームワークに適用してみると上手くいきません。
MVC フレームワークはリクエストに対して常にレスポンスを期待するため、レスポンスをプールして取りに行けるようにする仕組みを作らなくてはなりません。
public class UserController : Controller { | |
private readonly IUserCreateUseCase usecase; | |
public UserController(IUserCreateUseCase usecase){ | |
this.usecase = usecase; | |
} | |
public IActionResult Create(UserCreateRequestViewModel viewModel){ | |
var userName = viewModel.UserName; | |
var request = new UserCreateRequest(userName); | |
usecase.Handle(request); // 戻り値が void | |
var response = ??? // Presenter がどこかにレスポンスをプールしてそれを取りに行く? | |
var result = new UserCreateResponseViewModel(response.UserId); | |
return View(result); | |
} | |
} |
もし MVC フレームワークで利用しつつも、別媒体で実行するとき用に Presenter を利用したい場合やフレームワークに捉われないようにしたい場合は Presenter を利用しながら戻り値も返却するというのも手の一つです。
public class UserCreateInteractor : IUserCreateUseCase { | |
private readonly IUserRepository userRepository; | |
private readonly IUserCreatePresenter presenter; | |
public UserCreateInteractor( | |
IUserRepository userRepository, | |
IUserCreatePresenter presenter | |
){ | |
this.userRepository = userRepository; | |
this.presenter = presenter; | |
} | |
public UserCreateResponse Handle(UserCreateRequest request){ | |
presenter.Progress(10); // 10% 経過 | |
var username = request.UserName; | |
var duplicateUser = userRepository.FindByUserName(username); | |
presenter.Progress(30); // 30% 経過 | |
if(duplicateUser != null){ | |
throw new Exception("duplicated"); | |
} | |
presenter.Progress(50); // 50% 経過 | |
var user = new User(username); | |
presenter.Progress(80); // 80% 経過 | |
userRepository.Save(user); | |
var response = new UserCreateResponse(user.Id); | |
presenter.Complete(response); | |
return response; // 同じレスポンスを | |
} | |
} |
もちろん MVC フレームワークではなく WebSocket などの非同期でサーバープッシュするような仕組みで動作するようなシステムであれば相性は抜群です。
Flow of control
右下にあった Flow of Control という図はこの Presenter を利用したときの動作を表しています。
Flow of control を確認するために単体テストプログラムを記述してみましょう。
まずは Presenter を用意します。
単体テスト用なので Presenter は Interactor からのデータを保存しておくと後々のチェックに役立つでしょう。
public class UserCreateCollector : IUserCreatePresenter | |
{ | |
public List<int> Percentages { get; } = new List<int>(); | |
public UserCreateResponse Response { get; private set; } | |
public void Progress(int percentage) { | |
Percentages.Add(percentage); | |
} | |
public void Complete(UserCreateResponse response) { | |
Response = response; | |
} | |
} |
このデータ収集クラスを用いて単体テストを記述すると次のようになります。
[TestClass] | |
public class UserCreateInteractorTest | |
{ | |
[TestMethod] | |
public void TestCreateUser() { | |
var repository = new InMemoryUserRepository(); | |
var presenter = new UserCreateCollector(); | |
var interactor = CreateInteractor(repository, presenter); | |
var request = new UserCreateRequest("TestUser"); | |
interactor.Handle(request); | |
var expectedPercentages = new List<int> { | |
10, | |
30, | |
50, | |
80 | |
}; | |
Assert.IsTrue(expectedPercentages.SequenceEqual(presenter.Percentages)); | |
Assert.IsNotNull(presenter.Response); | |
Assert.IsNotNull(presenter.Response.UserId); | |
var inserted = repository.FindByUserName("TestUser"); | |
Assert.IsNotNull(inserted); | |
} | |
private IUserCreateUseCase CreateInteractor( | |
IUserRepository repository, | |
IUserCreatePresenter presenter | |
) { | |
return new UserCreateInteractor(repository, presenter); | |
} | |
} |
この UserCreateInteractorTest はある種の Controller です。
あらかじめ決められている「入力」を Interactor に伝えています。
このプログラムの処理を追うと Frow of Control の図に沿っているのがわかるので順番に見ていきましょう。
まず、各種インスタンスが生成され、処理は interactor.Handle(request) に流れます。
この interactor は IUserCreateUseCase ですので IUserCreateUseCase.Handle に処理が引き渡されます。
[TestMethod] | |
public void TestCreateUser() { | |
var repository = new InMemoryUserRepository(); | |
var presenter = new UserCreateCollector(); | |
var interactor = CreateInteractor(repository, presenter); | |
var request = new UserCreateRequest("TestUser"); | |
interactor.Handle(request); // interactor は IUserCreateUseCase なので IUserCreateUseCase.Handle に処理が渡される |
public interface IUserCreateUseCase { | |
UserCreateUseCaseResponse Handle(UserCreateUseCaseRequest request); // ← このメソッドが呼ばれる | |
} |
interactor は IUserCreateUseCase ですがその実態は UserCreateInteractor ですので、移譲された処理は UserCreateInteractor.Handle に流れていきます。
public class UserCreateInteractor : IUserCreateUseCase { | |
private readonly IUserRepository userRepository; | |
private readonly IUserCreatePresenter presenter; | |
public UserCreateInteractor( | |
IUserRepository userRepository, // ← InMemoryUserRepository が実態 | |
IUserCreatePresenter presenter // UserCreateCollector が実態 | |
) { | |
this.userRepository = userRepository; | |
this.presenter = presenter; | |
} | |
public UserCreateResponse Handle(UserCreateRequest request) { // ← 処理はここに流れてきた | |
presenter.Progress(10); // 10% 経過 | |
/* | |
* 省略 | |
*/ |
UserCreateInteractor.Handle メソッドは次の行で presenter の Progress メソッドを呼び出しています。これにより IUserCreatePresenter.Progress メソッドに処理が流れます。
public class UserCreateInteractor : IUserCreateUseCase { | |
private readonly IUserRepository userRepository; | |
private readonly IUserCreatePresenter presenter; | |
public UserCreateInteractor( | |
IUserRepository userRepository, | |
IUserCreatePresenter presenter | |
) { | |
this.userRepository = userRepository; | |
this.presenter = presenter; | |
} | |
public UserCreateResponse Handle(UserCreateRequest request) { | |
presenter.Progress(10); // ← IUserCreatePresenter.Progress メソッドが呼ばれる |
public interface IUserCreatePresenter { | |
void Progress(int percentage); // ← このメソッドが呼ばれる | |
void Complete(UserCreateResponse response); | |
} |
IUserCreatePresenter の実態は UserCreateCollector ですので UserCreateCollector の Progress メソッドが呼ばれます。
public class UserCreateCollector : IUserCreatePresenter | |
{ | |
public List<int> Percentages { get; } = new List<int>(); | |
public UserCreateResponse Response { get; private set; } | |
public void Progress(int percentage) { // ← このメソッドが呼ばれる | |
Percentages.Add(percentage); | |
} | |
public void Complete(UserCreateResponse response) { | |
Response = response; | |
} | |
} |
これが一連の流れです。
図に表すと次のようになります。
元画像はこちらです。
<I> は interface を表し、白抜きの矢印は URL の汎化を表し、矢印は依存を表しています。
Flow of control の矢印は処理の流れ、つまり実際のプログラムの流れを表していたのです。
UseCase 沢山問題
ユーザ登録は一つのユースケースをクラスにしました。
ユーザに関するユースケースはそれ以外にもありそうです。
登録があるのであれば参照、変更、削除といった処理もあるでしょう。
それらのユースケースを一つずつ interface にした場合 MVC フレームワークの場合コントローラのコンストラクタが冗長な記述になります。
public class UserController : Controller { | |
private readonly IUserCreateUseCase UserCreateUsecase; | |
private readonly IGetUserUSeCase getUserUseCase; | |
private readonly IUpdateUserUseCase updateUserUsecase; | |
private readonly IDeleteUserUseCase deleteUserUsecase; | |
public UserController( | |
IUserCreateUseCase UserCreateUsecase, | |
IGetUserUSeCase getUserUseCase, | |
IUpdateUserUseCase updateUserUsecase, | |
IDeleteUserUseCase deleteUserUsecase | |
){ | |
this.UserCreateUsecase = UserCreateUsecase; | |
this.getUserUseCase = getUserUseCase; | |
this.updateUserUsecase = updateUserUsecase; | |
this.deleteUserUsecase = deleteUserUsecase; | |
} | |
/* 省略 */ | |
} |
これはアーキテクチャを採用するにあたって避けることのできない冗長さなのでしょうか。
これに対する私の回答です。
Bus パターン
Bus をご存知でしょうか。
MessageBus 等で知られるパターンです。
まずはこの図を見てください。
Request を Bus というものに渡すと Response を返却するという図です。
Bus パターンはこのように、「渡したリクエストに対応したレスポンス」を Bus が返してくれる仕組みです。
Bus の内実をお見せすると次のような図になっています。
Interactor がひしめき合っている状態です。
ここにリクエストが渡されると Bus はそのリクエストを UserCreateInteractor に渡して処理を移譲し返却すべきレスポンスを手に入れ返却します。
この Bus を利用すると UserController はこのようなコードになります。
public class UserController : Controller { | |
private readonly UseCaseBus bus; | |
public UserController(UseCaseBus bus){ | |
this.bus = bus; | |
} | |
public IActionResult Create(UserCreateRequestViewModel viewModel){ | |
var username = viewModel.UserName; | |
var request = new UserCreateRequest(username); | |
var response = bus.Handle(request); | |
var result = new UserCreateResponseViewModel(response.Id); | |
return View(result); | |
} | |
} |
このようにしてみると、リクエストを作るという行為はある意味「特定のレスポンスを受け取りたい」という意思表示です。
つまり、リクエストさえ受け取れればその処理系統はテストでもプロダクトでもなんでもよいのです。
もし新たな処理が増えたとしても、リクエストを Bus に渡せばレスポンスを受け取ることができるのでコンストラクタなどを修正する必要もなく機能拡張することができます。
public class UserController : Controller { | |
private readonly UseCaseBus bus; | |
public UserController(UseCaseBus bus){ | |
this.bus = bus; | |
} | |
public IActionResult Create(UserCreateRequestViewModel viewModel){ | |
var username = viewModel.UserName; | |
var request = new UserCreateRequest(username); | |
var response = bus.Handle(request); | |
var result = new UserCreateResponseViewModel(response.Id); | |
return View(result); | |
} | |
// 新たな処理を追加してもここ以外に影響がない | |
public IActionResult GetDetail(UserGetDetailRequestViewModel viewModel){ | |
var id = viewModel.Id; | |
var request = new UserGetDetailRequest(id); | |
var response = bus.Handle(request); | |
var result = new UserGetDetailResponseViewModel(response.UserName); | |
return View(result); | |
} | |
} |
テストとプロダクトの切り替え
テストの処理系とプロダクトの処理系は Bus に選ばせます。
といっても自動で選んでくれるわけではなく、環境に応じて設定する必要はあります。
xml や json などのファイルを利用してもよいのですし DI を設定する要領でプログラムに書いても構いません。
以下はローカルで動作するテスト環境用の設定です。
public class TestDILauncher : IDILauncher { | |
public void Launch(IServiceCollection services) { | |
services.AddSingleton<IUserRepository, InMemoryUserRepository>(); | |
var busBuilder = new SyncUseCaseBusBuilder(services); | |
busBuilder.RegisterUseCase<CreateUserRequest, MockCreateUserInteractor>(); | |
var usecaseBus = busBuilder.Build(); | |
services.AddSingleton(usecaseBus); | |
} | |
} |
Bus への設定は busBuilder.RegisterUseCase です。
見てわかるようにリクエストに対しての処理系を登録しているだけですね。
次はプロダクト用の設定です。
public class ProductDILauncher : IDILauncher { | |
public void Launch(IServiceCollection services) { | |
services.AddTransient<IUserRepository, UserRepository>(); | |
var busBuilder = new SyncUseCaseBusBuilder(services); | |
busBuilder.RegisterUseCase<CreateUserRequest, CreateUserInteractor>(); | |
var usecaseBus = busBuilder.Build(); | |
services.AddSingleton(usecaseBus); | |
} | |
} |
あとは環境に従ってテスト用またはプロダクト用のスクリプトを実行するようにすれば Bus はそれぞれの処理系統を呼び出すことができます。
冗長性とその解決策
アーキテクチャを採用するということはある程度冗長性を増すことと同義です。
そのアーキテクチャは「開発」のためではなく、「改修」のためである場合が多いでしょう。
つまり、ある意味アーキテクチャを採用するということは、未来に対してコストを払っているようなものです。
そしてそのコストを支払うのは開発フェーズに携わるプログラマです。
クリーンアーキテクチャを採用したときの冗長性はどうでしょうか。
クリーンアーキテクチャを実践すると、次のステップを開発者に実施してもらう必要があります。
- UseCase を定義する
- リクエストを定義する
- レスポンスを定義する
- Interactor を定義する
- Mock の Interactor を定義する(*)
- Interactor を DI 登録する
- Mock の Interactor を DI 登録する(*)
(*) マークについてはテスト用なので任意ではありますが、それを抜いたとしても多くの「面倒な」手続きを行ってもらう必要があります。
それは途方もない自制心をプログラマに課すということです。
それは祈ることと同義です。
アーキテクトの役目は祈ることが仕事ではありません。
採用したアーキテクチャを間違いなく守ってもらうためにやれるべきことは他にもあるはずです。
アーキテクトは何ができるでしょうか。
アーキテクチャの防衛
アーキテクチャを守るために最も効果的なのはライブラリやツールを用意することです。
アーキテクチャには「決まりきった」コードが散見する場合があります。
クリーンアーキテクチャの場合でも先ほど述べた
- UseCase を定義する
- リクエストを定義する
- レスポンスを定義する
- Interactor を定義する
- Mock の Interactor を定義する(*)
- Interactor を DI 登録する
- Mock の Interactor を DI 登録する(*)
これら決まりきったコードがあります。
決まりきっているのであればそこを補助するツールを作れば、面倒さは解決されます。
そこまでお膳立てをすれば、プログラマの協力を得ることはそう難しくはないでしょう。
体系立てられたコードは好まれるものです。
ClArc
そういうわけでツールを作ってみました。
https://nrslib.com/clarc-csharp/
このツールはコマンドライン上で UseCase の名前を指定すると以下の処理を行ってくれるツールです。
- UseCase を定義する
- リクエストを定義する
- レスポンスを定義する
- Interactor を定義する
- Mock の Interactor を定義する(*)
- Interactor を DI 登録する
- Mock の Interactor を DI 登録する(*)
まとめ
実践クリーンアーキテクチャの内容は同心円の図を再現すると、このようなコードになるという一例に過ぎません。
クリーンアーキテクチャのその本質は UI などの変化しやすいレイヤーとビジネスロジックという変化しづらいレイヤーを分離することだと思います。
レイヤーを適切に分離することで結果として各レイヤーが疎結合になり、モックを差し込んだテストが容易になるなどの効果が生まれます。
クリーンアーキテクチャを実践するにあたって、そのレイヤーの分割さえ適切に行うことができればその形に拘る必要はないでしょう。
適宜必要があればその形を崩しても構わないとさえ思います。
しかしアーキテクチャを実情に合わせて「正しく崩す」ことができるのは、そのアーキテクチャの正しい形と思想を理解しているときだと考えます。
正しい形さえ理解していれば、それを崩したときに得られるメリットとデメリットを天秤にかけることができます。
Presenter を使うか使わないかの選択はその最たる例だと思います。
event 構文などを使って Presenter の代わりとすることもできます。
MVC フレームワークではそもそも採用しないという判断もあります。
この記事にお付き合いいただいたことで、その判断ができるようになっているのであれば幸いです。