どうもバックエンドエンジニアの小室です。
最近クリーンアーキテクチャ について理解が以前に比べ深まってきました。
まとめがてら、簡単なユーザー作成機能をクリーンアーキテクチャ を使い実装していきたいと思います。
クリーンアーキテクチャ
お馴染みの円の図です。
レイヤーの説明
・Enterprise Business Rules
ビジネスロジック を表現するレイヤーになります。DDDによる設計が最も影響し、
DDDでいうEntity, Value Object, Domain Serviceが該当します。
・Application Business Rules
ビジネスロジック を組み合わせ、ソフトウェアは何ができるのか(UseCase)を表現するレイヤーになります。
・Interface Adapters
入力、永続化、出力を司るレイヤーになります。
・Frameworks & Drivers
フレームワーク やDBなど、詳細な技術についてを置くためのレイヤーです。
このレイヤーでは、Interface Adapterが理解できる形へ変換される必要があります。
レイヤー間の依存方向
依存方向
この矢印の意味は依存方向を表します。
依存方向は上位レベルのレイヤーに向かっていきます。
flow of control
大事なのはここ、リクエス トからレスポンスまでの流れを表しています。
もっと詳しく表したが図こちら。
flow of control
・フレームワーク 独立
アーキテクチャ は、ソフトウェアのライブラリに依存しない。
フレームワーク を道具として使うことを可能にし、システムはフレームワーク の限定された制約に縛られない。
・テスト可能
ビジネスルールは、UI、データベース、ウェブサーバー、その他外部の要素なしにテストできる。
・UI独立
ビジネスルールの変更なしに、UIを置き換えられる。
・データベース独立
Oracle あるいはSQL Server を、Mongo, BigTable , CouchDB あるいは他のものと交換することができる。
ビジネスルールは、データベースに拘束されない。
・外部機能独立
ビジネスルールは、単に外側についてなにも知らない。
原文: Clean Coder Blog
クリーンアーキテクチャ において大事(コア)な考え
重要なものを些細なものに依存させない。
重要なもの=ビジネスルール
ビジネスルールは図でいう
・赤色の層(Entity: Application Business Rules)
・黄色の層(UseCase: Enterprise Business Rules)
の2層の部分です。
些細なもの = DB, UI, フレームワーク など
重要なものを些細なものに依存させないために「依存関係の逆転」を行う。
※「依存関係の逆転」後述してます。
実装コード
ルーティング
<?php
Route:: post( '/createUser' , [ \App\Http\Controllers\UserController:: class , 'createUser' ]) ;
Controller
<?php
namespace App\Http\Controllers;
use App\Packages\Domain\User\Dto\UserInputData;
use App\Http\Requests\UserCreateRequest;
use App\Packages\UseCase\User\CreateUserUseCaseInterface;
class UserController extends Controller
{
@param
@param
@return
public function createUser(
UserCreateRequest $ request ,
CreateUserUseCaseInterface $ userCreateUseCase
)
{
$ userInputData = new UserInputData( $ request [ 'name' ] , $ request [ 'tel' ]) ;
$ userOutputDataResponse = $ userCreateUseCase -> handle( $ userInputData ) ;
return response() -> json([
'uuid' => $ userOutputDataResponse -> getUuid()
] ,200 ) ;
}
}
①でリクエス データをアプリケーション用Input Dataに変換します。
②インターフェースクラスを定義してメソッドインジェクション(DI)をしています。
DIについては Laravelで始める依存性の注入(DI) - Qiita
環境変数 に応じてRepositoryをInMemoryに切り替えたりもできます。
<?php
namespace App\Providers;
class AppServiceProvider extends ServiceProvider
{
public function register()
{
if ( app() -> environment() == 'local' ) {
$ this -> app-> bind(
'App\Packages\Infrastructure\User\RepositoryInterface' ,
'App\Packages\Infrastructure\User\InMemoryRepository'
) ;
$ this -> app-> bind(
'App\Packages\UseCase\User\CreateUserUseCaseInterface' ,
'App\Packages\UseCase\User\CreateUserInteractor'
) ;
} else {
$ this -> app-> bind(
'App\Packages\Infrastructure\User\RepositoryInterface' ,
'App\Packages\Infrastructure\User\UserRepository'
) ;
}
}
③UseCase InteractorにInput Dataを渡します。
Input Data<DS>
<?php
namespace App\Packages\Domain\User\Dto;
class UserInputData
{
private $ name ;
private $ tel ;
public function __construct ( $ name , $ tel )
{
$ this -> name = $ name ;
$ this -> tel = $ tel ;
}
@return
public function getName()
{
return $ this -> name ;
}
@return
public function getTel()
{
return $ this -> tel;
}
}
?>
いわゆるDTO ってやつです。
※ <DS>はData Structure
Input Boundary
<?php
namespace App\Packages\UseCase\User;
use App\Packages\Domain\User\Dto\UserInputData;
interface CreateUserUseCaseInterface
{
public function handle( UserInputData $ userInputData ) ;
}
?>
Use Caseのインターフェースクラスになります。
実装クラス(UseCase Interactor)で必要となるメソッド名を定義します。
DIを使うことで、Controller層はUseCase層の実際の実装クラス(UseCase Interactor)ではなく、
インターフェースクラス(Input Boundary)に依存することになり、Use Case層への依存度を緩和します。
※ <I>はInterface
UseCase Interactor
<?php
namespace App\Packages\UseCase\User;
use App\Packages\Domain\User\User;
use App\Packages\Domain\User\Dto\UserInputData;
use App\Packages\Domain\User\Dto\UserOutputData;
use App\Packages\Domain\User\VO\Name;
use App\Packages\Domain\User\VO\Tel;
use App\Packages\Infrastructure\User\RepositoryInterface as UserRepositoryInterface;
class CreateUserInteractor implements CreateUserUseCaseInterface
{
private $ userRepository ;
public function __construct ( UserRepositoryInterface $ userRepository )
{
$ this -> userRepository = $ userRepository ;
}
@param
@return
@throws
public function handle( UserInputData $ userInputData )
{
$ uuid = uniqid ( "user_" ) ;
$ user = new User(
$ uuid ,
new Name( $ userInputData -> getName()) ,
new Tel( $ userInputData -> getTel())) ;
$ this -> userRepository-> save ( $ user ) ;
$ userOutputData = new UserOutputData( $ uuid ) ;
return $ userOutputData ;
}
}
?>
UseCase Interactorではアプリケーションロジックを実装します。
①Data Access InterfaceはRepositoryのインターフェースクラスになります。
実装クラスで必要となるメソッドを定義します。
②User Entiryのオブジェクトを生成しています。
③User EntiryのオブジェクトをRepositoryに渡し、保存します。
Entity
<?php
namespace App\Packages\Domain\User;
use App\Packages\Domain\User\VO\Name;
use App\Packages\Domain\User\VO\Tel;
class User
{
private $ uuid ;
private $ name ;
private $ tel ;
public function __construct (
$ uuid ,
Name $ name ,
Tel $ tel
)
{
$ this -> uuid = $ uuid ;
$ this -> name = $ name ;
$ this -> tel = $ tel ;
}
@return
public function getUuid() : string
{
return $ this -> uuid;
}
@return
public function getName() : Name
{
return $ this -> name ;
}
@return
public function getTel() : Tel
{
return $ this -> tel;
}
@param
public function changeName( Name $ name )
{
$ this -> name = $ name ;
}
}
?>
エンティティはビジネスルールをカプセル化 したオブジェクトです。
クリーンアーキテクチャ でいうEntityはDDDでいうEntityとは定義幅が異なります。
DDDドメイン モデルはEntityとValue Objectとドメイン サービスを含んだものを指します。
クリーンアーキテクチャ のEntityは、DDDドメイン モデルにあたります。
ドメイン 駆動設計のエンティティと比べるとその定義幅はかなり広いです。
UseCase層で受け取ったデータをもとにEntityのオブジェクトを作成しています。
EntityのValueObject
<?php
namespace App\Packages\Domain\User\VO;
use Exception ;
class Name
{
private $ value ;
public function __construct ( string $ value )
{
if ( strlen ( $ value ) < 3 ) throw new Exception () ;
if ( strlen ( $ value ) > 20 ) throw new Exception () ;
$ this -> value = $ value ;
}
@return
public function getValue() : string
{
return $ this -> value ;
}
}
?>
User Entiryを構成するValue Objectになります。
DataAccessInterface
<?php
namespace App\Packages\Infrastructure\User;
use App\Packages\Domain\User\User;
interface RepositoryInterface
{
@param
@return
public function save( User $ user ) ;
@param
@return
public function find( $ userId ) ;
}
?>
Repositoryのインターフェースクラスを定義しています。
DataAccess
<?php
namespace App\Packages\Infrastructure\User;
use App\Packages\Domain\User\User;
use App\Models\User as UserModel;
class UserRepository implements RepositoryInterface
{
@param
@return
public function save( User $ user )
{
$ userModel = new UserModel() ;
$ userModel -> uuid = $ user -> getUuid() ;
$ userModel -> name = $ user -> getName() ;
$ userModel -> tel = $ user -> getTel() ;
$ userModel -> save () ;
}
public function find( $ userId )
{
$ userModel = UserModel:: findOrFail( $ userId ) ;
...
}
}
?>
Repositoryクラス(Data Access )を定義しています。
Repository Interface(Data Access Interface<I>)を継承しています。
DIにより、Data Access はUseCase層であるData Access Interface<I>に依存することになります。
InMemoryRepository
<?php
namespace App\Packages\Infrastructure\User;
use App\Packages\Domain\User\User;
class InMemoryRepository implements RepositoryInterface
{
protected $ data ;
public function __construct ()
{
$ this -> data = collect() ;
}
public function all()
{
return $ this -> data ;
}
public function save( User $ user )
{
$ this -> data -> push([
'name' => $ user -> getName() ,
'tel' => $ user -> getTel()
]) ;
}
public function find( $ userId ){}
}
DIにより、ロジックが疎結合 になります。
特定のインフラストラク チャに依存しないようにロジックを記述できます。
インメモリに切り替えることも容易になります。
まとめ
俯瞰してみると、重要なビジネスルールであるEntity層とUseCase層はフレームワーク を変えたとしても、
特に変更はなくそのまま使うことができる様な形になり、フレームワーク に依存していないと、見て取れます。
UIや、データベースなどの変化しやすいレイヤーと
ビジネスロジック という変化しづらいレイヤーを分離することができました。
最後に
Wizではエンジニアを募集しております。
興味のある方、ぜひご覧下さい。
careers.012grp.co.jp