チュートリアルでDDD体験: ドメインモデルの成長を紹介

プロダクト技術本部の川口です。

3年間、ビッグローブ光といった固定回線のインフラ部門に所属していましたが、今年の4月に BIGLOBE の基幹システムのリニューアルを推進していく部署に異動することになりました。

所属するチームでは、ドメイン駆動設計(DDD)で開発しています。

チームにジョインすると開発チュートリアルをやることになっており、そこで IntelliJ や Spring Boot での開発の仕方を学んだり、チュートリアルを通して DDD を学んだりします。

今回は、DDD のチュートリアルで実際に作成したドメインモデルがどういう風に成長していったかについて紹介します。

勤怠管理アプリ チュートリアル

お題は GitHub のパブリックリポジトリに公開されています。 https://github.com/biglobe-isp/kintai

チュートリアルの内容を簡単に説明すると既存のコードと仕様に沿ってリファクタリングしていくといったものです。

このチュートリアルでは以下のような流れで DDD を学んでいきます。

  1. チュートリアルの課題をよく読む
  2. ドメインモデルを作成する
  3. ドメインモデルのレビューを受ける
  4. 2 と 3 を繰り返しある程度めどがついたらコードを書く
  5. コードのレビューを受ける
  6. 2, 3, 4, 5 を繰り返す

今回は 2, 3, 4 の工程 をメインに紹介します。 ドメインモデルは PlantUML を使って作成します。

※今回は触れてませんがJavaでコーディングしています。

初期ドメインモデル

まず最初にドメインモデルを作ってみようといわれてみたものの、クラス図の作り方とかよくわかっておらずインターネットで調べながら作成し、最初にレビューを受けたモデルはこうなりました。

図1
最初に作成したドメインモデル図

((ヽ(´Д`;)ノ)) ワカラン …

データモデルにもなってないような感じです。

チームメンバーとモブプロをしながらクラス図の書き方のアドバイスをもらいつつ、このとき受けたレビューでは

  • ドメインを用いてユーザが必要とする形に加工するサービスクラスが必要
  • どのクラスで休憩時間を抜いた実際の労働時間と残業時間を算出するかモデルに記述する
  • 勤務情報の登録で使うクラス、月間累計時間の参照で使うクラスでパッケージを切る

といったコメントをいただきました。

チュートリアルではドメインエキスパートにあたるような人物は存在しないため、ユビキタス言語のように開発者とドメインエキスパートが使う共通のドメイン用語も明確になっていません。
問題文を再度読みながら考察すると、時間にはx時間といった幅とx時x分といった時点を表す表現や休憩時間を含めた / 含めない労働時間があることに気づきました。
弊社で使っている勤怠管理システムを見れば、より適切なドメイン用語が見つけられると思い参考にしてみました。

勤怠管理システムと考察した結果はこうなりました

  • 時間は幅、時刻は時点と定義する
  • 休憩時間を含めない実際の労働時間 =「実働時間」と命名する
  • 実働時間と残業時間を算出するには勤務開始・終了時間と休憩時間が必要そう
  • 始業時間と終業時間、休憩時間は「就業規則」と言ってもよさそうなので一つにまとめたクラスを作成する

そのため、弊社で使っている勤怠管理システムの用語を参考にクラス名をつけました。

上記のコメントと考察を踏まえ、修正したモデル図は以下となりました。

図2
修正したドメインモデル図

図1と比べるとドメインの理解が進んでる感じがしています。

図1のモデルには登場しなかった「実働時間」や「残業時間」、「就業規則」といったクラスが登場しています

図1のモデルではよくわからず矢印をつけていましたが、参照するクラスに矢印をつけました。


このモデル図でレビューを受けたときのコメントは

  • CSVに保存される6項目を保持するクラスをつくる
  • 6項目を保持するクラスを永続化するためのリポジトリを定義して、サービス層から呼び出す線を引いてみる

です。

このチュートリアルでは日付、勤務開始時間、勤務終了時間、実働時間、残業時間、勤怠登録をした時刻をCSVに保存する必要があります。
サービスクラスから呼び出してデータを永続化するためのリポジトリも必要です。

以上の観点に加え、再度モデル図を見直した際に以下のように考察しました。

  • 勤務時間の登録と月間で集計した勤務時間の参照、この2つのユースケースを実現するサービスクラスが必要そう
  • 勤務登録する際に必要な日付と始業時間・終了時間を保持するクラスが必要そう
  • 勤務情報クラスが永続化したいクラスのように見えるが、永続化したい情報が足りておらず、サービス層でドメインを扱うには不便な状態になっている
  • 残業時間を算出するために、実働時間と就業規則の始業・終業時間から算出される規定勤務時間が必要そう

上記のコメントと考察を踏まえて、再度モデルを修正しました。

中期ドメインモデル

図3 再度修正したドメインモデル図

変更点としては、新たに勤務情報登録サービスと勤務情報リポジトリを追加しました。
実働時間の算出のために就業規則パッケージを作成し、規定勤務時間クラスを作成しました。

この図を見て「おやっ」と思った人がいるかもしれません。
リポジトリのインターフェースはドメイン層に置く人が多いかもしれませんが、チームではサービス層に置くようにしています。
理由としては、リポジトリはデータの永続化が責務でありアプリケーションの機能と認識しました。そのためビジネスルールを表現するドメイン層ではなく、アプリケーションの要求を表現するサービス層の方が適していると考えたためです。

このモデル図のレビューでは以下の内容のコメントをいただきました。

  • 就業規則を変更するために就業規則パッケージと変更用のリポジトリを用意してもよい、勤務情報登録サービスがリポジトリから就業規則を受け取れるようにする
  • 時間と時刻の表現の統一
  • 就業規則 -> 休憩時刻の線は 1 対 1..n

ここまでくるとクラス名や多重度にも着目したり、お題には書かれていないですが今後の仕様変更やビジネスの要求のために拡張性を持たせて変更を容易に行えるようにモデルを考える必要が出てきました。

このモデルを作り終えた時点でコードを書き始めました。

コードを書いていると、モデルでは現状こうしてるけど実装してみるとこっちのほうがいいなという場合がありました。
月間累計時間を算出する際に中期のモデル図の内容で書いていたところ、月毎の勤務実績の集合である月間勤務実績があったほうが実装がしやすいと感じたことと、累計実働時間と累計残業時間を別々のクラスにするメリットを余り感じなかったため、変更しました。

後期ドメインモデル

最終的に以下のようなモデルになりました。

図4 完成したドメインモデル図

主な変更としては、就業規則が後で変更になる場合を考慮して就業規則パッケージと就業規則リポジトリを追加したこと、
月間勤務実績クラスとそれを参照できる月間勤務実績リポジトリ、累計実働時間、累計残業時間を持つ月間累計勤務時間クラスを追加しました。

仕様が変わる可能性を踏まえて、最終的に4つのパッケージに分類しました

  • 勤務情報
  • 就業規則
  • 月間累計勤務時間
  • 勤務時刻入力

就業規則は会社の勤務ルールが変わる場合、月間累計勤務時間は参照したい情報が変わる場合、勤務情報は仕様が変わる可能性は低いと思いましたが、モデルの中心となるドメインとして分類しました。
中期モデルでは勤務時刻入力は勤怠情報パッケージに入ってましたが、勤務時刻入力の内容は加工されて勤怠情報として登録されること、今回の要件にはありませんが有給休暇の情報を入力できるように変更する場合(そうなるとパッケージ名の変更の検討が必要そうですが)も考えて、パッケージを分けました。


最後に 専門家の方に説明しながら見てもらいました。

  • 勤怠の怠という意味合いにあたるような概念は今回のモデルだと出てこない
    • 欠勤とか遅刻とかの概念が出てくるのであれば、違和感はない
  • Input / 入力という言葉はドメインの言葉としてはおさまりが悪い
    • システムとしては入力してるような動作かもしれないが、ドメインはビジネスのルールを表現するので入力という言葉は持ち込みたくない

といったコメントをいただきました。

適当なクラス名を1つ考えるだけでも難しいなと感じました。

学んだこと、感想

  • モデリング、特にクラス名に適当な名前を付けることが難しかった
  • どのパッケージ、クラスでビジネスロジックを表現するのかが難しかった
  • ドメインモデルの描き方やどれくらいの単位でクラス・パッケージを分割すればよさそうかといったイメージができるようになった
  • ドメインモデルをイテレーティブに改良することが重要と実感できた

書籍だけで DDD を学ぼうとすると、抽象的な内容が多かったり、実感が湧かなかったりして理解するのが難しいですが、お題を通して DDD を実践したことで理解が進み、モデリングやコーディングのスキルを磨くことができました。
あくまで今回はチュートリアルで、実際の業務ではさらに複雑なビジネスロジックが存在します。複雑なビジネスに対してドメインエキスパートとメンバーで議論しながら「ユビキタス言語」や「境界付けられたコンテキスト」を見つけ出していくことが DDD の醍醐味ではないかと思います。

そういった経験を今後増やして、ビジネスの変化に強くて柔軟性がある設計ができるようになりたいです。

※ IntelliJ は、JetBrains s.r.o.の商標または登録商標です。

※ GitHub は、GitHub Inc.の商標または登録商標です。

※ Javaは、Oracle、その子会社及び関連会社の米国及びその他の国における登録商標です。

"); $("#box2 .footer-module-inner").wrapAll(""); $("#box2 .hatena-module-category a.category-はたらく人を知る,#box2 .hatena-module-category a.category-カルチャーを知る").each(function(){ var txt = $(this).text(); $(this).text(txt.replace("を知る", "")); }); }); $(".urllist-with-thumbnails .urllist-item .urllist-categories").each(function(){ $(this).children("a.urllist-category-link").each(function(){ if( $(this).hasClass("category-TechBlog") || $(this).hasClass("category-はたらく人を知る") || $(this).hasClass("category-カルチャーを知る") || $(this).hasClass("category-お知らせ") ){ var target = $(this).parent(".urllist-categories").prevAll(".urllist-image-link"); var txt = $(this).text(); txt = $.trim(txt); $(this).clone(true).removeClass().addClass("main-category main-category-"+txt).insertAfter(target); return false; } }); }); $(".archive-entries .archive-entry .categories").each(function(){ $(this).children("a.archive-category-link").each(function(){ if( $(this).hasClass("category-TechBlog") || $(this).hasClass("category-はたらく人を知る") || $(this).hasClass("category-カルチャーを知る") || $(this).hasClass("category-お知らせ") ){ var target = $(this).parent(".categories").nextAll(".entry-thumb-link").children(".entry-thumb"); var txt = $(this).text(); txt = $.trim(txt); $(this).clone(true).removeClass().addClass("main-category main-category-"+txt).appendTo(target); return false; } }); }); $("a.category-はたらく人を知る,a.category-カルチャーを知る").each(function(){ var txt = $(this).text(); $(this).text(txt.replace("を知る", "")); }); setTimeout(function(){ $(".hatena-urllist.entries-access-ranking li.urllist-item .urllist-categories").each(function(){ $(this).children("a.urllist-category-link").each(function(){ if( $(this).hasClass("category-TechBlog") || $(this).hasClass("category-はたらく人を知る") || $(this).hasClass("category-カルチャーを知る") || $(this).hasClass("category-お知らせ") ){ var target = $(this).parent(".urllist-categories").prevAll(".urllist-image-link"); var txt = $(this).text(); txt = $.trim(txt); $(this).clone(true).removeClass().addClass("main-category main-category-"+txt).insertAfter(target); return false; } }); }); $(".hatena-urllist.entries-access-ranking a.category-はたらく人を知る,.hatena-urllist.entries-access-ranking a.category-カルチャーを知る").each(function(){ var txt = $(this).text(); $(this).text(txt.replace("を知る", "")); }); },1000); });