Node.js + DDD + API サーバ設計
感想
JS にも awilix のような DI コンテナがあるのだということがわかった
IoC (Inversion of Control) は実現できている
JS にはインタフェースはないので、あまり安全には見えない (そもそも動的型なので仕方ない)
DI を実現するライブラリだと InversityJS というものもあるようだ
DI コンテナは複雑化する気配のあるアプリケーションになら導入しても良さそう。そうでなければかえって読みにくくなってしまう気がする
読む予定のやつ
https://dev.to/remojansen/implementing-the-onion-architecture-in-nodejs-with-typescript-and-inversifyjs-10ad
https://www.bennadel.com/blog/2795-thinking-about-inversion-of-control-ioc-in-node-js.htm
https://postd.cc/choose-design-over-architecture/
https://twitter.com/rufuse/status/806198869142937601
モチベーション
DDD や Clean Architecture を実践したいとなんとなく思っていたので、例を探してみたところ以下のようなリポジトリを発見した。
https://github.com/talyssonoc/node-api-boilerplate
DDD, CleanArchitecture に則って関心ごとの分離を図っている。
レイヤー
アプリケーション層
インタフェースからの入力とビジネスドメインを仲介する責務を持つ
アプリケーションのユースケースやサービスをもつ
ドメイン層
ビジネスドメインクラス群を定義する
全てのビジネスルールはここで定義される
インフラストラクチャ層
アプリケーション外と連携する
データベースやメールサービス、フレームワーク等
インターフェース層
アプリケーションへのエントリーポイントを持つ
DI
Inversion of control パターン、すなわち依存関係逆転の原則を実現するために、DI コンテナとして awilix を採用している。
インフラストラクチャ層
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);
#Node.js #DDD