JavaScript
flow
ドメイン駆動設計
es6
flowtype

FlowTypeで集約をどうやって書くか

前回の記事@kinzalさんに教えていただいた$Diff<this, {}>の使い方についてちょっと考えてみたところ、
また新たな問題に気づいたのでまとめます。

集約難しい問題

// @flow
class Post {
  id: string;
  state: "Public" | "Private";
  content: string;
  constructor( params: $Diff<this, {}> ) {
    this.id = params.id;
    this.state = params.state;
    this.content = params.content;
  }
  isActive(): boolean {
    return this.state === "Public" && this.content.length > 0;
  }
}
class User {
  id: string;
  posts: Post[];
  constructor( params: UserParams ) {
    this.id = params.id;
    this.posts = params.posts.map( post => new Post( post ) );
  }
  hasActivePost(): boolean {
    return !!this.posts.find( post => post.isActive );
  }
}

現在、私はローカルエンティティを持つ集約したドメインモデルを表現するときは上記のようにしています。
この場合、$Diff<User, {}>を使う書き方でUserParamsを定義すると、ネストしたクラスベースの型がstructuralなtypeになってくれません。

type UserParams = $Diff<User, {}>;

const userA: UserParams = {
  id: "hoge",
  posts: [
    { id: "fuga", state: "Public", content: "" },
    { id: "hoge", state: "Private", content: "hello" }
  ]
}; // error!

3案ほど考えてみましたが、「これ!」という結論はでませんでした。
知見がや意見が欲しいです。

ゴリゴリ書く

// @flow

type PostParams = {
  id: string,
  state: "Public" | "Private",
  content: string
};

class Post {
  id: string;
  state: "Public" | "Private";
  content: string;
  constructor( params: PostParams ) {
    this.id = params.id;
    this.state = params.state;
    this.content = params.content;
  }
  isActive(): boolean {
    return this.state === "Public" && this.content.length > 0;
  }
}

type UserParams = {
  id: string,
  posts: PostParams[]
};
class User {
  id: string;
  posts: Post[];
  constructor( params: UserParams ) {
    this.id = params.id;
    this.posts = params.posts.map( post => new Post( post ) );
  }
  hasActivePost(): boolean {
    return !!this.posts.find( post => post.isActive );
  }
}

const userDataA: UserParams = {
  id: "hoge",
  posts: [
    { id: "fuga", state: "Public", content: "" },
    { id: "hoge", state: "Private", content: "hello" }
  ]
};

const userA = new User( userDataA );

長所

  • シンプル
  • typeの組み合わせで型を考えられるため見通しがさそう

短所  

  • paramsの再定義が必要でリファクタするたびに書き直しが必要。コンストラクタも変わるので大変。

いままではこれで書いてました。

ファクトリを定義する

// @flow
class Post {
  id: string;
  state: "Public" | "Private";
  content: string;
  constructor( params: $Diff<this, {}> ) {
    this.id = params.id;
    this.state = params.state;
    this.content = params.content;
  }
  isActive(): boolean {
    return this.state === "Public" && this.content.length > 0;
  }
}
class User {
  id: string;
  posts: Post[];
  constructor( params: $Diff<this, {}> ) {
    this.id = params.id;
    this.posts = params.posts;
  }
  hasActivePost(): boolean {
    return !!this.posts.find( post => post.isActive );
  }
}

type UserParams = $Diff<User, {}>;

const userDataA: Object = {
  id: "hoge",
  posts: [
    { id: "fuga", state: "Public", content: "" },
    { id: "hoge", state: "Private", content: "hello" }
  ]
};

const userParamsFromJson = ( userData: any ): UserParams => {
  return { ...userData, posts: userData.posts.map( post => new Post( post ) ) };
};

const userA = new User( userParamsFromJson( userDataA ) );

長所

  • paramsの再定義が必要ない
  • Classインスタンスの下のドメインモデルは必ずクラスインスタンス、という運用ができる

短所  

  • 結局二度手間感がある
  • 複雑なドメインオブジェクトになるとファクトリがひどいことになる

paramsメンバにピュアオブジェクトを常に保持する

// @flow

type PostParams = {
  id: string,
  state: "Public" | "Private",
  content: string
};
class Post {
  params: PostParams;
  constructor( params: PostParams ) {
    this.params = params;
  }
  isActive(): boolean {
    return this.params.state === "Public" && this.params.content.length > 0;
  }
}

type UserParams = {
  id: string,
  posts: PostParams[]
};
class User {
  params: UserParams;
  constructor( params: UserParams ) {
    this.params = params;
  }
  hasActivePost(): boolean {
    return !!this.params.posts.find( post => new Post( post ).isActive );
  }
}

const userAParam: UserParams = {
  id: "hoge",
  posts: [
    { id: "fuga", state: "Public", content: "" },
    { id: "hoge", state: "Private", content: "hello" }
  ]
};

const userA = new User( userAParam );

全てのクラスはparamsメンバにピュアオブジェクトを持つ。
classとしての表現を用いるたびnewをする必要がある。

長所

  • paramsの再定義が必要ない  
  • typeの組み合わせで型を考えられるため、の見通しがさそう
  • コンストラクタを書くのが楽  

短所  

  • コンストラクタに重い処理があるとhasActivePostのような処理まで重くなる
  • 上記userAからclassのPost配列を取得しようとすると、userA.params.posts.map(post => new Post(post))みたいになってしんどい。

さて、いかがでしょうか。
ファクトリ使うのが一番スッキリしそうですが、なんとなくどれも正解じゃない気がしています・・・