search
LoginSignup
2

More than 1 year has passed since last update.

posted at

updated at

InputをDomainレイヤーに渡すまで

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

はじめに

DDD-Community-JP(以下、DDDCJ)内で開催するモデリング会での、以下の話題についてお話しします。

いただいたご意見をもとに、もう少し深掘りしていきます。
どうするのがいい、という結論はありません。
メリデメ見て、どちらがいいかを選ぶのが良いか、それぞれのチームで選ぶのが大事と考えています。

前提

入力は何らかの形でドメイン層では値オブジェクトになる想定です。
また、レイヤーは以下の様に呼びます。

レイヤー名 アーキテクチャによる別名
プレゼンテーション層 インターフェースアダプター
インフラストラクチャ層 インターフェースアダプター
ユースケース層 アプリケーション層、アプリケーションビジネスルール
ドメイン層 エンタープライズビジネスルール

2つの選択

大きく、2つの選択があります。
1. プレゼンテーション層で、入力をVOに変換する。
2. プレゼンテーション層からユースケース層はプリミティブ型、ユースケース層でVOに変換する。

このお話をするにあたって大事な考え方の引用

冒頭ツイートに対する引用リツイートで、以下の様にお話をいただいています。
このお話をもとに、記事を書いています。

それぞれの考え方

両者の考え方、メリデメについて話します。
次の章でコードにして話しますが、長くなるのでまずは雰囲気で。

選択肢1: プレゼンテーション層で実施する

ポイントは2つです。

  • 入力の取り扱いをプレゼンテーション層の責務とする。ドメイン層のレベル(関心ごと)として扱えるところまで持っていく。
  • 入力を表す情報は、「入力そのまま(raw)」と「値オブジェクト」の2種類しかない。

image.png

ここで「違和感」と感じるものの一つは 「プレゼンテーション層から一つ飛ばしでドメイン層を使う」ところ。
ここはモデリング会でも議論になりましたが、依存方向は守られているので問題ないという考えです。

一方、この方式の危険性。
「ドメイン層が繋がってるから、ついプレゼンテーション層の責務をドメイン層に書いてしまう」こと。
ドメイン層と繋がってる分、チーム内でついついそこの書き分けを間違える人が出てくる危険ありです。

ドメインオブジェクト作成失敗のハンドリングが、二箇所に分散されるのも悩みです。

選択肢2: ユースケース層で実施する

ポイントは2つです。
- ドメインオブジェクトの組み立て責務は、ユースケース層の責務。

image.png

ドメインオブジェクトに関わる操作がユースケース層に統合されます。
バリデーション、作れなかった等のハンドリングなどもユースケース層で行われるため、ドメインオブジェクトに関する責務がまとまります。

一方、プリミティブ型のまま渡すことで、ユースケースとのやりとりが複雑でわかりにくくなる部分もあります。

具体的に

両方をコードで書いてみました。
使う題材が簡単すぎる、書き方が悪いなどでどちらかに寄って見える部分は、大目にみてやってください。

題材

日程登録の仕組みです。
年月日を指定して予定を登録しよう! というシンプルな内容です。

image.png

コードはgithubに置いています。(pythonです)

全体の流れ (前提として)

プレゼンテーションに仕事を持たせるため、外からの入力は「和暦」にしています。
ドメイン層の「予定日付」の関心はあくまで日付のみなので、ここの変換はプレゼンテーションでやっています。

インターフェースが受け取ったパラメータを、一度西暦変換メソッドにかけています。
(令和2年12月13日2020-12-13 にするイメージ)

その後、ユースケース層に西暦化された入力を渡し、Schedule エンティティを作ります。

最後、リポジトリに保存して終了です。

image.png

選択肢1: プレゼンテーション層で実施する

サンプルコード

入力を変換した戻り返す時点で、値オブジェクトに変換してしまいます。

flask_request.py(プレゼンテーション層)
from domain.schedule.schedule_date import ScheduleDate
from domain.schedule.schedule_title import ScheduleTitle

    def convert_from_wareki(self,
                            schedule_year: str,
                            schedule_month: str,
                            schedule_day: str,
                            schedule_title: str):
        schedule_date = self._convert_from_wareki_to_date(schedule_year,
                                                          schedule_month,
                                                          schedule_day)

        self.request["schedule_year"] = int(schedule_date.year)
        self.request["schedule_month"] = int(schedule_date.month)
        self.request["schedule_day"] = int(schedule_date.day)
        self.request["schedule_title"] = schedule_title

        domain_schedule_date = ScheduleDate(self.request["schedule_year"],
                                            self.request["schedule_month"],
                                            self.request["schedule_day"]
                                            )
        domain_schedule_title = ScheduleTitle(self.request["schedule_title"])
        return domain_schedule_date, domain_schedule_title

この後は、受け取った値オブジェクトをそのままユースケース層に渡し、ユースケース層で
エンティティを組み立てます。

flask_interface.py(プレゼンテーション層)
    try:
        schedule_date, schedule_title = request_obj.convert_from_wareki(
            schedule_year=input_year,
            schedule_month=input_month,
            schedule_day=input_day,
            schedule_title=input_title
        )

        use_case.register_schedule(
            schedule_date, schedule_title
        )
schedule_usecase.py(ユースケース層)
    def register_schedule(self, schedule_date: ScheduleDate, schedule_title: ScheduleTitle):
        try:
            schedule_id = ScheduleId()
            schedule_id.publish()
            schedule = Schedule(schedule_id, schedule_date, schedule_title)
        except Exception as e:
            print(e)
            raise Exception

        self.repository.save(schedule)

コードで見るメリデメ

プレゼンテーション --> ユースケースのやりとりが、「日付とタイトル渡します」だけになり、
やってることが読み取りやすいのがわかると思います。(選択肢2と見比べないと伝わらないかもしれないですが)
入力を、コードにとっての関心ある形に変換する責務を全うしてくれた効果が見えます。

選択肢1(日付とタイトルを登録するもの、というのが読み取りやすい)
        use_case.register_schedule(
            schedule_date, schedule_title
        )
選択肢2(入力を全て、粒度に関わらず並列で渡さないといけない)
        use_case.register_schedule(
            schedule_year, schedule_day, schedule_month, schedule_title
        )

一方で、 flask_request.py を見ていると 「元号変換のメソッド、ドメイン層の ScheduleDate に持たせてみようかな」という誘惑にかられる人がいるかもしれない ”におい” を感じる人がいるかもしれません。

今回エラーは作っていないですが、ドメインオブジェクト失敗エラーを上げる箇所を
プレゼンテーション層(flask_request.py)、ユースケース層(schedule_usecase.py) 両方にしないといけないのも悩みの種です。

もう一つ。
今回 python で書いているので気にしてない(できない)ことですが、公開範囲の話もあります。
プロジェクト構成によってはプレゼンテーション層からドメイン層を参照させてないこともあると思います。
この場合、全部公開してしまうか、プレゼンテーションで使うところだけ公開するか…も悩みどころです。

選択肢2: ユースケース層で実施する

サンプルコード

プレゼンテーション層では、まだドメイン層のオブジェクト組み立ては行いません。
プリミティブ型のまま進めます。

flask_request.py(プレゼンテーション層)
    def convert_from_wareki(self,
                            schedule_year: str,
                            schedule_month: str,
                            schedule_day: str,
                            schedule_title: str):
        schedule_date = self._convert_from_wareki_to_date(schedule_year,
                                                          schedule_month,
                                                          schedule_day)

        self.request["schedule_year"] = int(schedule_date.year)
        self.request["schedule_month"] = int(schedule_date.month)
        self.request["schedule_day"] = int(schedule_date.day)
        self.request["schedule_title"] = schedule_title

        return self.request["schedule_year"], self.request["schedule_month"], self.request["schedule_day"], self.request["schedule_title"]

値オブジェクト、及びエンティティの組み立ては、ユースケース層で行います。

flask_interface.py(プレゼンテーション層)
    try:
        schedule_year, schedule_month, schedule_day, schedule_title = request_obj.convert_from_wareki(
            schedule_year=input_year,
            schedule_month=input_month,
            schedule_day=input_day,
            schedule_title=input_title
        )

        use_case.register_schedule(
            schedule_year, schedule_day, schedule_month, schedule_title
        )
schedule_usecase.py(ユースケース層)
    def register_schedule(self, year: int, month: int, day: int, title: str):
        try:
            schedule_id = ScheduleId()
            schedule_id.publish()
            schedule_date = ScheduleDate(year, month, day)
            schedule_title = ScheduleTitle(title)
            schedule = Schedule(schedule_id, schedule_date, schedule_title)
        except Exception as e:
            print(e)
            raise Exception

        self.repository.save(schedule)

コードで見るメリデメ

こちらではプレゼンテーション層のロジックにドメイン層の話が出てこないので、
「誤ってプレゼンテーション層の責務がドメイン層に入り込む」危険はないのがわかります。

ドメインオブジェクト生成のロジックも一箇所の try節 にまとまっている点も、わかりやすいです。

ドメインオブジェクト生成は、全て一箇所にまとまっている。
        try:
            schedule_id = ScheduleId()
            schedule_id.publish()
            schedule_date = ScheduleDate(year, month, day)
            schedule_title = ScheduleTitle(title)
            schedule = Schedule(schedule_id, schedule_date, schedule_title)

一方で、前述の繰り返しになりますが入力の扱いは少しややこしいです。
このコードの例だと、年月日をまとめて、分解して、を繰り返すことになるのが書いてて「ちょっと…」という気分になることもあるかもしれません。

  1. 入力は 年 月 日 別々の str で受け取り
  2. 入力の 年 月 日 を date 型に変えて (和暦から西暦変換過程)
  3. 年 月 日 の int 型に変えて
  4. 最後、ドメインオブジェクトで 年月日 まとめた形に変える。

まとめ

長く色々書きましたが、はじめに書いた通りどちらがいいということは明言しません。
それぞれのメリデメを理解した上で、チームとしてどちらかを選択するのが良いと思います。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
2