本記事は、DDD-Community-Jp Advent Calendar 2020の13日目です。
はじめに
DDD-Community-JP(以下、DDDCJ)内で開催するモデリング会での、以下の話題についてお話しします。
前回書いたときも議論に上がった、プレゼンテーション層でVO作ることの是非。
— 98lerr (@98lerr) November 8, 2020
プレゼンテーションからユースケースにするとき、プリミティブよりVOで渡した方が、コード全体で「統一した型で表現できる」というのがしっくりきた。
むしろ、積極的にVOを使うべきという話。#ModelingKai
いただいたご意見をもとに、もう少し深掘りしていきます。
どうするのがいい、という結論はありません。
メリデメ見て、どちらがいいかを選ぶのが良いか、それぞれのチームで選ぶのが大事と考えています。
前提
入力は何らかの形でドメイン層では値オブジェクトになる想定です。
また、レイヤーは以下の様に呼びます。
レイヤー名 | アーキテクチャによる別名 |
---|---|
プレゼンテーション層 | インターフェースアダプター |
インフラストラクチャ層 | インターフェースアダプター |
ユースケース層 | アプリケーション層、アプリケーションビジネスルール |
ドメイン層 | エンタープライズビジネスルール |
2つの選択
大きく、2つの選択があります。
- プレゼンテーション層で、入力をVOに変換する。
- プレゼンテーション層からユースケース層はプリミティブ型、ユースケース層でVOに変換する。
このお話をするにあたって大事な考え方の引用
冒頭ツイートに対する引用リツイートで、以下の様にお話をいただいています。
このお話をもとに、記事を書いています。
結論としては「揃っていればどちらでも実装に耐えうる」なんですが、そこに至る論拠として「引数の値からVOを組み立てるのはどの層の責務か?」という議論が大切だと思います。「スッキリ書ける」だけが根拠だと後々困る判断になることがあります。(続)
— 松岡@ログラス/DDD,アジャイル (@little_hand_s) November 8, 2020
それぞれの考え方
両者の考え方、メリデメについて話します。
次の章でコードにして話しますが、長くなるのでまずは雰囲気で。
選択肢1: プレゼンテーション層で実施する
ポイントは2つです。
- 入力の取り扱いをプレゼンテーション層の責務とする。ドメイン層のレベル(関心ごと)として扱えるところまで持っていく。
- 入力を表す情報は、「入力そのまま(raw)」と「値オブジェクト」の2種類しかない。
ここで「違和感」と感じるものの一つは 「プレゼンテーション層から一つ飛ばしでドメイン層を使う」ところ。
ここはモデリング会でも議論になりましたが、依存方向は守られているので問題ないという考えです。
一方、この方式の危険性。
「ドメイン層が繋がってるから、ついプレゼンテーション層の責務をドメイン層に書いてしまう」こと。
ドメイン層と繋がってる分、チーム内でついついそこの書き分けを間違える人が出てくる危険ありです。
ドメインオブジェクト作成失敗のハンドリングが、二箇所に分散されるのも悩みです。
選択肢2: ユースケース層で実施する
ポイントは2つです。
- ドメインオブジェクトの組み立て責務は、ユースケース層の責務。
ドメインオブジェクトに関わる操作がユースケース層に統合されます。
バリデーション、作れなかった等のハンドリングなどもユースケース層で行われるため、ドメインオブジェクトに関する責務がまとまります。
一方、プリミティブ型のまま渡すことで、ユースケースとのやりとりが複雑でわかりにくくなる部分もあります。
具体的に
両方をコードで書いてみました。
使う題材が簡単すぎる、書き方が悪いなどでどちらかに寄って見える部分は、大目にみてやってください。
題材
日程登録の仕組みです。
年月日を指定して予定を登録しよう! というシンプルな内容です。
コードはgithubに置いています。(pythonです)
全体の流れ (前提として)
プレゼンテーションに仕事を持たせるため、外からの入力は「和暦」にしています。
ドメイン層の「予定日付」の関心はあくまで日付のみなので、ここの変換はプレゼンテーションでやっています。
インターフェースが受け取ったパラメータを、一度西暦変換メソッドにかけています。
(令和2年12月13日
を 2020-12-13
にするイメージ)
その後、ユースケース層に西暦化された入力を渡し、Schedule
エンティティを作ります。
最後、リポジトリに保存して終了です。
選択肢1: プレゼンテーション層で実施する
サンプルコード
入力を変換した戻り返す時点で、値オブジェクトに変換してしまいます。
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
この後は、受け取った値オブジェクトをそのままユースケース層に渡し、ユースケース層で
エンティティを組み立てます。
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
)
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と見比べないと伝わらないかもしれないですが)
入力を、コードにとっての関心ある形に変換する責務を全うしてくれた効果が見えます。
use_case.register_schedule(
schedule_date, schedule_title
)
use_case.register_schedule(
schedule_year, schedule_day, schedule_month, schedule_title
)
一方で、 flask_request.py
を見ていると 「元号変換のメソッド、ドメイン層の ScheduleDate
に持たせてみようかな」という誘惑にかられる人がいるかもしれない ”におい” を感じる人がいるかもしれません。
今回エラーは作っていないですが、ドメインオブジェクト失敗エラーを上げる箇所を
プレゼンテーション層(flask_request.py
)、ユースケース層(schedule_usecase.py
) 両方にしないといけないのも悩みの種です。
もう一つ。
今回 python で書いているので気にしてない(できない)ことですが、公開範囲の話もあります。
プロジェクト構成によってはプレゼンテーション層からドメイン層を参照させてないこともあると思います。
この場合、全部公開してしまうか、プレゼンテーションで使うところだけ公開するか…も悩みどころです。
選択肢2: ユースケース層で実施する
サンプルコード
プレゼンテーション層では、まだドメイン層のオブジェクト組み立ては行いません。
プリミティブ型のまま進めます。
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"]
値オブジェクト、及びエンティティの組み立ては、ユースケース層で行います。
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
)
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)
一方で、前述の繰り返しになりますが入力の扱いは少しややこしいです。
このコードの例だと、年月日をまとめて、分解して、を繰り返すことになるのが書いてて「ちょっと…」という気分になることもあるかもしれません。
- 入力は 年 月 日 別々の str で受け取り
- 入力の 年 月 日 を date 型に変えて (和暦から西暦変換過程)
- 年 月 日 の int 型に変えて
- 最後、ドメインオブジェクトで 年月日 まとめた形に変える。
まとめ
長く色々書きましたが、はじめに書いた通りどちらがいいということは明言しません。
それぞれのメリデメを理解した上で、チームとしてどちらかを選択するのが良いと思います。