のんラボ

社内でドメイン駆動設計入門の読書会 #12

2020/05/18 2020/05/18 社内でドメイン駆動設計入門の読書会 #12

社内でドメイン駆動設計入門の読書会 #12

こんにちは。Nonです。

今回も会社で読書会をしている話をしようと思います。

内容は控えめに、ディスカッションの内容重視で書いていきたいと思います。

より具体的なコードや内容がみたい!という方は購入しましょう!

前回:会社でドメイン駆動設計入門の読書会 #11

読んでいる本

読んでいる本はこちらのドメイン駆動設計です。

ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本


以前の記事でも言っていたように、業務でDDDを利用して開発することが多くなったのですが、DDDに精通している人が少ないという問題がありました。

そこで、その精通している人が読書会をしようかと誘ってくださいまして、是非にと参加させていただきました。

進行方法

読書会の進行方法は

  1. 今回読書の対象にする章を決める。
  2. 10〜20分間その章を読む
    1. 読み終わってしまった人は、もう一周読み直すか、次の章に進んでもらう
  3. その後40分間で、その章に対する疑問や考え方をディスカッションする
  4. 1〜3を毎週定期的に行う

という進行方向となっています。

社内で読書会をするのはこれが初めてなので、進行方法はもっといいのあったら教えて下さい。

今回読んだ内容

  • ドメインのルールを守る「集約」
  • 集約とは
    • 集約の基本的構造
    • オブジェクトの操作に関する基本的な原則
    • 内部データを隠蔽するために
  • 集約をどう区切るか
    • IDによるコンポジション
  • 集約の大きさと操作の単位
  • 言葉との齟齬を消す
  • まとめ

ディスカッション

今回はより現実的な話

今までに挙がった例はどれも単体での動作のみに注目してきましたが、現実のドメインは様々なEntityが複雑に関連しています。

そうなるとEntityは肥大化していきますし、振る舞いも相当なものになるでしょう。

そういうときに、どのようにまとめるべきなのか。ということを話題にしていました。

正直まだよくわからない。

最初から申し訳ないですが、集約難しいです。
現実の関心事を論理的に切り分けるのは一苦労です。
なので、この本にかかれている内容を一部抜粋して、私の考えを記していきたいと思います。

ユーザーとサークルの関係

ユーザーはサービスを利用するにあたって、必須な情報でしょう。

  • ログインするため
  • アプリを利用するため
  • ログを残すため

色々なことにユーザー情報が利用されることでしょう。

対して、サークルも同様です。

  • アプリをより楽しく利用するため
  • サークル内でやり取りすることがアプリの真髄

など、サークル作ることは必須ではありませんが、サークル作らないことはこのアプリにとって利用していないのとほとんど変わらない場合を考えます。

するとユーザー⇔サークルは密接関係を持つことがわかります。

サークルとユーザーのドメインは?

ユーザー作成にサークルが関係無いことはわかります。

サークル作成にはユーザーが必須に感じますね。

copied.class UserService
{
    private $circleFactory;

    public function __construct(CircleFactoryInterface $circleFactory)
    {
        $this->circleFactory = $circleFactory;
    }

    public function createCircle(CircleName $circleName): Circle
    {
        return $this->circleFactory->create($circleName);
    }
}
copied.class Circle
{
    private $id;
    private $circleName;
    private $members;

    public function __construct(int $id, CircleName $circleName, MemberCollection $members)
    {
        $this->id = $id;
        $this->circleName = $circleName;
        $this->members = $members;
    }

    // ...(略)
}

class CircleName
{
    private $circleName;

    public function __construct(string $circleName)
    {
        if (mb_strlen($circleName) > 50) {
            throw new OverflowException('サークル名は50文字以内です。');
        }
        $this->circleName = $circleName;
    }

    public function __toString(): string
    {
        return (string)$this->circleName;
    }
}

class MemberCollection
{
    private $members;

    public function __construct(array $members = [])
    {
        foreach ($members as $member) {
            if ($member instanceof User) {
                throw new RuntimeException('Member is must be User type.');
            }
        }
        $this->members = $members;
    }

    public function toArray(): array
    {
        $result = [];
        foreach ($this->members as $member) {
            $result[] = [
                'id' => $member->id(),
                'name' => $member->name(),
            ];
        }
        return $result;
    }
}

のような感じでしょうか。ユーザーサービスはサークルEntityを作成するふるまいを持ちます。

このとき、注目すべきEntityがユーザーEntityとサークルEntityで2つ出てきます。

ユーザーEntityはユーザーの名前を変更する振る舞いを持ちますが、今回の注目ごとはサークルの作成です。果たしてこの振る舞いがこのユースケースと関係あるでしょうか?

このふるまいがあるせいで、関心事が散漫とならないでしょうか

また、このコードを是とすると、changeName以外のふるまいがユーザーEntiryに集中することになるので、サークル作成ドメインに違う関心事が書かれたコードがどんどんと追加されることになっていまいます。

ドメインごとにUserEntityを作成するべき?

もしかしたら境界づけられたコンテキストの方の分野かもしれませんが、ドメインごとにEntityを分ける戦略が考えられます。

copied.src┓
   ┣User┓
   ┃    ┣CreateUser
   ┃    ┣User
   ┃    ┣UserName
   ...
   ┣Circle┓
   ┃      ┣CreateCircle
   ┃      ┣Circle
   ┃      ┣CircleName
   ┃      ┣User
   ┃      ┣UserService
   ...
   ┣Domain
   ...

うーん。でもCircleに関心事があるドメインにUserServiceがあるのが少し違和感。本では容赦なく使用していましたが、Circleを作成するにはUserにも関心が向くという知識がドメインに与えられるのでいいことなのかもしれない

EntityはEntityを持つことがある?

copied.class MemberCollection
{
    private $members;

    public function __construct(array $members = [])
    {
        foreach ($members as $member) {
            if ($member instanceof User) {
                throw new RuntimeException('Member is must be User type.');
            }
        }
        $this->members = $members;
    }

    public function toArray(): array
    {
        $result = [];
        foreach ($this->members as $member) {
            $result[] = [
                'id' => $member->id(),
                'name' => $member->name(),
            ];
        }
        return $result;
    }
}

に書いたように、CircleはMemberとしてUserを持ちます。

copied.if ($member instanceof User) {
    throw new RuntimeException('Member is must be User type.');
}

の部分。

なので、CreateCircleというドメインにEntityが2つあるのは是とするのが、普通の考えなのかもしれません。

Circleに所属するのはUserなので、Userの知識を持つUserをCircleが持つというのは自然な考えだと思います。

この辺はもっとドメイン駆動設計に慣れたらまとめ直したい。

正直、腑に落ちていない点が少しあります。この状態でかくあるべきというのは危険ですので、これ以上の展開は避けておきますね。

最後に

ちょっと難しい話でした。それぞれのEntity同士の関係になると一気に難しくなった気がします。ここを理解(納得)できないと、実装も揺らぎそうで怖いです。

次回は複雑な嬢他県を表現する「仕様」です。
また記事にしますので、その時はよしなに。

.