Node.js + DDD + API サーバ設計
感想
JS にも awilix のような DI コンテナがあるのだということがわかった
IoC (Inversion of Control) は実現できている
JS にはインタフェースはないので、あまり安全には見えない (そもそも動的型なので仕方ない)
DI コンテナは複雑化する気配のあるアプリケーションになら導入しても良さそう。そうでなければかえって読みにくくなってしまう気がする
読む予定のやつ
モチベーション
DDD や Clean Architecture を実践したいとなんとなく思っていたので、例を探してみたところ以下のようなリポジトリを発見した。
DDD, CleanArchitecture に則って関心ごとの分離を図っている。
レイヤー
アプリケーション層
インタフェースからの入力とビジネスドメインを仲介する責務を持つ
アプリケーションのユースケースやサービスをもつ
ドメイン層
ビジネスドメインクラス群を定義する
全てのビジネスルールはここで定義される
インフラストラクチャ層
アプリケーション外と連携する
データベースやメールサービス、フレームワーク等
インターフェース層
アプリケーションへのエントリーポイントを持つ
DI
インフラストラクチャ層
ORM クラスを Repository に注入する
awilix により注入する
アプリケーション層
Repository を Operation に注入する
Repository をコンストラクタインジェクションする
インターフェース層
Operation を Controller に注入する
awilix の injection メソッドにより Operation に Repository をインジェクトし、その上で Operation のインスタンスを得る
メモ
アプリケーション層
Operation クラスが全ての操作 (モデルの操作。ユーザの作成や読み取り等) のスーパークラス
Operation クラスにより EventEmitter クラスが拡張されている
EventEmitter に outputs を生やしている。ここには、操作の出力 (成功、失敗、検証エラー 等) を設定できる
さらに、on メソッドをオーバーライドし、output に応じたリスナーを登録できるようにしている
execute(userData) メソッドの実行により、具体的な処理の実行は行われる
処理の実行結果は登録したリスナーに渡される
各 Operation クラスのサブクラスに対する Repository の注入は、コンストラクタインジェクションにより行われている
code:javascript
const Operation = require('src/app/Operation');
const User = require('src/domain/user/User');
class CreateUser extends Operation {
constructor({ usersRepository }) {
super();
this.usersRepository = usersRepository;
}
この登録は container に対して行われる
code:javascript
const {
CreateUser,
GetAllUsers,
GetUser,
UpdateUser,
DeleteUser
} = require('./app/user');
const container = createContainer();
// Repositories
container.register({
usersRepository: asClass(SequelizeUsersRepository).singleton()
});
// Operations
container.register({
createUser: asClass(CreateUser),
getAllUsers: asClass(GetAllUsers),
getUser: asClass(GetUser),
updateUser: asClass(UpdateUser),
deleteUser: asClass(DeleteUser)
});
//
インタフェース層
外側からは、Operation クラスを使うときは、出力に応じたリスナーを登録して置けば良い
code:javascript
// getAllUsers は Operation クラスのサブクラス
// Outputs として SUCCESS, ERROR をもつ
const { SUCCESS, ERROR } = getAllUsers.outputs;
getAllUsers
.on(SUCCESS, (users) => {
res
.status(Status.OK)
.json(users.map(userSerializer.serialize));
})
.on(ERROR, next);
getAllUsers.execute();
Controller への Operation の注入は awilix-express を利用して行われている
code:javascript
const { inject } = require('awilix-express');
...
// Awilix により getAllUsers のインスタンスが生成される
// GetAllUsers のコンストラクタの引数である Repository も、Awilix によって注入される
router.get('/', inject('getAllUsers'), this.index);