yaakaito.org

TypeScriptとDDDとLazyload

DDD, TypeScript

こんにちは!うきょーです! DDDをやってるとRepositoryを経由してEntityをやり取りするわけなんですが、APIの都合とか、パフォーマンスの都合でAggregate内の一部をLazyloadしたい、みたいなケースがある。 出来ればLazyloadは考えたくないし、諦めて外に出してしまうというのも一つの手ではあるが、これもあんまりしたくない。

というわけで実装例を作ってみる。

サンプル

GithubのIssues APIを例にしてみる。 このAPIはIssueの取得と、そのIssueのコメントの取得という2つのAPIに分かれてる、まあよくある感じだ。

List issues for a repositoryを叩いてIssueのリストを取得したあと、それぞれのコメントをLazyloadできる、という形にしてみよう。

モデルを考える

今回あんまり関係ないところは略す。

実装都合で始まってはいるが、コメントのリストを扱う何かしらのものがあった方がいいように思える。 GithubのIssueページの仕様から考えると、 「Issueには複数件のCommentを投稿することができる。それを閲覧することができるが、リストで出す場合には不要、だがCommentの総数は表示したい。」 みたいな要求なんだろう。 「複数件のComment」はただのArrayでもよさそうだけど、「総数」だけ別に扱いたいようなので、そういうモデルを作った方がいいだろう。一旦「IssueComments」ということにしておくか。対応させて「Comment」も「IssueComment」にすることにする。

この「IssueComments」っていうのが外向けのRepositoryみたいな役割をするイメージ。

面倒なんで最初からコードで書いていく。 typescript-dddbaseという自作のライブラリを使っている。

Issue

Issueのidentityとして「IssueId」を定義した。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module Github {
    export class IssueId extends DDD.Identity<number> {
        constructor(id: number) {
            super(id);
        }
    }

    export class Issue extends DDD.Entity<IssueId> {
        constructor(
            public id: IssueId,
            public title: string,
            public body: string,
        ) {
            super(id);
        }
    }
}

IssueComment

Commentの部分。CommentはLocal Entityか。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module Github {
    export class IssueComments {

    }

    export class IssueComment extends DDD.Entity<DDD.NumberIdentity> {
        constructor(
            id: DDD.NumberIdentity,
            body: string
        ) {
            super(id);
        }
    }
}

さてどうしよう。

Repository

githubの方のRepository、今回はIssueで考えてしまったので、実装は割愛。そういうものがあると思ってください。 RepositoryRepositoryとかってアレですね・・・。

ともかくIssueRepository

を作るか。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module Github {
    export class IssueRepository {

        private apiClient = new GithubClient();

        issuesForRepository(repository: Repository): monapt.Future<Issue[]> {
            // 本来は repository.onwer と repository.name とかその辺から
            return this.apiClient.issuesForRepositry('yaakaito', 'typescript-ddd-lazyload').map((response, p) => {
                var factory = new IssueFactory();
                p.success(factory.issuesFromAPIResponse(response));
            });
        }
    }
}

monaptっていう自作のライブラリをまた使ってる。Scalaの Option Try Future を真似したライブラリ。僕はFutureが気に入っているし、Deferredはあんまり好きじゃないのでこっち。(JavaScriptなんでちょっと残念なAPIになってる)

GithubClientはgithubのAPIクライアントだと思ってください。(実装みたい人はコード見てください)

Factory

適当。

1
2
3
4
5
6
7
8
9
10
11
12
module Github {
    export class IssueFactory {
        issuesFromAPIResponse(response: Object[]): Issue[] {
            return response.map(issue => new Issue(
                new IssueId(parseInt(issue['id'], 10)),
                parseInt(issue['number'], 10),
                issue['title'],
                issue['body']
            ));
        }
    }
}

テスト用のアプリケーション

こんな感じで使ってみる。

1
2
3
4
5
6
7
$(() => {
    var repos = new Github.IssueRepository();
    repos.issuesForRepository('yaakaito/typescript-ddd-lazyload').onComplete(r => r.match({
        Success: issues => console.log(issues),
        Failure: error => console.error(error)
    }));
});

よさげ

やっと本題

ながかった・・・

本題のIssueCommentを考える。

当然Issueに属することになるわけなのだが、まずは初期化から考えよう。 Commentの総数は別に扱う、という話で、これはAPIからやってくるので、こういう感じ?

1
2
3
4
5
6
7
export class IssueComments {
    constructor(
        public count: number
      ) {

    }
}

Commentを引いてくるのに親のIssueの情報とかも必要だし、ポインタとnumberが必要か。

1
2
3
4
5
6
7
8
9
export class IssueComments {
    constructor(
        public count: number,
        private issueId: IssueId,
        private issueNumber: number
    ) {

    }
}

これをIssueにいれてあげるとこんな感じか、

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export class Issue extends DDD.Entity<IssueId> {
    comments: IssueComments;

    constructor(
        public id: IssueId,
        public issueNumber: number,
        public title: string,
        public body: string,
        commentCount: number
    ) {
        super(id);
        this.comments = new IssueComments(commentCount, id, issueNumber);
    }
}

これでIssueRepositoryから取り出したタイミングでCommentの総数は取得できるようになった。

Local Entity用のRepositoryを作る

今回の場合はCommentがLocal Entity。このRepositoryは公開したくないので、exportせずに作る。 他はさっきと同じ雰囲気で作っていって使う。Factoryも公開しなくていいかな。 (公開したくないのでプロパティにはしない)

さっきと一緒で本来はIssueから引けるとよさそうなんだけど、割愛。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/// <reference path="../../../../definitions/dddbase/dddbase.d.ts" />
/// <reference path="../../../../definitions/monapt/monapt.d.ts" />

module Github {
    export class IssueComments {
        constructor(
            public count: number,
            private issueId: IssueId,
            private issueNumber: number
        ) {

        }

        getAll(): monapt.Future<IssueComment[]> {
            var repository = new IssueCommentRepositry();
            // 本来はissueIdからIssueを取ってきたりするのだが略
            // ...
            return repository.commentsForIssue(null);
        }
    }

    export class IssueComment extends DDD.Entity<DDD.NumberIdentity> {
        constructor(
            id: DDD.NumberIdentity,
            body: string
        ) {
            super(id);
        }
    }

    class IssueCommentRepositry {
        private apiClient = new GithubClient();

        commentsForIssue(issue: Issue): monapt.Future<IssueComment[]> {
            return this.apiClient.issueCommentsForIssue('yaakaito', 'typescript-ddd-lazyload', '1').map((response, p) => {
                var factory = new IssueCommentFactory();
                p.success(factory.issueCommentsFromAPIResponse(response));
            });
        }
    }

    class IssueCommentFactory {
        issueCommentsFromAPIResponse(response: Object[]): monapt.Future<IssueComment[]> {
            return response.map(comment => new IssueComment(
                new DDD.NumberIdentity(parseInt(comment['id'], 10)),
                comment['body'],
            ));
        }
    }
}

こんな感じだろうか。使ってみる。

1
2
3
4
5
6
7
8
9
10
11
12
$(() => {
    var repositry = null;
    var repos = new Github.IssueRepository();
    repos.issuesForRepository(repositry).onComplete(r => r.match({
        Success: issues => {
            issues[0].comments.getAll().onSuccess(comments => {
                console.log(comments);
            })
        },
        Failure: error => console.error(error)
    }));
});

よさげ。

雑感

まじめに解説/実装例作ろうとすると途中が長過ぎて心折れる。 Issueの方が楽かと思ったけど最初からRepositoryでやればよかった!!!

コードはこちら