会議室の予約のステータスについて考える紆余曲折


本記事は、 DDD-Community-Jp Advent Calendar 2020の6日目です。

はじめに

DDD-Community-JP(以下、DDDCJ)内では架空の会議室予約のドメインを考え、C#で実装する勉強会を行っています。

この勉強会の中で会議室の予約に対する考え方で、いろいろ試行錯誤をしていき、
いまのところ、こんな感じで落ち着いてきたなと思ったので、紆余曲折を含めて書いていこうと思います。

モデリング全体の紆余曲折については、以下の記事をご参照ください。

なお、この勉強会では日本語でプログラミングをしています。
日本語プログラミングの良さ等については、下記の記事をご参照ください。

予約をするときに、ステータスがあると考えた

始めに会議室の予約をする、と考えた時に、

予約無し → 予約済み → キャンセル済み もしくは 利用中

みたいな感じでステータスがあると考えました。

予約をする前の概念 → 予約希望?

先程のステータスの中で、以下の予約無しと予約済みの間にある概念ってなんだろう、ということが気になりました。

予約無し → (この間は???) → 予約済み 

予約をしようとした時に、その場所が埋まっているかどうか、予約できる時間帯なのかどうか、といった予約に関するルールがあると思います。

そして、予約が成立したということは、それは保存がされたとき。

当時はそう捉えていたので、予約に関するオブジェクトを組み立て、それを保存するまでの過程を何と表現すればよいのか。

それを、私たちは予約希望と捉えていました。


public class 予約希望
{
    private readonly MeetingRoom room;
    private readonly ReserverId reserverId;
    private readonly 予約期間 range;
    private readonly 想定使用人数 想定使用人数;

    public 予約希望(MeetingRoom room, ReserverId reserverId, 予約期間 range, 想定使用人数 想定使用人数)
    {
        this.room = room;
        this.range = range;
        this.想定使用人数 = 想定使用人数;
        this.reserverId = reserverId;
    }

    public MeetingRoom Room => room;
    public 予約期間 Range => range;
    public 予約年月日 予約年月日 => Range.予約年月日;

    public ReserverId ReserverId => reserverId;
    public 想定使用人数 想定使用人数_ => this.想定使用人数;

}

ユースケース層のメソッド

public bool 予約する(予約希望 予約希望) {

    予約済み群 予約希望日の予約の一覧 = repository.この日の予約一覧をください(予約希望.予約年月日);

    if (予約希望日の予約の一覧.かぶってますか(予約希望)) {
        return false;
    }

    repository.Save(予約希望);
    return true;
}

ここまで見るとわかりますが、予約希望はユースケースのメソッドに対するコマンドオブジェクトに近い振る舞いになっています。

予約希望自体に、予約に関するルールが記述できないのも問題です。
予約希望を組み立てるのが、実質このメソッドを呼ぶプレゼンテーション層になってしまうので、ルールを書いておくと、ドメイン知識の流出に繋がります。

予約をする前の概念は必要か?

予約希望、といった名前が悪いのかと思い、
仮予約とか、成立前予約とか、いろいろ考えておりましたが、

「成立しなかったならば、登録されずに捨てられるというあたりも勘案しておきましょう。
申請を受けてから登録されるまでに時間がかかるので一時的に保存しておくようであれば、区別する必要があるけれど、今回は自動的に登録まで一気に実行するだろうから区別する必要がどこまであるのか」

といった指摘を受けまして、今回考えたシステムの中で、申請前の概念を区別して表現するといった意味は無いだろうということで、そこを表現することは止めることにしました。1


public class 予約
{
    private 予約Id 予約Id;

    private 予約者Id よやくしゃ;
    private 利用期間.利用期間 りようきかん;
    private 会議室Id かいぎしつ;
    private 会議参加予定者 かいぎさんかよていしゃ;

    public 予約(予約者Id よやくしゃ, 利用期間.利用期間 りようきかん, 会議室Id かいぎしつ, 会議参加予定者 かいぎさんかよていしゃ)
    {
        if (!new 予約可能ルール.予約可能ルール().IsSatisfied(りようきかん))
        {
            throw new ルール違反Exception();
        }

        // 予約可能かどうか判定する?
        予約Id = 予約Id.Create();
        this.よやくしゃ = よやくしゃ;
        this.りようきかん = りようきかん;
        this.かいぎしつ = かいぎしつ;
        this.かいぎさんかよていしゃ = かいぎさんかよていしゃ;
    }

    /// <summary>
    /// 変更用のコンストラクタ
    /// </summary>
    public 予約(予約Id よやくid, 予約者Id よやくしゃ, 利用期間.利用期間 りようきかん, 会議室Id かいぎしつ, 会議参加予定者 かいぎさんかよていしゃ)
    {
        this.予約Id = よやくid;
        this.よやくしゃ = よやくしゃ;
        this.りようきかん = りようきかん;
        this.かいぎしつ = かいぎしつ;
        this.かいぎさんかよていしゃ = かいぎさんかよていしゃ;
    }

    public 予約 変更する(予約Id 予約Id, 予約者Id 予約者Id, 利用期間.利用期間 利用期間, 会議室Id 会議室Id, 会議参加予定者 会議参加予定者)
    {
        if (!new 予約変更可能ルール.予約変更可能ルール().IsSatisfied(りようきかん))
        {
            throw new ルール違反Exception();
        }

        return new 予約(予約Id, 予約者Id, 利用期間, 会議室Id, 会議参加予定者);
    }
}

ユースケースの引数は、コマンドオブジェクトとして用意するようにしました。
引数で貰ったオブジェクトの値から、予約を組み立てて問題なく生成ができれば保存をするといった形になっています。


public async Task 会議室予約するAsync(予約Request request)
{
    try
    {
        var よやく = new 予約(request.よやくしゃ,
            request.りようきかん,
            request.かいぎしつ,
            request.かいぎさんかよていしゃ);

        await _repository.Add(よやく);
    }
    catch (ルール違反Exception ex)
    {  
        throw new UseCaseException(ex);
    }

}

分け方に関して

実装をモブでやっている中で、ステータスをどう表現するかを考えたときに、以下の指摘が一つわかりやすい指針になるなと感じています。

  • 振る舞いが変わるならば、予約前後で別クラスというのはあり
  • 振る舞いが変わらないならば、同じクラスで状態として扱うというのもあり
    • いわゆる「区分」みたいなやつで区別するというイメージ

キャンセルされた予約に関心があるのは、予約する側では無い?

キャンセル済みの予約をどう表現するのか。
明らかに予約済みとキャンセル済みの予約は関心事が違うので、しっかりと分けておきたいです。

では予約の中で予約ステータスを保持しておき、「キャンセルする」のユースケースが実行されたときにキャンセル済みに変更するか?
それに関しても、予約として成立したものを「予約」と表現しているのに、その中にキャンセルされた、というのはおかしいのではないか、という違和感が残ります。

あれこれ悩ませていたときに、

「キャンセルされた枠があって、そこを予約する人は、そこが元々空いてたのか、キャンセルされてあいたのかは、気にしないですよね。
元々空いてたのかキャンセルされたのかが気になるのは、
利用実績とか利用履歴を追跡したい人の観点じゃないかしら」

という指摘に、たしかに! となりました。

キャンセルした時に、そのとき会議室を参加する予定の人たちには、通知をする必要はあれど、
予約をする観点から見た時に、キャンセル済み予約として概念を表現する必要性は無いと気づきました。

そのことから、予約の集約は、予約が成立した状態=予約済み を表現するものであり、予約成立前とキャンセル済みのものは、その範囲に入らないことが決まりました。

予約をする、予約の実績を参照する のコンテキスト分け

EventStormingでも分析していましたが、
「予約をする」と「会議室の利用」と「予約と利用の実績確認」は、今の所コンテキストを分けています。

それは、「予約をする」は利用前の時系列であり、
「会議室の利用」は当日会議室がどう利用されたか(予約しなくても使えるパターンもある)、
「予約と利用の実績確認」は、利用者側ではなく、会議室の管理者というアクターになってくる為、関心事が違うだろうとコンテキストを分けています。

こういったコンテキストを分けておくと、いま考えている関心事は、誰の関心事なのだろうかと考えることに繋がっていけそうだと感じました。

キャンセルという振る舞いは、予約自身が持つものか?

予約をキャンセルするという振る舞いについては、予約の集約が持つものだろうか? と考えた時に、「自分自身がキャンセルする」という行為に、またぎこちなさを感じました。
実際、予約をキャンセルする行為は、永続化されているデータを削除する行為です。
既存の予約が存在するかどうか、キャンセルしても良い予約なのか(まだ予約日当日を迎えていないなど)を判断するルールはあれど、それを予約集約の中でやるのは、収まりが悪い感じがしています。

「『キャンセルする』が実は、『特定のオブジェクトに持たせるとぎこちなくなる』というアレなのかもしれない」

上記のような指摘を受けて、これはおそらく重要な知識をもった手続きではあれど、ひとつの集約に置くのぎこちない感じがするので、ドメインサービスとして定義したほうが良いだろうと考えました。

まとめ

予約のステータスを考えたときに、以下のような気付きがありました。

  • ドメインオブジェクトとして考えていたのに、その中に振る舞いやルールが無い状態 → ドメイン貧血症になっていたら、どこか別にルールが存在してないか、もしくはその概念が本当に必要なのかを考える

  • あるオブジェクトのステータスを考えたときに、いまのコンテキストでは、このステータスに関心があるのだろうか? と一旦考える

  • キャンセルの概念はかなりのぎこちなさを感じるので、重要な分析ポイント

  • 振る舞いが変わるならば、予約前後で型を別に表現するのはあり。振る舞いが変わらないのであれば、ステータスを区分として持っておく

  • コンテキストの分ける候補として、時系列(事前に予約する、当日に予約したものを利用する)やアクターの違いに着目してみる

特に、振る舞いの変わる変わらないで、型で区別するか、ステータス区分を集約の中で保持しておくか、というのは自分ではモヤモヤしていたので、良い指針だなと感じました。


  1. もちろん、予約してから成立までの時間が掛かる(貸し出す側の確認とか、何かしら審査が必要とか)としたら、そこを表現するのに価値はあると思います