typescriptでDDDってどうやるんだろうと思って試しにやってみたときのメモです。
バグと仕事の環境上,tsのバージョンは3.6です。
ドメインモデルの書き方
ドメインモデルのクラスの書き方です。
バリューオブジェクト(インスタンス変数一つ)
シンプルなバリューオブジェクトです。
class UserId {
#value:number;
get value() {
return this.#value
}
constructor(userId: number) {
this.#value = userId
}
}
const userId = new UserId(1);
console.log(userId.value)
インスタンス変数が一つの場合はコンストラクタの引数をそのまま代入します。
ただ、tsの場合は初期化しないと型がanyとなるので、エラー防止のために明示的に型を指定します。
バリューオブジェクト(インスタンス変数が複数)
export interface NameProps {
firstName: string;
lastName: string;
}
export default class Name {
private _firstName: string;
get firstName() {
return this._firstName
}
private _lastName: string;
get lastName() {
return this._lastName
}
constructor(props: NameProps) {
this._firstName = props.firstName;
this._lastName = props.lastName;
}
}
インスタンス変数が複数あり、コンストラクタから初期化する場合は専用の型を作って受け取るようにします。
typescriptのバージョンの都合上_を先頭につけています。
今回はサンプルとしてgetterをつけていますが、基本的に使用が想定されない変数にむやみにgetterをつけないようしましょう。
また、変にグローバルに型を参照できてもこまるので、
引数の型はバリューオブジェクトと同じファイル内に定義します。
ファーストコレクションクラス
配列をインスタンス変数として持つクラス。
配列の加工や判断のロジックは複雑になりがち+ あちこち散らばると面倒なので、
全てクラスに閉じ込める
export default class Sequence {
private sequence: number[];
constructor(sequence: number[]) {
this.sequence = sequence;
}
public sum() {
this.sequence.reduce((prev, current, index, arr) => prev + current);
}
}
派生するオブジェクトの生成
ほぼ同じ概念だが、固有の処理をもたせたい場合の書き方。
例えば、普通ポイント、埋め合わせポイントがある場合共通の概念としてポイント数と有効期限があるといった感じ
この場合は抽象クラスを使う
export interface PointProps {
point: number;
expirationDate: Date;
}
export abstract class Point {
protected point: number;
protected expirationDate: Date;
constructor(props: PointProps) {
this.point = props.point;
this.expirationDate = props.expirationDate;
}
abstract isAvailable();
}
// 埋め合わせポイント
export class MakeupPoint extends Point {
constructor(props: PointProps) {
super(props);
}
public isAvailable() {
// 固有の処理
}
// 固有のメソッド
public setExpirationByformat(date: string) {
this.expirationDate = new Date(date);
}
}
基本的なクラスの使い方ですが、thisやsuperなどにクセがあります。
superで親のコンストラクタを起動しますが、代入されるのは実装元のクラス(今回の場合はMakeupPointクラス)のフィールドです。
なので、変数呼び出す場合は thisを使います。
また、抽象クラスを継承する関係上、抽象クラスのアクセスレベルはprotectedにしておきましょう。
ドメインモデルへ変換するクラス
ここからは我流が混じってます。
外部のデータソースを受け取る場合どこかしらでドメインモデルにあるいは、外部通信するのにふさわしい形に変換する必要があります。
その場合の書き方です。
リクエストクラス
REST API でリクエストボディなどを受け取ってドメインモデルに変換するクラスです。
class SampleRequestClass {
param1: number;
param2: string;
body: Object;
constructor(body: Object) {
this.body = body;
}
validate() {
// バリデーション処理、失敗した場合は例外を投げる
this.param1 = this.body.param1;
this.param2 = this.body.param2;
}
toDomainModel() {
return new DomainModel(this.param1,this.param2) // ドメインモデル
}
}
外部から使いやすいようにvalidate関数とドメインクラスに変換する関数を用意しています。
必ずバリデーションを通るように、バリデーション後にインスタンス変数を代入するようにしています。
他に思いついたら随時更新していきます。
注意点
自分もよくわかってないのですが、DDDの関係上 インスタンスを生成する回数が増えます。
これがパフォーマンス(特に速度)にどう影響を与えるのかはまだ完全に把握していません。
最後に
tsだとあまりクラスを使わないので、結構手こずりました。
その上、3.7以降のpromise.allのバグの関係上古い書き方を強制される部分もあり結構面倒でした。
そこら辺のバグは3.9で修正されるらしいので、更新されたら色々いじっていきたいと思います。