もくじ
Clean Architectureで開発してま
論よりコード!
自分でも実装してみてます。
<I>にしている箇所が重要で、
- 切り替え
- テスト
できるようにしている
<DS>の箇所も切り出しているので
- テスト
できる
Entitiesでは、
Entity(Domain)を生成する
・DataAccess Interface(xxxRepository)はEntityに依存していて、Entityをタイプヒンティングすることでチェックする。Entityの生成時はValue Objectを採用することでも、EntityのDBのカラムの仕様を表現することができる
// 参考にさせて頂いているリポジトリでは、DDDで行なっているEntityをインスタンスとして生成時にプロパティを指定し、インスタンスに生成後に変更させない制約は実装されていないのでClean Architectureでは重要ではないか、実践した時に開発しにくいのかもしれない。
Clean Architectureでは、図のようなクラス分割とInterface(汎化)を利用することで、
- 切り替え
- テスト
できるようにして、DBやFW変更など技術的なサービス刷新の変更に耐えられるようにしている。
丸い円が大事なことは→の依存の向き
- EntitiesやUse CasesにWeb, UI, Devices, DBを依存させていること
EntitiesやUse CasesはWeb, UI, Devices, DBに依存しない
→ Web, UIの都合、Devices, DBの変更に左右されないように実装すること
@see
https://github.com/nrslib/LaraClean
お勉強用リポジトリ
https://github.com/yuukanehiro/laravel-clean-architecture
Clean Architectureで何が嬉しいの?
- テストができる
・Interactor
・Repository
・ViewModel - FWの変更に耐えられるようにする
- ビジネスロジックにミドルウェアやライブラリや技術的な実装が入らないようにする、分離する
だから、「ビジネスモジュールを中心にシステム(アーキテクチャ)を組み立てる」ことができる。
技術的な実装にビジネスが左右されないようにする、
ビジネスモジュールがFWやDB, ビジネスモジュールに依存しないようにする為のアーキテクチャ。
それがクリーンアーキテクチャ。
Cleanに保つ戦術
- Interactorパターン
1つの責務を担当するクラス。handle(), execute()といった実行メソッド1つのみを持ったクラスにする - Value Objectパターン
・操作対象と操作メソッドを凝集したクラス。仕様やバリデーションを一箇所にまとめやすくなる
・Value Objectのコンストラクタでバリデーションを行うので、バリデーションがまとまる
・Value Objectのメソッドで「動詞 + 目的語」となっていたら注意
目的語のValue Objectでそのメソッドは実装すべき
Bad
class User { private int $age; public function __construct(UserAge $age) { $this->age = $age; } public function increaseAge(): void { $this->age->increase(); } }
UserクラスにincreaseAge()が実装されている。「動詞 + 目的語」になっている。
このメソッドはAgeクラスで実装されているべき
Good
class Age { private final int $value; public function __construct(int $value) { $this->value = $value; } public function increase(): void { return new Age(++$this->value); } }
- Policyパターン
早期returnとPolicy Patternによって、ifのネストを浅くする - Strategyパターン + Map
Mapを利用してInterfaceを実装したクラスをbindすることで、switchやifを排除できる
// PHPの場合
https://stackoverflow.com/questions/36853791/laravel-dynamic-dependency-injection-for-interface-based-on-user-input
具体的な工夫
- ビジネスモジュール(Application … UseCase)とインフラストラクチャモジュール(Inflastructure… Repository)の間にInterfaceに依存させる
インフラストラチャモジュールの変更がビジネスモジュールに波及しないようにする。
ビジネスモジュールはInterfaceを用いてインフラストラチャモジュールを利用する。どのインフラストラチャモジュールを利用するかはビジネスモジュールが決める、切り替えることができる。
ビジネスモジュールが主導権を握る // 依存関係の逆転の原則 - メソッドインジェクションでInterfaceを指定する(Adapterパターン)ことで、実装とテストを分けられる
・CotrollerでのメソッドインジェクションでInteractorを指定する
・Interactor内部でもRepositoryのコンストラクタはxxxRepositoryInterface
→
・Interactor, Repositoryどちらもテストできるようにしている
・ミドルウェアが変わっても付け替えることができる
テスト用実装とbindを切り替えることができる
namespace App\Providers; use App\Lib\Context\UserContext; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { /** * Register any application services. * * @return void */ public function register() { // Mock で実行したい場合はコメントアウト $this->registerForInMemory(); // Mock で実行したい場合はコメント外す // $this->registerForMock(); }
テスト用のInteractorに切り替えられる用にメソッドインジェクションによりInterfaceを指定する
namespace App\Http\Controllers; use App\Http\Models\User\Commons\UserViewModel; use App\Http\Models\User\Create\UserCreateViewModel; use App\Http\Models\User\Index\UserIndexViewModel; use Illuminate\Http\Request; use Illuminate\Routing\Controller as BaseController; use packages\UseCase\User\Create\UserCreateUseCaseInterface; use packages\UseCase\User\Create\UserCreateRequest; use packages\UseCase\User\GetList\UserGetListUseCaseInterface; use packages\UseCase\User\GetList\UserGetListRequest; class UserController extends BaseController { public function index(UserGetListUseCaseInterface $interactor) { $request = new UserGetListRequest(1, 10); $response = $interactor->handle($request);
- {UseCase名}Requestクラスを生成してInteractorに渡す
- Interactorが実行するのはhandle()のみ // 単一責任の原則
handle()やexecute()といった実行メソッドのみを作ることでクラスの責務が単一になり、それがクラス名となり可読性が向上し、生産性を維持できる。
$users = array_map( function ($x) { return new UserViewModel($x->id, $x->name); }, $response->users ); $viewModel = new UserIndexViewModel($users); return view('user.index', compact('viewModel')); } public function create(UserCreateUseCaseInterface $interactor, Request $request) { $name = $request->input('name'); $request = new UserCreateRequest($name); $response = $interactor->handle($request); $viewModel = new UserCreateViewModel($response->getCreatedUserId(), $name); return view('user.create', compact('viewModel')); } }
Interactorでデータ処理を行なって、ViewModelにデータを渡す。
ViewModelはフロントに渡す為のJSONを整形する。
なぜViewModelを使うのか?
ビジネスロジックとフロントに渡す為のデータの整形を分離したいから。
フロントに渡す為のパラメータやデータ構造を明示化することで、FWが変更になったとしても対応しやすくする
Interctorで利用するRepositoryもInterfaceを利用する。これもテスト用のMockリポジトリに切り替えられるようにする為
namespace packages\Domain\Application\User; use packages\Domain\Domain\User\UserRepositoryInterface; use packages\Domain\Domain\User\User; use packages\Domain\Domain\User\UserId; use packages\UseCase\User\Create\UserCreateUseCaseInterface; use packages\UseCase\User\Create\UserCreateRequest; use packages\UseCase\User\Create\UserCreateResponse; class UserCreateInteractor implements UserCreateUseCaseInterface { /** * @var UserRepositoryInterface */ private $userRepository; /** * UserCreateInteractor constructor. * @param UserRepositoryInterface $userRepository */ public function __construct(UserRepositoryInterface $userRepository) { $this->userRepository = $userRepository; } /** * @param UserCreateRequest $request * @return UserCreateResponse */ public function handle(UserCreateRequest $request) { $userId = new UserId(uniqid()); $createdUser = new User($userId, $request->getName()); $this->userRepository->save($createdUser);
$this->userRepository->save($createdUser);
引数にはEntityを入れることで、{Entity名}Repositoryクラスのsave()でタイプヒンティングする。stringやintegerを利用してパラメータからsave()するのではなく、Entityをsave()する。
- Entity … ドメイン層
- {Entity名}Repository … インフラストラクチャ層
インフラストラクチャ層はドメイン層に依存している
return new UserCreateResponse($userId->getValue()); } }
Interactorでのreturn時に{UseCase名}Responseを生成して返却
テスト用のInteractor
Repository
通常のDBを扱うリポジトリ
namespace packages\Infrastructure\User; use Illuminate\Support\Facades\DB; use packages\Domain\Domain\User\User; use packages\Domain\Domain\User\UserId; use packages\Domain\Domain\User\UserRepositoryInterface; class UserRepository implements UserRepositoryInterface { /** * @param User $user * @return mixed */ public function save(User $user) { DB::table('users') ->updateOrInsert( ['id' => $user->getId()], ['name' => $user->getName()] ); } /** * @param UserId $id * @return User */ public function find(UserId $id) { $user = DB::table('users')->where('id', $id->getValue())->first(); return new User($id, $user->name); } /** * @param int $page * @param int $size * @return mixed */ public function findByPage($page, $size) { // TODO: Implement findByPage() method. } }
テストのDB
namespace packages\MockInteractor\User; use packages\UseCase\User\Create\UserCreateUseCaseInterface; use packages\UseCase\User\Create\UserCreateRequest; use packages\UseCase\User\Create\UserCreateResponse; class MockUserCreateInteractor implements UserCreateUseCaseInterface { /** * @param UserCreateRequest $request * @return UserCreateResponse */ public function handle(UserCreateRequest $request) { return new UserCreateResponse('test-id'); } }
DBを利用しないインメモリ用 // テスト用
namespace packages\InMemoryInfrastructure\User; use packages\Domain\Domain\User\UserRepositoryInterface; use packages\Domain\Domain\User\User; use packages\Domain\Domain\User\UserId; class InMemoryUserRepository implements UserRepositoryInterface { private $db = []; /** * @param User $user * @return mixed */ public function save(User $user) { $this->db[$user->getId()->getValue()] = $user; var_dump($this->db); } /** * @param UserId $id * @return User */ public function find(UserId $id) { $found = $this->db[$id->getValue()]; return $this->clone($found); } /** * @param User $user * @return User */ private function clone(User $user){ $cloned = new User($user->getId(), $user->getName()); return $cloned; } /** * @param int $page * @param int $size * @return User[] */ public function findByPage($page, $size) { $start = ($page - 1) * $size; return array_slice($this->db, $start, $size); } }
Domain/Domain/User
UserRepositoryInterface
Infrastructure/User/UserRepositoryが利用するインターフェイスを定義
namespace packages\Domain\Domain\User; interface UserRepositoryInterface { /** * @param User $user * @return mixed */ public function save(User $user); /** * @param UserId $id * @return User */ public function find(UserId $id); /** * @param int $page * @param int $size * @return mixed */ public function findByPage($page, $size); }
packages\Domain\Domain\User\User
UserのEntity
https://github.com/yuukanehiro/laravel-clean-architecture/blob/main/src/packages/Domain/Domain/User/User.php
namespace packages\Domain\Domain\User; class UserId { private $value; /** * UserId constructor. * @param string $value */ public function __construct($value) { $this->value = $value; } /** * @return mixed */ public function getValue() { return $this->value; } }
idはValueObjectを採用している。nameプロパティはstringになっている。
/** * @return UserId */ public function getId(): UserId { return $this->id; } /** * @return string */ public function getName(): string { return $this->name; } }
Entity
可変。同一性判定…識別子で同一であれば同一。例)社員の田中さん。たなかさんの給料や役職は変わってもEntityは同一。社員idなどで一意な識別子が必要。
Userのnameが変更されることはある
namespace packages\Domain\Domain\User; class User { /** * @var UserId */ private $id; /** * @var string */ private $name;
プロパティはprivateで外部から直接参照させない
/** * User constructor. * @param UserId $id * @param string $name */ public function __construct(UserId $id, string $name) { $this->id = $id; $this->name = $name; }
生成時のみプロパティを設定してEntityを設定する。
setter()は存在させない、getter()のみに限定。Entityインスタンスを生成後に値が変更されないようにする。変更したい場合は新たにEntityインスタンスを生成する制約。
// 上記が基本方針で、setter()を作っても良い方針の場合は、public function changeName()のようなpublicな意図がわかる関数名を定義してsetterとする。とはいえ、基本はEntityを新たに作る。
/** * @return UserId */ public function getId(): UserId { return $this->id; } /** * @return string */ public function getName(): string { return $this->name; } }
ValueObject
不変。同一性判定..保持する全ての属性が同一であれば同一。例) 社員id, 給料、役職
UserIdが変更されることはない。生成されたら破棄されるのみ
https://github.com/yuukanehiro/laravel-clean-architecture/blob/main/src/packages/Domain/Domain/User/UserId.php
Infrastructure
namespace packages\Infrastructure\User
namespace packages\Infrastructure\User; use Illuminate\Support\Facades\DB; use packages\Domain\Domain\User\User; use packages\Domain\Domain\User\UserId; use packages\Domain\Domain\User\UserRepositoryInterface; class UserRepository implements UserRepositoryInterface { /** * @param User $user * @return mixed */ public function save(User $user) { DB::table('users') ->updateOrInsert( ['id' => $user->getId()], ['name' => $user->getName()] ); }
save()の引数でDomain/Domain/Userのオブジェクトを型指定して、Userインスタンスからのみ登録するように限定している
/** * @param UserId $id * @return User */ public function find(UserId $id) { $user = DB::table('users')->where('id', $id->getValue())->first(); return new User($id, $user->name); }
DBから取得したオブジェクトをそのまま返さずに、一旦Domain/Domain/Userクラスのインスタンスを生成して返却している。
/** * @param int $page * @param int $size * @return mixed */ public function findByPage($page, $size) { // TODO: Implement findByPage() method. } }
集約とコンポジション
インスタンス生成時にコンストラクタでnew()や委譲をしているものはコンポジション、と覚える。
コンポジション(合成)
A has Bの関係。 // A is Bは継承
例. 社員と社員証のような、社員が消滅したら社員証も消滅する関係
class Employee { private $employee_id_card; public function __construct(EmployeeIdCard $employee_id_card) { $this->employee_id_card = $employee_id_card; } }
Employeeインスタンスを生成すると同時にEmployeeIdCardインスタンスが生成されている … コンポジション(合成)
集約
- 例1.空港と飛行機の関係。空港が消滅しても飛行機は消滅しない。
- 例2.労働市場における、会社と社員の関係。// 会社が消滅しても労働市場としての社員は消滅しない
class AirPort { private $plane_ids; public function setPlanes(array $plane_ids) { // List<PlaneId> PHPはarrayしかない...。 $this->plane_ids = $plane_ids; } }
もしくはコンストラクタで変数に代入するパターン
class AirPort { private $plane_ids; public function __construct(array $plane_ids) { // List<PlaneId> PHPはarrayしかない...。 $this->plane_ids = $plane_ids; } }
AirPortインスタンスを生成した時には空港Idは生成されない。
コンストラクタで識別子を代入していたり、後からsetter()でプロパティに代入する… 集約
注意すること
書き込み処理は必ず集約の単位で代入すること。
OK
Interface AirPortRepositoryInterface { public function insert(Airport $airport); public function update(Airport $airport); public function save(Airport $airport); }
引数には集約単位であるAirportクラスが入っている。
Bad
Interface AirPortRepositoryInterface { public function insert(array $plane_ids); public function update(AirportId $airport_Id, ≈, array $airport_params); public function save(array $plane_ids, array $airport_params); }
- Array $plane_idsを引数にするなら、集約の親であるAirportを設定すべき
- $airport_paramsの中身が謎
・AirportクラスのプロパティのみであればAirportクラスを入れる
・中身をValueObjectクラスに責務毎に分解してValue Object化した方が良い
仕様 {UseCase名}Specification
アプリケーションの複雑な仕様を定義しておくクラス。
{UseCase名}Interactorクラスにそのまま複雑な仕様を記述しがち、検索条件や評価条件といったものはSpecificationクラスとして分離してDomainに配置しておくことで、Domainの知識として整理することができる。
ドメインをまたがる(JOIN)が絡むSELECTはどうするの?
Entityなどを利用してやろうとすると、N+1問題の発生やループ処理が発生してパフォーマンスが悪くなる。パフォーマンスを気にして、ページングなどで取得数を絞るとシステムを利用する人の期待値を落とすことになる。そうなると利用されないシステムが作られていく。
→
QueryServiceで対応する。// {UseCase名}Interactorクラスがhandle()の中で{Entity名}QueryService->{UseCase名}Result()メソッドでクエリ1発で取得する。
CQS
Bad
class User { private UserAge $age; public function __construct(UserAge $age) { $this->age = $age; } public function increaseAge(): UserAge { $this->age->increase(); return $this->age; } }
increaseAge()で更新と取得を同時に行なってしまっている。
Good
class User { private int $age; public function __construct(UserAge $age) { $this->age = $age; } public function increaseAge(): void { $this->age->increase(); } public function getAge(): UserAge { return $this->age; } }
CQRS
- ロック競合が起こりにくくなる
- 扱いやすくなる
CQRS違反は頻出パターンであり便利でもある。提唱者は利便性から原則違反を知りつつ必ず守っているわけではない、必ず守らなければいけない原則ではないとしている。
高凝集、低結合にする
- オブジェクト指向は忘れる
- クラス設計するときに「このクラスの責務は何か、責務は何か、責務、責務」と考える
凝集度
凝集度は「クラス内における、データとロジックの関係性の強さを表す指標」であると、冒頭で説明しました。インスタンス変数と、そのインスタンス変数を用いるロジックを同じクラス内に閉じ込めた構造が高凝集
仙塲 大也. 良いコード/悪いコードで学ぶ設計入門保守しやすい 成長し続けるコードの書き方 (Japanese Edition) (p.112). Kindle 版.
凝集度は高い方が良い。
クラスの責務を明確にする。
色々な責務がまとめて詰め込まれているクラス。xxxUtil, xxxServiceなどの名前がついているものは寄せ集めた凝集度が低いクラスである可能性が高いものになっていがち… 低凝集
UserCreateService, UserUpdateServiceなどにUseCaseで分けていればyoki
凝集度に影響がない場合に、staticメソッドを使えます。簡単に言えば、ログ出力用メソッドやフォーマット変換用メソッドなど、凝集度に無関係なものはstaticメソッドとして設計して良い
仙塲 大也. 良いコード/悪いコードで学ぶ設計入門保守しやすい 成長し続けるコードの書き方 (Japanese Edition) (p.115). Kindle 版.
カプセル化
クソコード動画「カプセル化」 pic.twitter.com/kAhXCEHYVT
— ミノ駆動 (@MinoDriven) June 23, 2019
- 利用するクラスが利用されるクラスの実装や状態を気にしないで利用できるようにしておく
・利用されるクラスのクラス変数が可変であってはいけない
・可変になっていると利用するクラスが利用されるクラスの状態を常に気をつけないとバグが発生する
→クラス変数にfinalプロパティをつける
・「(利用するクラスは)尋ねるな、(利用するクラスに)命じろ」ができる設計にしておく
利用するクラスが利用されるクラスのプロパティを設定する構造はアンチパターン
・利用されるクラス側でコンストラクタの時点で完全コンストラクタ化しておく
そうすると利用するクラスは利用される側のクラスの実装を気にしないでよくなる
Bad
class User { public function __construct( Pc $pc ) { $this->pc = $pc; } public function programing() { $this->pc->run()->vscode()->writeCode(); } }
UserクラスがPcクラスに尋ねすぎている。Pcクラスが未熟なクラスになっている、UserクラスがPcクラスを知りすぎている。PcクラスはAPIとして1メソッドで外部からの要求に応えるようにPcクラスを設計する必要がある。
Good
class User { public function __construct( PC $pc ) { $this->pc = $pc; } public function programing() { $this->pc->writeCode(); } }
writeCode()だけで利用できる。良き。
- 完全コンストラクタ化
・コンストラクタの時点でクラス変数にすべて代入しておく
・クラス変数はfinalでイミュータブルにしておく
思考停止のsetter(), getter()の機械的実装はアンチパターン
低凝集の問題
- 操作対象クラスと操作クラスが別なクラスがあると、低凝集なので複数の操作クラスで同じ動作をさせるメソッドが実装される場合がある
→操作対象クラスと操作メソッドは1つのクラスで納めると高凝集となる - 操作クラスのメソッドの引数に操作対象クラスを指定すると、「出力引数」でありアンチパターン。メソッドの引数は値の受け渡しのみをするべき。出力引数を実装すると、操作メソッドの実装を詳細に追わないといけなくなる。
@仙塲 大也. 良いコード/悪いコードで学ぶ設計入門保守しやすい 成長し続けるコードの書き方 (Japanese Edition) (p.128). Kindle 版.
結合度
結合度は低い方が良い。
Aクラスの中で、Bクラスを利用して依存させない。… AクラスはBクラスに依存している
AクラスとBクラスを独立させる。
OK
$printer->print($counter->getCount());
PrinterクラスとCounterクラスが独立している
Bad
class Printer { public function __construct(Counter $counter) { $this->counter = $counter; } public function print() { echo $this->counter->getCount(); } }
PrinterクラスにCounterクラスが依存している。
SOLID原則
開発メモ
- Go言語で基本的なCRUD操作を行うREST APIを作成
わかりやすい。コンストラクタやDIがわかる
https://github.com/koga456/sample-api - Clean Architecture with Go
Clean Architectureのテンプレの例
リンク先にも海外のサンプル実装がある - GolangでMVCなAPIサーバを作るときのディレクトリ構成とプロジェクト生成コマンド
config, responseの実装など勉強になる - echo.Context を最大限に活用する
echo.ContextのWrapする方法やbindなど勉強になる