DDDではリポジトリに対してDIPを利用し、インターフェースと実装を切り離す傾向にある。
これはいわゆる「抽象に依存せよ」ってやつなので、
DDDというよりは既存のプログラミングテクニックになる。
で、これを実現するためにリポジトリを以下のようにインターフェースで実装する。
コードはTypeScriptです。
interface UserRepo {
insert(user: User, master: DbMasterConnection): Promise<User>;
findByName(name: string, con: DbConnection): Promise<User>;
findById(id: number, con: DbConnection): Promise<User>;
}リポジトリの実装自体はインフラレイヤに置く。
「データを取得する」ということなので、個人的にはDaoというクラス群を作るようにしている。
class UserDao implements UserRepo {
public insert(user: User, master: DbMasterConnection): Promise<User> {
return TUser.insert(user, master);
}
public findByName(name: string, con: DbConnection): Promise<User> {
return TUser.findByName(name, con);
}
public findById(id: number, con: DbConnection): Promise<User> {
return TUser.findById(id, con);
}
}Daoはリポジトリに対する実装なんだけど、
DB,memcache,ファイルなどへのアクセスを管理するだけ。
実装自体は別のクラスに切り出している。
上記の UserDao だと TUser というのがDBへアクセスするための実装クラスになっている。
memcacheとかファイルへのアクセスもこのように切り出すことが多い。
class TUser{
public static insert(user: User, master: DbMasterConnection): Promise<User> {
if(user.isForInsert() === false){
throw AppException.createError(AppErrorRegistry.NOT_INSERT_ENTITY);
}
return new Promise<User>((resolve, reject) => {
master.connection.query(
'INSERT INTO t_user(id, name, password, create_date, update_date) values(?,?,?,?,?)',
[null, user.name, user.password, user.createDate, user.updateDate],
(err, result) => {
if(err){
reject(err);
}else{
user.changeIdForInsertEntity(result.insertId);
resolve(Promise.resolve(user));
}
}
);
});
}
public static findByName(name: string, con: DbConnection): Promise<User> {
return new Promise<User>( (resolve, reject) => {
con.connection.query(
'SELECT * FROM t_user WHERE name = ?',
[name],
(err, rows) => {
if(err){
reject(err);
}else{
if(rows.length == 0){
resolve(User.createEmptyEntity());
}else{
resolve(this.mapUserEntity(rows[0]));
}
}
}
);
});
}
private static mapUserEntity(row): User {
return new User(row.id, row.name, row.password, new RecordTime(row.create_date, row.update_date))
}
public static findById(id: number, con: DbConnection): Promise<User> {
return new Promise<User>( (resolve, reject) => {
con.connection.query(
'SELECT * from t_user WHERE id = ?',
[id],
(err, rows) => {
if(err){
reject(err);
}else{
if(rows.length == 0){
resolve(User.createEmptyEntity());
}else{
resolve(this.mapUserEntity(rows[0]));
}
}
}
);
});
}
}このような実装をすることで、
リポジトリを利用する側は取得するデータがDBのものだろうと、
memcacheのものだろうと意識する必要なくエンティティ(集約)を取得できるし、
インターフェース自体はリポジトリで保証されている。
DBからのデータ取得をmemcacheに切り替える必要がある場合は
Daoのレイヤを修正するだけで、ドメインレイヤは何も影響を受けない。
で、ここからが問題。
「リポジトリを利用する側は取得するデータがDBのものだろうと、 memcacheのものだろうと意識する必要なくエンティティ(集約)を取得できる」
とかって言ったけど、
インターフェースの引数におもいっきりDBコネクション(DbMasterConnection, DbConnection)が含まれている。
interface UserRepo {
insert(user: User, master: DbMasterConnection): Promise<User>;
findByName(name: string, con: DbConnection): Promise<User>;
findById(id: number, con: DbConnection): Promise<User>;
}これは複数のリポジトリにまたがるトランザクションを管理するため。
DDDにおいてトランザクション管理はアプリケーションレイヤの責務だけど、
トランザクションをかけたDBコネクションはインフラレイヤで利用するので、
リポジトリがDBコネクションを意識せざるを得ない。
(引数で渡さずにクラス内のプロパティで持ってもいーけど、結局リポジトリで意識せざるを得ない)
insert,update系の処理はもちろんだけど、
select系の処理もデータによっては「マスターから取得したい」というケースがあるので(特に課金周りとか)、
DBへのアクセスでは常にDBコネクションが必要になってしまう。
このリポジトリがDBコネクションを意識する感じがイヤ。
PHPであれば、
1リクエスト1プロセスという性質上、DBコネクションをシングルトンで実装することで
グローバルに取得できるから、それでもいーかなと思ったり・・・しなくもない・・・。
JavaだとスレッドIDごとにコネクションを管理するようにマルチトンで実装すれば「可能」なのかな?
でも、可能なだけでアンチパターンだと思う。
フレームワークでアノテーションによるトランザクション管理機能があった気がするから、
それを利用するのかな?
nodeはシングルスレッドだからマルチトンで実装することすらできない気がする。
でも、TypeScriptにはデコレータがあるから、
同じようなことやろうと思えばできるかも・・・。
ということで、何も解決していません。
何か解決策があればいーなとは思うけど、
DDDはデザインパターンのような特定範囲のクラス設計ではなく、
アプリケーション全体のアーキテクチャなので、
理想通りいかないことが多いのかなと。
うまく実戦向けに噛み砕いて実装してあげる必要があると思う。
そして、そーゆーところが面白い。