BIGLOBEの「はたらく人」と「トガッた技術」

DDD くらいできるようになりたいよねって話

f:id:biglobe-editor2:20200110114138j:plain

はじめに

基盤本部(開発部門)の小野田です。

私自身、BIGLOBE に 2019 年 7 月に転職したばかりで、ドメイン駆動設計(DDD)の実践歴は浅いのですが、最近は開発業務の他にも中途採用者の DDD 教育や 現場で DDD!2nd のドライバー役をする機会を頂くなど、DDD を広める活動にも少し関わっています。

その中で「DDD ムズイ」という言葉をよく聞いたので、DDD の実践に悩んでいる人向けにサンプル問題の解説を通して、実は DDD 自体は難しくないんだよってことを教える目的で本記事を書きました。

なお、この記事の内容やプログラムは、教育用に作成した架空のもので実在のサービスや団体などとは一切関係ありません。

TL;DR(最初に結論)

  • DDD 自体はドメインを中心にモデリングと実装をイテレーティブに繰り返す設計プロセスであり、モデリングとオブジェクト指向プログラミング(OOP)の理解があれば誰でもできます。
  • 難しいのは DDD 自体ではなくて、モデリングまたは OOP です。特に「良いモデル」を得ることは非常に難しいです。
  • なので「DDD ムズイ」と感じる人はモデリングと OOP の勉強をすると、DDD ができるようになるかもしれません。

サンプル問題

現場で DDD!2nd の増田氏のハンズオンで題材になった 鉄道料金の計算問題 をサンプル問題として取り上げます。

実業務を扱った問題のためやや複雑で、注意深く設計しないとコードが複雑になってしまいます。

料金計算について

仕様に明記されていない箇所は、以下のルールであるものとして実装しました。

  • 往復割引のほか団体割引でも 10 円未満の端数は切り捨てる仕様としました。
  • 団体割引で 31 人以上の団体も 8 人以上の団体が受ける割引を受ける仕様としました。また大人と子供が混在している場合は、大人を優先して無料にします。

回答

Kotlin でのサンプル問題の回答を GitHub に公開しました。

ただし画面は用意していないので bootRun した後 curl コマンドを実行する必要があります。

curl コマンドのサンプル

curl -D - -H "Content-type: application/json" -X GET -d '{"destination":"shinosaka", "trainType":"nozomi", "seatType":"reserved", "adults":2, "children":3, "departureDate":"2020-09-04", "tripType":"round-trip"}' localhost:8080/jr-pricing/apply

モデリング

まずは鉄道料金を計算する上で必要な情報を洗い出しましょう。例えば「料金」「運賃」「特急料金」「乗車人数」などがあると思います。

一方「申込者」や「申込経路(Web からなのか窓口からなのかなど)」は鉄道の業務には必要な情報だと思いますが、料金計算の業務には不要な情報です。

こんな感じで情報を拾い上げ、情報間の関係を整理すると以下のようなドメインモデルができあがります。

f:id:biglobe-editor2:20200110110416p:plain
ドメインモデル

パッケージごとに責務や設計意図などを説明します。

price

料金を計算するクラス群を集めたパッケージです。

fare

運賃を計算するクラス群を集めたパッケージです。

super_express_surcharge

特急料金を計算するクラス群を集めたパッケージです。先ほど載せたドメインモデルをもう少し詳細化すると以下のようなモデルになります。

f:id:biglobe-editor2:20200110104251p:plain
特急料金のドメインモデル

列車区分(のぞみ | ひかり)と座席区分(自由 | 指定)の組み合わせごとに特急料金計算サービスを用意し、あとは料金計算サービスの calculate を呼び出すと目的地(新大阪 | 姫路)までの特急料金が返る仕組みです。デザインパターンを知っている人には factory パターンを使ったと言えば伝わると思います。

実装を見るまではイメージが湧かない人もいるかもしれませんが、特急料金を計算するサービスを 1 つ用意して、そいつに列車区分と座席区分と目的地に応じて特急料金を計算させる構成にすると、SuperExpressSurchargeCalculationService が if 文まみれでごちゃごちゃしちゃうので、このような構成にしました。

Evans 氏も以下のようにオブジェクトの組み立て操作自体 1 つの責務として設計すべき(時もある)と言っています。

オブジェクトの生成は、それ自体が主要な操作になり得るが、複雑な組み立て操作は、生成されるオブジェクトの責務には合わない。そういう責務を組み合わせてしまうと、理解しにくく不格好な設計が作り出されるかもしれない。 (エリック・エヴァンスのドメイン駆動設計より)

ちなみに Season の置き場所には悩みましたが、私の中で「季節変動」と「割引」は違う概念であり、Season の利用者は SuperExpressSurcharge のみであることから本パッケージに置きました。

discount

割引に関するクラス群を集めたパッケージです。

団体を

  • 8 人以上 30 人以下の少人数グループ
  • 31 人以上の大人数グループ

の 2 つにグルーピングしました。理由は前者は「割引率」、後者は「割り引く人数」というように、同じ割引でも扱う対象が少し違うためです。

モデリングのアウトプット

Web にある多くの記事がドメインモデルのみを提示して実装に移りますが、モデリングに不慣れな人はモデルからコードにつなげることができないと思います。

その場合は詳細なクラス図、シーケンス図など、各自の理解度に応じて実装に必要な追加情報をアウトプットしましょう。別にモデリングのアウトプットは UML に限定されず、例えば料金計算がよくわからないのであれば以下のような表を作成しても構いません。

目的地 列車 座席 大人 子供 出発日 旅行区分 運賃 特急料金 料金
新大阪 のぞみ 自由 1 0 2019-09-04 片道 8910 5280 14190
新大阪 のぞみ 自由 1 0 2019-09-04 片道 - - 28380

ちなみに私のチームもドメインモデルのみということが多いですが、メンバの理解度に応じて適宜詳細なクラス図やシーケンス図を作成することもあります。

実装

ここまでくると実装を開始できます。

今回は DDD では有名なレイヤー化アーキテクチャを採用します。ただし今回用意するのは presentation 層、application 層、domain 層の 3 層のみです。

domain 層の実装

domain 層の主要なクラスの実装を見ながら、オブジェクト指向プログラミングの考え方を紹介します。

料金計算サービス

まずは主要クラスである料金計算サービスの実装を見ましょう。

PriceCalculationService

少し長いですが、実はこのクラス自身は大したことはしていません。入力値をコンストラクタで受け取り、割引やら運賃やら特急料金を計算する責務を持ったクラスを呼び出すだけです。オブジェクト指向ではこれを 委譲 と呼びます。

専門用語を出すと難しく感じるかもしれませんが、多くの人が普段やっている仕事の依頼と同じです。依頼者がある仕事をする責務を他の人に委ねるのと同じく、料金計算サービスは割引やら運賃やら特急料金の計算を専任の計算サービスに委ねています。

特急料金計算サービス

続いて特急料金計算サービスの実装を見ましょう。例えば「のぞみ自由席」の特急料金を計算するサービスの実装は以下のようになっています。単に目的地と特急料金の対応関係( map )を持つだけの小さなクラスです。

SuperExpressSurchargeCalculationServiceForNozomiFreeSeat

ちなみにこれら特急料金計算サービスを生成する factory の実装は以下の通りです。

SuperExpressSurchargeCalculationServiceFactory

モデリングの時はイメージが湧かなかったかもしれませんが、もしこれをクラス分けせず 1 つの特急料金計算サービスに実装していたら if 文まみれの辛いコードになっていました。個人的には if 文が 1 つのクラス/メソッドに集中しないように設計/実装するよう心がけています。

割引

続いて割引に移りましょう。割引については「往復割引を適用するクラス」「団体割引を適用するクラス」の 2 つに分割しました。両者に共通するのは tell-don’t-ask の原則 に従っていることです。つまり

  • 料金計算サービスが割引を適用するクラスに割引可能かを尋ねる。
  • 可能な場合は料金計算サービスが割引を適用するクラスの割引処理を呼び出す。

とはせず、いきなり「割引して!」とお願いしています。これで呼び出し元に if 文を書かなくて済みます。「結局呼び出し先で if 文書くから同じじゃん」と思うかもしれませんが、料金計算サービスに 4 つの if 文が増えるのと、割引の各クラスに if 文が 1 つずつ増えるのと、どちらが見やすい(保守しやすい)でしょうか?

反復する

さて、これで終わりではなくて、実装で得た知見をモデルにフィードバックします。

例えば「601km を超える場合は」の部分をコードでは Destination#isTooFar と微妙な名前にしてあります。これは私がこの部分の業務をよく理解できておらず、適切な表現が浮かばなかったためです。

他にも application 層は不要で presentation 層と domain 層の 2 層で良いのでは?などなど、いくつか修正すべき点があります。

まとめ

DDD は単に

  1. 業務を学んで
  2. 理解したことをモデルで表現して
  3. モデルの通りに実装する

を繰り返すプロセスです。その際に必要となるのは

  1. モデリングスキル
  2. オブジェクト指向設計/プログラミングのスキル

であり、別に DDD 固有のスキルは必要ありません。なので DDD できない!ムズイ!と感じる人は、1 と 2 のどちらが原因であるかを分析して対策すると、DDD ができるようになるかもしれません。

2020 年こそ、DDD をできるようになりましょう。

参考図書