ネクストデザイン有限会社  |  English  
更新:  初版:2016-11-03

ドメイン駆動設計ドメインモデルの継続的なテスト


はじめに


ドメイン駆動設計の考え方とその実践方法について提案します。

ドメインモデルからアプリケーションを自動生成し、ドメインモデルの継続的なテストを可能にします。

イテレーションが軽量になり、反復サイクルも短縮され、開発者はドメインに集中できます。


ドメイン駆動設計で結果を出すためには、設計スキルとモデルの両方を段階的に洗練していくことがポイントです。

そのためには、イテレーションを軽快にする工夫と仕組みが必要です。


紹介した自動生成ツールは、下の「ダウンロード」で公開しています。


ドメイン駆動設計の基本

ドメイン駆動設計とは

ドメイン駆動設計は、 Eric Evans氏が、著書「エリック・エヴァンスのドメイン駆動設計」で提唱するソフトウェア設計手法です。

原著の初版は2003年の出版です。日本語版は2011年に出版されました。

最近のことではありませんので、すでに日々実践されている方も多いと思います。

※ドメイン駆動設計 Domain-driven design, DDD

※「エリック・エヴァンスのドメイン駆動設計」: 翔泳社 (ISBN 9784798121963) (本記事では DDD本 と略記します)


書籍「エリック・エヴァンスのドメイン駆動設計」(以下、DDD本) には、多くのことが書かれています。

ドメイン駆動設計を簡潔にまとめることは容易ではありません。

また、読む側の関心事や経験によっても、ドメイン駆動設計の受けとめ方は異なるかもしれません。

ここでは、開発方法論としての見方と、「アナリシスパターン」のようなパターン集としての見方、2つの見方で考えてみます。


まず、開発方法論として見た場合、ドメイン駆動設計は "何か新しい特別な手法" を示しているわけではありません。

その代わりに、既存の手法を活用します。

既存の手法には、すでに多くの実績やノウハウが蓄積されています。

主に、 モデル駆動開発オブジェクト指向モデリング・プログラミングアジャイル手法 を活用します。


つぎに、設計や構築のためのパターン集として見た場合、DDD本には多くのパターンやガイドラインが示されています。

ただ、「デザインパターン」や「アナリシスパターン」のような一覧形式ではありませんので、分かり難いかもしれません。

本記事でも、主要なキーワードとして、幾つか挙げています。全容はDDD本を参照してください。

※デザインパターン: エリック・ガンマ、ラルフ・ジョンソン、リチャード・ヘルム、ジョン・ブリシディース著

※アナリシスパターン: マーチン・ファウラー著


ただし、気をつけなくてはいけないのは、パターンに振り回されたり、多くのパターンを取り入れることが、ドメイン駆動設計ではないという事でしょう。


ドメイン駆動設計に関しては、DDD本以外にも多くの情報かあります。

ここでは、その中から、「Domain-Driven Design Quickly」を紹介します。

この文献は、Abel Avram氏とFloyd Marinescu氏の共著で、徳武 聡氏によって翻訳されたものです。

A4サイズで90ページ程で、よくまとまっていて、日本語も分かり易く、お薦めします。

[目次に戻る]

考え方

ドメイン駆動設計の基本となる考え方です。

   アプリケーションの本質はドメインです。

   ドメインを抽象化したドメインモデルを土台にして、アプリケーションを構築します。

   ドメイン知識からユビキタス言語を導き出し、チーム内のコミュニケーションに使用します。

   ドメインモデル、実装コード、ユビキタス言語 の3つを常に一致させて、アプリケーションをイテレーティブに構築します。

   アプリケーションの設計・構築活動に有用なモデリングパターンやガイドラインを示します。

いきなり、DDD固有のキーワードを使いましたが、以降で少しづつイメージを掴んで行ってください。


手法的には、すでに実績のある3つの手法と、そこで培われたノウハウを活用します。

   モデル駆動開発

   オブジェクト指向技術

   アジャイル開発


そして、いくつかの方針を追加します。

   「ドメインモデル貧血症」と言われるような、責務が欠落したクラスは作りません。

   「分析モデル」や「設計モデル」のように、工程や担当によってモデルを分離しません。

   1つのモデルを作成し、それを継続的に洗練し、使い続けます。

   コミュニケーションや成果物には、ユビキタス言語の使用を徹底します。

   ユビキタス言語、モデル、実装の間に整合性を保ち、相互に追跡可能にします。

[目次に戻る]

始め方

ドメイン駆動設計を導入し、スキルアップと期待される結果を得るためには、工夫が必要です。

ドメイン駆動設計を必要以上に難解に考えたり、最初からすべてを実践しようとすると、うまくいかないでしょう。


最初はドメイン内の幾つかのコアモデル要素から始めます。

例えば、医療ドメインであれば、医師や病室などです。

できれば、ドメインにおいて主要なオブジェクトで、オブジェクト定義が明確なものが良いと思います。

オブジェクト定義とは、それが何ものかを定義した、短い文章です。

そこに使用されているオブジェクト名や単語は、「ユビキタス言語」の構成要素、語彙です。

設計が進捗するにつれて、他の関連するオブジェクトが増え、オブジェクト定義も専門的になっていくでしょう。


ドメインモデルとユビキタス言語のリファクタリングを繰り返しながら、設計スキルとドメインモデルの両方を、段階的に洗練していきます。

いろいろなモデリングパターンやプラクティスを、一気に取り入れて使いこなすのは難しいことです。

それらは、段階的に取り入れ、体得し、ドメインモデルを深化させていくことが重要です。


ただし、開発が進むにつれてイテレーション (繰り返し) は重くなる傾向があります。

その結果、現実的な工数では繰り返すことができなくなるプロジェクトもあります。

この問題を解消できる開発環境が必要です。開発環境について、後述の「実践方法 - 開発プロセス」で提案します。

[目次に戻る]

最初の壁と利口なUIの罠

ドメイン駆動設計やオブジェクト指向を始めるときの、最初の壁について紹介します。

折角、ドメイン駆動設計やオブジェクト指向に取り組んだけれど、結局、何も変えられないということがあります。

原因の1つは、下図に示すような「立ち上がり期」の停滞感です。

ここでの停滞とは、チームが空回り気味で、具体的な成果物 (例:動作する画面など) が出てこないような状態です。

打開策が見つからずに、次節に述べるようなアンチパターンに後戻りするチームもあります。

導入した時の立ち上がりの比較

※注意:上のグラフは感覚的なものです。

この立ち上がり期の停滞感を乗り越えなければ、オブジェクト指向やドメイン駆動設計の効果を得られる前に、従来型に戻ってしまうことになります。

[目次に戻る]

利口なUIとは

DDD本は、深い考察やモデリングパターンを数多く含んでいます。

オブジェクト指向技術やモデル駆動設計を実践してきた技術者にとって、次に進むべき方向を示してくれる心強い本です。

一方、これからドメイン駆動設計を始めるという方の場合は、最初に知っておくべきポイントがあります。

それは、「利口なUI」と呼ばれるアプリケーション構造についてです。

「利口なUI」は、ドメイン駆動設計やオブジェクト指向では、アンチパターンです。

しかし、「利口なUI」は、実際の開発現場では多く使われています。

そのため、(特に、「オブジェクト思考」ではない技術者にとっては) 納得しにくいことかもしれません。

(ちなみに、オブジェクト指向言語を使っていても、オブジェクト思考になっているとは限りません。)


例として、Struts系のフレームワークを使ったWebアプリケーションを考えます。

もし、下図のような構造になっていれば、それは「利口なUI」パターンです。

また、よく似たパターンにトランザクションスクリプトと呼ばれるものがあります。

トランザクションスクリプトは、マーチン・ファウラー氏の著書「エンタープライズアプリケーションアーキテクチャパターン」に示されています。

DDD本では、利口なUI とトランザクションスクリプトは違うものとされていますが、どちらも同類のアンチパターンでしょう。

下図のようなアプリケーション構造は、利口なUI やトランザクションスクリプトの例です。

トランザクションスクリプトの例

上図の (A) や (B) では、JSPやアクションの中にSQL文が組込まれていて、そのSQL文で多くの業務ロジックを実現します。

そして、同じような、あるいは全く同じSQL文が、JSPやアクションの中に散在しています。

(C) では、Modelと称するクラスがありますが、実態は、DAOやDTOと呼ばれるような実装上の役割しか持っていません。

(A) (B) と同様にSQL文が重複したり散在します。

そして、ドメインの本質的な振る舞い (ビジネスロジック) は、ほぼ画面都合のSQL文で実現されているでしょう。


オブジェクト指向の場合は、オブジェクトはそれにふさわしい名前と責務を持ちます。

そして、それらが協調することでビジネスロジックを実現します。

ふさわしい名前と適切に割り当てられた責務は、そのオブジェクトの役割を見通しやすくして、再利用と変更を容易にします。

しかし、SQLで実現されたロジックの多くには、オブジェクトの存在や役割に相当する概念は無く、多くのデータエンティティに跨った大きな処理になっています。

そのため、目的が分かり難く、再利用も変更も難しいです。

初期はなんとかなっても、すぐにソフトウェア・エントロピー (複雑度) が増大し、手に負えなくなるでしょう。


ドメイン駆動設計を始めるためには、ドメイン駆動設計的にもオブジェクト指向的にも、これらがアンチパターンであるという認識が必要です。


しかし、利口なUI には悩ましい利点もあります。

利口なUI の利点

※引用:ここから (DDD本の第4章 p.75)

   単純なアプリケーションの場合、生産性が高く、すぐに作れる。

   それほど有能でない開発者でも、この方法ならほとんど訓練しないで仕事ができる。

※引用:ここまで (引用元には箇条書きで他に5点示されています)


なぜ悩ましいかというと、この利点を、多くのリーダーや開発者が疑問に思わないからです。

大量の開発者を一時的に集めて作業をするようなプロジェクトにおいては、都合の良い面もあるかもしれません。

ただし、プロジェクトの後半や仕様変更時、保守・改修といった局面で、これらが利点となっているケースは皆無に近いのではないでしょうか。

分析・設計やアーキテクチャのリファクタリング (洗練) 不足が表面化するのは、開発の初期局面ではなく後半です。

ちなみに、経験上「生産性が高く、すぐに作れる」というのは、開発の初期段階に限られます。

開発の後半で問題が生じてきても、原因は要員数や時間の問題とされ、手法やアーキテクチャの問題と認識されることは少ないように思われます。

最初の壁と利口なUIの罠」を参考ください。


ドメイン駆動設計を習得し実践していくためには、トランザクションスクリプトの課題を理解しておくことが最低限必要かと思います。

トランザクションスクリプトの欠点については、下にDDD本からの引用を示しましたが、これだけではすこし分かり難いかもしれません。

ただ、トランザクションスクリプトに起因する問題点を実際に経験されている方も多いのではないでしょうか。

特に開発工程の後半や、保守や改修作業において、エントロピーの増大に悩まされた方は少なくないでしょう。

利口なUI の欠点

※引用:ここから (DDD本の第4章 p.75)

   アプリケーションの統合は困難で、データベースを経由させるしかない。

   ふるまいが再利用されことも、ビジネスの問題が抽象化されることもない。ビジネスルールは、適用先の操作それぞれで複製されることになる。

※引用:ここまで (引用元には箇条書きで他に2点示されています)

[目次に戻る]

ドメイン駆動設計の主要なキーワード

アプリケーション

ドメインとは、システム化対象領域を指します。例えば、在庫管理業務などです。

ドメインモデルとは、ドメインに存在する概念などを抽象化 (オブジェクトモデリング) したものです。

多くの業務アプリケーションで採用されているレイヤ構成 (ビュー層/ドメイン層/永続化層) で言えば、ドメインモデルは、ドメイン層に存在するものです。

ただし、「ドメイン貧血症」と言われるような、アンチパターンな構成をイメージすると、出だしから勘違いしてしまうことになります。

そのようなアンチパターン構成のドメイン層にあるのは、ビュー層と永続化層の間でデータのバケツリレーをするだけのようなクラスであって、ドメインモデルとは異質な存在です。

ドメインモデルは、アプリケーションの本質的な責務を持った概念、オブジェクトの集まりです。

下図は、アプリケーションの構成です。

ドメイン駆動設計の実践では、ドメインモデル、実装コード、ユビキタス言語をアジャイルに、つまりイテレーティブに開発していきます。

アプリケーションの構成要素

[目次に戻る]

開発チームと実践的モデラ

開発チームは、ユーザ、ドメインエキスパート (業務の専門知識をもつ人) 、ソフトウェア技術者で構成されます。

チーム内では、(後述の) ユビキタス言語を使ってコミュニケーションを行います。

実践的モデラ (パターン) とは、主に体制についての指針です。

分析、モデリング、設計、プログラミングというように、過度に役割を分離しません。

実践的モデラパターンでは、チームメンバー全員が、モデラであり、プログラマです。


例えば、分析モデルという名前は、他の手法でもよく使われますが、これは、主に業務分析工程で作成される、ビジネス視点のモデルです。

設計モデルは、ソフトウェアの土台となる、ソフトウェア技術視点のモデルです。

実装モデルは、プログラム・コードです。

これらを、担当者や役割を (過度に) 分けずに、全員が一通り実践できるチームが必要です。


もしも現実問題として、実践的モデラが実現困難な場合でも、全員がモデルと実装コードを理解し、関心と責任を持つことが求められます。

モデリングとプログラミングを分離すると、モデルと実装の不一致など、ドメイン駆動設計はうまくいきません。

例えば、モデリング時に新たな発見があって、モデルを変更した場合には、その変更は、ユビキタス言語と実装コードに、正確に反映されなければなりません。

また、実装時に気づいた問題は、ドメインモデルとユビキタス言語に反映されなければなりません。

同様に、コミュニケーション中に気づく問題もあるでしょう。それも、ドメインモデルと実装コードに反映されなければなりません。

ユビキタス言語に変更 (用語の意味の変更など) があった場合には、会話の中に正確に反映されなければなりません。

つまり、ユビキタス言語、モデル、実装の間に一貫性を保つためには、全員が (少なくとも主要なメンバーは) 実践的モデラであることが求められます。

[目次に戻る]

ユビキタス言語

どんなチームでもコミュニケーションは重要です。

しかし、人や役割によって使用する名詞や動詞の意味が、微妙に違うと感じたことはないでしょうか。

ユビキタス言語とは、ドメインエキスパートの知識やドメインモデルをもとに、単語の微妙な違いを排除し、より正確なコミュニケーションを行うためのものです。 ドメインモデルや実装モデルの要素と正確に対応付けができることがポイントです。


※DDD本 p.26 から引用:ここから

「モデルを言語の骨格として使用すること。 チーム内のすべてのコミュニケーションとコードにおいて、その言語を厳格に用いることを、チームに約束させること。 図やドキュメント、そして何より会話の中では同一の言語を使用すること。 言語を使う上で問題があれば、代わりの表現を用いて実験することで、問題を取り除くこと。 そうした表現は代りとなるモデルを反映している。 そこで、新しいモデルに合わせてコードをリファクタリングし、クラス、メソッド、モジュールの名前を変更すること。 会話の中で用語が混同されていたら、普通の単語の意味について認識を合わせるのと同じやり方で解決すること。 ユビキタス言語における変更は、モデルに対する変更であると認識すること。」

※DDD本から引用:ここまで


ソフトウェア技術者はクラスやメソッドなどに関心があり、それらを実世界の概念と結び付けて思考し会話します。

一方、ユーザやドメインエキスパートには、クラスなどの知識はありません。

もし「クラス」という単語を使っていたとしても、それば、ソフトウェア技術者が使うクラスとは別の何かである可能性があります。

ドメインモデルを作成するときは、チーム内でのコミュニケーションがとても重要です。

ドメイン駆動設計では、このコミュニケーションを正確にするために、共通言語として「ユビキタス言語」を明確に位置づけます。

そして、必ずユビキタス言語を使ってコミュニケーションするようにします。


ユビキタス言語があれば、例えば「見積書」という名前は、会話の中でも、モデルの中でも、実装コードの中でも、そのまま「見積書」と現され、同じ概念を指します。

アプリケーションユーザも、ドメインエキスパートも、ソフトウェア技術者も、頭の中で読み替えたりしないで、そのまま使えます。

ただし日本では、「見積書」はローマ字表記になるかもしれません。

英語表記になる場合は、読み替えが必要になるので、その点は検討が必要かもしれません。


ユビキタス言語の語彙は、モデルや実装コードのクラス名、メソッド名などと双方向に追跡可能 (紐付けられる) にします。

[目次に戻る]

ドメインモデル

ドメインモデルとは、あるドメイン (アプリケーション領域、業務領域) に存在する概念を抽象化したものです。

ドメインモデルは、ユビキタス言語や、UMLなどのモデリング言語、プログラミング言語で定義されます。

ソフトウェアで表現されたドメインモデルを構成するオブジェクトのパターン (主な種類) には、エンティティ、値オブジェクト、サービスがあります。

イメージとしては、ドメイン:書店在庫業務, エンティティ:本, 値オブジェクト:ISBN, サービス:在庫移動 のような感じです。

それぞれについて以下に示します。

[目次に戻る]

エンティティ

ドメインモデル内のオブジェクトは主に、エンティティ、値オブジェクト、サービスの3つに分類されます。

エンティティは、連続性と識別性 (同一性と同値は区別される) を持ちます。

少し乱暴ですが、データベースの論理設計で抽出された論理テーブルの1つ1つは、(かなりの確率で) 妥当なエンティティと言えます。

実装の観点では、エンティティは、テーブル名や列名を、そのクラス名や属性名として持つクラスです。

ただし、テーブルにはありませんが、エンティティはドメインにおける重要で本質的な責務をメソッドなどとして持ちます。

これは、重要な違いです。


ドメイン駆動設計はオブジェクト指向モデリングを前提とはしません。

しかし、現実的な手法として、オブジェクト指向モデリングが採用されます。

そこで、オブジェクトを見つけるときのヒントとして、少々、古典になりますが、「オブジェクト指向システム分析」シュレイアー、メラー著 にクラスの候補として以下が挙げられています。

  • 有形物
    • 人、商品、伝票。
  • 役割
    • 人や構成により演じられる役割。医師、患者、顧客、従業員。医師は患者にもなる。
  • 出来事
    • 飛行、事故、故障、サービス要求。
  • 相互作用
    • 買い入れ、結婚。
  • 仕様
    • 製品などの仕様。

DOA (データ中心アプローチ) でも、上に挙げたような概念やモノは、テーブルの候補となるでしょう。

そして、それらの概念やモノは、ドメイン駆動設計のエンティティの有力な候補です。

各エンティティの責務や協調者についても、分析、設計、実装を行うことで、ドメイン駆動設計のドメインモデルとなっていきます。

[目次に戻る]

値オブジェクト

識別性を持たない、変更不可の不変 (immutable) オブジェクトで、その属性だけに関心があるようなオブジェクトです。

多くの場合、エンティティオブジェクトの状態を記述する属性として振る舞います。

例として、図形処理アプリケーションの座標点クラス (Point) があります。

多くの場合、エンティティの属性の状態を現すクラスは値オブジェクトとなります。

値オブジェクトには一意性は必要なく、同値性が重要です。


エンティティの属性の状態を示すためには、使用するプログラミング言語のプリミティブ型で十分なケースも多くあります。

例えば、Pointクラスではなくても、実数値を2つ持った2次元配列で十分と思われるケースもあります。

しかし、Pointクラスのような値オブジェクトをドメインモデルに追加することで、ドメインモデルをより洗練できることもあります。

ただし、過度に追加するとクラスの爆発と言われるような状況に陥ることもあります。

[目次に戻る]

エンティティと値オブジェクトの例

このサンプルは、正確な医療情報や知識に基づくものではありません。

集約の例

このモデルでは、患者オブジェクトは、血糖値とヘモグロビンA1cの値を基に血糖コントロールの状況をメッセージ形式で応答します。

血糖値は、空腹時血糖値、食後血糖値など1日の中でも変動する検査値です。

ヘモグロビンA1c (HbA1c) は、おおよそ2カ月間の平均血糖値を推定できる検査値です。

血糖コントロールとは、運動や食事、薬などで、血糖値を適正範囲に保つことです。


患者オブジェクトが「血糖コントロール状況を応答する」という責務を持たないモデルもあるでしょう。

例えば、次図のように診療記録オブジェクトが「血糖コントロール状況を応答する」責務をもつモデルも考えられます。

集約の例2

また、医師オブジェクトや診断サービスなどを追加すべきかもしれません。


ドメインモデルをすぐに決定できるケースは少ないかもしれません。

実際には、ドメインエキスパートの知識、ユースケース、DDD本のパターンや指針、アナリシスパターン、デザインパターンなどを参考に、ドメインモデルを繰り返し洗練していくことになります。

従って、ドメイン知識やモデリング技術は重要です。

そして、モデリングしたイメージを素早く実装し検証するスピードも重要です。

もしも、1回のイテレーション (モデリング → 実装 → 検証) が重くて時間がかかるようだと、ドメインモデルに対するチームの関心が薄れ、集中できなくなります。

[目次に戻る]

サービス

ドメインには、エンティティや値オブジェクトとして扱うには不自然なものが存在します。

GRASPパターンの純粋人工物 (Pure Fabrication)です。

ドメインには実在しないオブジェクトであり、サービスという形でユビキタス言語に組み込みます。

サービスは基本的に状態を持ちません。

エンティティや値オブジェクトではなく、アクションや操作といった概念として存在します。


例えば、在庫管理業務の中に、「倉庫間移動」という業務処理があります。

倉庫間移動とは、「ある倉庫の在庫商品を別の倉庫に移動する」ことです。

ここで、この倉庫間移動は、どのオブジェクトの責務でしょうか。

単独のオブジェクトの責務とする場合もあれば、複数のオブジェクトの相互作用とする場合もあるでしょう。

例えば、

(1) 倉庫クラスの静的メソッドとします。

     Warehouse#transferStock(倉庫1, 倉庫2, 商品, 数量)

(2) 倉庫クラスのインスタンスメソッドとします。

     warehouse.transferStock(別の倉庫, 商品, 数量)

などが考えられます。

しかし、ある責務を、あるエンティティオブジェクトや値オブジェクトの責務とすると、不自然な場合があります。

このようなときには、サービスオブジェクトを検討します。

そうして、次のように設計してみます。

(3) 倉庫サービスの倉庫間移動

     StockService#transferStock(倉庫1, 倉庫2, 商品, 数量)


ドメインモデルのクラス図サンプル


サービスオブジェクトを適切に追加することで、適切な責務割当てを実現できる場合があります。

サービスオブジェクトは、デザインパターンのファサード (Facade) としての役割もあります。

但し、色々な責務をサービスに詰め込んでしまうと、アンチパターンと同類になってしまうので、注意してください。

まずは、適切なエンティティや値オブジェクトを検討します。


例として、平面に図形描画する処理系を実現する場合を考えてみます。

円や矩形の描画処理は、どのように設計・実装すべきでしょうか。


ケース(1)

DrawingService.drawCircle(x, y, radius);

DrawingService.drawRectangle(x, y, width, height, angle);


ケース(2)

Circle circle = new Circle(x, y, radius);

circle.draw();

Rectangle rectangle = new Rectangle(x, y, width, height, angle);

rectangle.draw();


ケース(1)の場合、DrawingServiceはすぐに肥大化し、役割は不明瞭になるでしょう。

ケース(2)の方が適切でしょう。


なお、ここでは、在庫管理業務や描画処理系そのものについては議論していません。

業種や組織によっても最適なドメインモデルは違うでしょう。

[目次に戻る]

モジュール

Javaのパッケージと同義です。モジュール名もユビキタス言語に含まれる名前です。

モジュール名、モジュール構成もリファクタリングの対象です。

モジュール間は低結合、モジュール内は高凝集にします。

[目次に戻る]

集約

集約を見つけ出し適切にモデリングすることは深い問題です。 DDD本からの引用を挙げておきます。

※引用:ここから (DDD本 p.123)

関係を最小限に抑えるように設計することにより、 関連を辿る処理は単純化され、 関係性の爆発的増加もある程度は制限される。 しかし、ほとんどのビジネスドメインは非常に強く相互に結びついているので、 結局はオブジェクトの参照を通じて、長くて深い経路を辿ることになる。 ある意味で、こうしたもつれはこの世界の現実を反映している。 現実には、はっきりした境界が引いてもらえることはめったにないのだ。 これはソフトウェアの設計における問題である。

※引用:ここまで (DDD本 p.123)


次図は集約の例です。ドメインとしては架空の書店の書籍情報管理を想定しています。 実際のモデリング結果ではありません。 もし、出版社の業務であれば、集約ルートとしては、書籍よりもISBNの方が適切かもしれません。 また、同じ書店の中でも、業務が違えば、モデルも変わるかもしれません。 どんな業務でも完璧に振る舞うモデルを作るというのは、現実的ではありません。 ただし、変えたくなる原因が、業務用語の不統一や慣行の違い等であれば、まずその概念を整理・統合すべきかもしれません。

(DDD本の「境界づけられたコンテキスト」も参考になります)

実際の開発では、必要なユースケースを実行できるように、リファクタリングと検証を繰り返しながら、ドメインモデルを作り上げていきます。

価格やISBNは、JavaのBigDecimalやStringで十分な場合もありますが、ドメインモデルに価格クラスやISBNクラスを追加することで、より洗練された分かり易いモデルになることもあります。

集約の例3

 ・集約ルートエンティティはグローバルな同一性を持ちます。

 ・境界内部のエンティティは境界内でのみ一意となるローカルな同一性を持ちます。

 ・境界外にあるオブジェクトは、ルートエンティティへの参照を保持できます。

 ・境界外にあるオブジェクトは、境界内部への参照を保持することはできません。集約ルートを介して参照します。

[目次に戻る]

DDDとオブジェクト指向

ドメイン駆動設計では、実績のある既存の手法を活用します。

特に、モデル駆動開発、オブジェクト指向、アジャイル開発です。

ドメイン駆動設計において、オブジェクト指向モデリングやオブジェクト指向プログラミングは必須ではありません。

しかし、実績やモデル駆動開発との相性を考えると、事実上最適なパラダイムと言えます。

ドメインモデルのモデリングやプログラミングに、オブジェクト指向を活用します。

もちろん、ドメインモデル以外のビュー層やインフラストラクチャ層などにも適用します。

[目次に戻る]

DDDとモデル駆動開発

モデル駆動開発 (Model-Driven Development, MDD) は、 OMG (Object Management Group) が提唱するモデル駆動アーキテクチャ (Model-Driven Architecture, MDA) に則ったソフトウェア開発手法です。

MDDでは、分析・設計・実装・テストといった開発の成果をモデルとして作成し、 モデル変換を繰り返すことでアプリケーションを開発 (実装ソースコードを自動生成) する手法です。

ドメイン駆動設計では、分析モデルと設計モデルといった分け方はしません。両方の目的で使える唯一のモデルを作成します。

分析モデルと設計モデルを分けて作成すると、担当者や工程でモデルが分離されたり、実装と乖離してしまうことがあります。

ドメイン駆動設計では、このような分断をしないで、1つのモデルだけを作成して、そのモデルと一致した実装を行います。

モデルは、ドメインに存在する概念やドメインエキスパートが使う言葉などを、ソフトウェア設計の土台にできるように整理したものです。

[目次に戻る]

DDDとアジャイル開発

ドメイン駆動設計はイテレーティブな開発を基本としています。

ウォータフォール的な発想や計画では、ドメイン駆動設計は成功しないでしょう。

また、最初から多くのプラクティスやパターンを取り入れて、うまく実践できたという事例も稀でしょう。

パターンやプラクティスを段階的に取り入れながら、ドメインモデルを高度化し深化させていく、アジャイルな開発スタイルです。

同様に、ドメイン駆動設計スキルも段階的に向上させていければよいわけです。

※ 本記事では、アジャイル開発とイテレーティブ開発、反復型開発を区別しないで混用します。

[目次に戻る]

ドメイン駆動設計と他の手法との協調

ドメインモデリングで役に立つ概念やパターンを簡単に紹介します。

アナリシスパターン

マーチン・ファウラー氏の著書「アナリシスパターン」には、再利用可能なオブジェクトモデルがパターンとして示されています。 これらのオブジェクトモデルは、例題となった業務 (ドメイン) だけでなく、他のドメインにも適用できるものです。 自分たちのアプリケーションのドメインモデルとして、そのまま適用できなかったとしても、 ドメインモデルを作成するときの貴重な手本になるでしょう。 アナリシスパターンで示されているパターンは、よく洗練されたものであり、深いモデルです。 時として、深すぎて難しく感じるものも少なくないかもしれませんが、 深さのレベルを少し落として取り入れたり、ドメインモデリングのヒントにするだけでも、とても有益なパターン集です。

[目次に戻る]

デザインパターン

GoFと呼ばれる4人による共著「オブジェクト指向における再利用のためのデザインパターン」では、 オブジェクト指向設計、実装に関する23個のパターンを示しています。 各パターンは主に次の4つに分類されます。

(1) 生成に関するパターン

(2) 構造に関するパターン

(3) 振る舞いに関するパターン

(4) マルチスレッドプログラミングに関するパターン

実装の観点からの設計パターンです。ドメイン駆動設計のドメインモデルを作成する際や、リファクタリングする際の有用な指針となるでしょう。そのまま適用できるケースも少なくないと思われます。 GoFとは、エーリヒ・ガンマ、リチャード・ヘルム、ラルフ・ジョンソン、ジョン・ブリシディースの4人です。

アナリシスパターンやデザインパターンを知ると、パターンを使いたくなりますが、パターンを使うことを優先しないように注意してください。

[目次に戻る]

Naked Objects パターン

ここで、ドメイン駆動設計に通じるコンセプトを持つ Naked Objects パターンを紹介します。

Naked Objects パターンでは、ドメイン駆動設計と同様に、ドメインモデルを中心に置きます。

アプリケーションを開発する時には、ドメインオブジェクトだけを作成します。

ユーザインタフェース層やデータアクセス層は、ドメインオブジェクトから自動で作成します。

十分に洗練され深化したドメインモデルがあれば、 ドメインモデルの各インタフェースをビューとしてユーザに公開するだけで、アプリケーションとして機能する、という考えです。

ドメインモデルの各インタフェースとは、サービスやクラスのメソッド、プロパティのことです。

ビューは、画面のことです。


「Naked Objects for .NET - 生産性の高い.NETフレームワーク- InfoQ」から引用します。

※引用:ここから

ユーザインタフェースのレイヤやデータアクセスのレイヤを書く必要がないだけでなく、naked objectsパターンは良いオブジェクトモデリングも促します。

なぜならドメインモデルのプロトタイプを直ちにエンドユーザが評価できるアプリケーションにすることができるからです。

これらのことを聞いた時にほとんどの人は、大規模で複雑なビジネスアプリケーションではおそらく有効でないだろう、という反応を示します。

※引用:ここまで

元記事の中で、大きなアプリケーションでの事例も紹介されています。


ドメイン駆動設計で行うリファクタリングの本質的な対象は、ドメインモデルです。

ドメインモデルのサービスやクラスのメソッド、プロパティを更新 (リファクタリング) すると、ユーザインタフェースのレイヤやデータアクセスのレイヤの変更も必要です。

それは、単純ですが、軽量ではありません。

単純ミスも起きやすい部分です。

ドメインモデルに集中できなくなることもあります。

本質的ではない部分を自動化することで、「naked objectsパターンは良いオブジェクトモデリングも促します。」


なお、本記事で紹介しました Javaドメインモデル駆動開発とは は、 Naked Objects パターンにインスパイア (触発) されたものです。

[目次に戻る]

CRC (Class Responsibility Collaborator)

ドメイン駆動設計とは直接関係しませんが、オブジェクト指向分析ツールにCRCカードがあります。 CRCカードは、オブジェクトを発見するためのツールです。 また、オブジェクト指向の初学者のための演習用としても使用されます。 CRCでは、同じ大きさの小さなカード (主に紙のカード) を使用します。 1枚のカードにオブジェクトの候補を1つ記入し、クラス名、責務、協調者を記入します。 実際にオブジェクトの候補を挙げながら、その責務や、(責務を果たすために) 相互作用しなくてはならない協調者 (他のオブジェクト) を実際に書いてみることで、 オブジェクトの発見や、発見するためのアプローチを理解できます。 CRCでは、分析チームのメンバーや演習の参加者同士が、机上と口頭で相互作用してみて、候補に挙げたオブジェクトに正しい責務が割り当てられ、 期待する振る舞い (ユースケース) を実現できるか等を検証します。 ドメイン駆動設計においても、もし、ドメインオブジェクトの見つけ方で悩まれているならば、CRCカードを使ってみる価値があると思います。

[目次に戻る]

実践方法 - 開発プロセス

ドメイン駆動設計では反復型開発 (Iterative and Incremental Development) が基本です。

本記事で提案する「Javaドメインモデル駆動開発」も、基本は反復型開発です。

反復型開発とは

※引用: 反復型開発「ウィキペディア日本語版」更新日時: 2016年1月3日 11:56 (UTC) ここから

反復型開発(はんぷくがたかいはつ、Iterative and Incremental Development)とは、 より古典的なウォーターフォール・モデルの弱点を克服すべく開発されたソフトウェア開発工程の手法である。 反復型開発の中でもRADとDSDMは、よく知られたフレームワークである。 反復型開発は、エクストリーム・プログラミングや他のアジャイルソフトウェア開発フレームワークの基本的要素でもある。

※引用: 反復型開発「ウィキペディア日本語版」更新日時: 2016年1月3日 11:56 (UTC) ここまで


※引用: 反復型開発「ウィキペディア日本語版」更新日時: 2016年1月3日 11:56 (UTC) ここから

反復型開発の基本的考え方は、ソフトウェアシステムを徐々に開発していき、ソフトウェア開発者が過去の開発から学んだことを生かして、使用可能なシステムを段階的にリリースしていくというものである。 開発者は、開発そのものと実際のシステムの使用から学ぶ。重要な点は、要求仕様の単純なサブセットから開発を始め、徐々に改良を加えていき、最終的に完全なシステムを実装するということである。 反復ごとに設計が修正され、新たな機能が追加されていく。

※引用: 反復型開発「ウィキペディア日本語版」更新日時: 2016年1月3日 11:56 (UTC) ここまで


上の記述は、一般的な定義です。ドメイン駆動設計のキーワードを加えると次のようになります。

「反復型開発は、リファクタリングを繰り返し、段階的にモデルの完成度を上げていく手法です。

繰り返す単位をイテレーションと呼び、1回のイテレーションの中で、分析から設計、実装、検証までを完結させます。

ドメイン駆動設計では、ドメインモデルと実装とユビキタス言語を、常に整合させながら繰り返すことが重要です。」


[反復型開発の流れ]

反復型開発の流れ図

Javaドメインモデル駆動開発とは

ドメイン駆動設計に適した反復型の開発プロセスです。

特別な仕組みは一つだけで、Javaで実装されたドメインモデルから、Java Webアプリケーションを自動生成することです。

その結果、ドメインモデルの振る舞いを素早く検証できるようになります。

イテレーションが軽量化されて、反復サイクルも短縮されます。

ドメイン以外の余計な作業が減少し、開発者はドメインモデルのリファクタリングに集中できるようになります。

イテレーションの流れ

イテレーションの流れ図


「Javaドメインモデル駆動開発」では、ドメインモデルを Javaで実装し、コード中心でイテレーティブに開発します。

基本的に、モデル図よりも、実装を先行させます。(Java コードファースト)

ただし、先にモデル図を使って設計したい場面では、先にモデル図を作ります。

コードファーストに固執する必要はありません。

1つのドメインの中にも、コードファーストが適した部分、モデルファーストが適した部分、それぞれ存在すると思います。

自動生成されるもの

開発の流れ図

[入力]

① Java実装されたドメインクラス

①は、開発者自身が実装します。サービスクラスも含みます。

[出力]

動くソフトウェアモデル = Java Webアプリケーションが出力されます。

Java Webアプリケーションは、1つのフォルダの下に次のファイルを含んでいます。

② ビュー層のクラス群 (①から自動生成されます)

③ ドメイン層のクラス群 (① + DDBuilder基底クラス)

④ 永続化層のクラス群 (①から自動生成されます)

また、このフォルダは、

⑤ Eclipse用の設定ファイル

⑥ Wicket, Hibernateなどのライブラリ

を含んでいて、そのままEclipseにインポートできるようになっています。

ビュー層には独自のページを追加できます。次のイテレーションで自動生成を実行しても、独自ページは上書きされません。

DDBuilderとEclipseの関係

DDBuilderとEclipseの役割図

Javaドメインモデルの実装には、任意のエディタやIDEを使用できますが、 DDBuilderは、Web アプリケーションを Eclipse のプロジェクト形式で出力しますので、Eclipse の仕様を強く推奨します。


DDBuilder と Eclipse の相互連携の流れは以下の通りです。

[1]生成/読込み更新

   初回は新規生成です。

   2回目以降は更新動作です。

   読込み更新は、[3]で更新されたドメイン実装を読取り、Webアプリケーションに反映します。

   主に、WebアプリケーションのUI層が更新されます。

   この処理は、DDBuilder操作画面のボタンを押下すると実行されます。

[2]インポート/リフレッシュ

   インポートとリフレッシュは Eclipse で行う操作です。

   インポートは、初回に実行します。

   リフレッシュは[1]の更新結果を Eclipse に通知・反映するために行います。

   リフレッシュしないと、Eclipseは Web アプリケーションが更新されたことを検知できませ。

[3]ドメインモデルの実装

   モデル図などをもとに Eclipse で Java プログラミングします。

   これは、開発チームのタスクです。


詳しい流れは「DDBuilderを使った開発の流れ」を参照ください。

期待できる効果

  • 継続的なテスト
  • 早い段階からのテスト (シフト レフト)
  • イテレーションの工数削減と期間短縮
  • イテレーションの質の向上
  • コード中心・実装一致
  • 永続化周りの煩雑なコードの削減
  • 手組み実装によるミスとロスの排除

◎継続的なテスト

ドメインモデルをいつでも、素早く (アジャイルに) 、動かして検証できるということは、スムーズな開発をする上で重要です。

例えば、UMLで表現されたドメインモデルは、そのままでは動かしてみることはできません。

ソフトウェアで表現されたドメインモデル (例: Javaで実装されたドメインモデル) も、それだけでは動かせません。

実際に動かすためには単体テストコードやドライバのようなものが必要です。

つまり、ドメインモデルを実際に動かして検証するためには、ドメインモデル以外の実装も必要になります。

本記事では、動作可能な実装モデルを、「動くソフトウェアモデル」と呼ぶことにします。

動くソフトウェアモデル (検証モデル) は次のような観点で必要です。

  • 動くソフトウェアモデル は、各イテレーションの成果物のひとつです。
  • 実際に動かすことで、ドメインモデルの検証をより正確にします。
  • 具体的に動かすことで、開発チーム内でのドメインモデルに対する知識や理解のバラつきを抑止します。
  • 過剰な机上分析状態に陥ることを予防します。
  • 技術寄りではないメンバーも、Webアプリケーションとして操作することで深く理解し検証できます。

もしも、動くソフトウェアモデルを作らずに、机上のUML図や単体テストレベルのテストコードだけでドメインモデルを検証しようとすると、 検証に手間がかかるうえに、メンバー間の齟齬も発生しやすくなります。 しかし、具体的なアプリケーションがあれば、UML図などに不慣れなメンバーでも、検証作業が容易になり、精度も上がります。

◎イテレーションの質の向上

イテレーションは反復型開発のコアプロセスです。

動くソフトウェアモデルを使って検証できれば、イテレーションの品質が上がります。

また、イテレーションを軽快に繰り返すことができれば、チームの関心事をドメインモデルに集中させることができます。

ただ現実には、動くソフトウェアモデルを ”いつでも動かせる状態” に維持することは、簡単ではありません。

ドメインモデルを動かすために、想定以上の時間を要するケースが多いからです。

例えば、Webアプリケーション型で検証しようする場合、ドメインモデルをリファクタリングする度に、ユーザインタフェース層やインフラストラクチャ層などの変更が必要になるからです。

イテレーションの本来の目的はドメインモデルのリファクタリング (洗練) です。

しかし、イテレーションの工数が増えて、簡単には繰り返せなくなることがあります。

1回1回のイテレーションが重くては、反復型開発のメリットは得られません。

この問題を解決するためには、開発プロセスの一部を自動化するなど、何らかの対策が必要です。

成果物

開発チームは、DDBuilderが生成するJava Web アプリケーションに縛られる必要はありません。

本当の成果物は、アプリケーションではなく、「ソフトウェアで表現されたドメインモデル」です。

ここでは、その表現手段として Java 言語、検証手段として Web アプリケーションを使用したにすぎません。

Webアプリケーションはドメインモデルを評価するための入れ物にすぎません。

ドメインモデルは DDBuilder に依存しません。

いつでも DDBuilder を使わないで、開発を継続できます。 (自作の「Wicket + Java + JPA Webアプリケーション」として開発できます)

従って、DDBuilderは、Web アプリケーション自動生成ツールというよりも、ドメインモデルのインキュベータ (孵化器) と考えた方がより適切です。

もちろん、そのまま Webアプリケーションとして実運用することもできます。

自作のページも追加できます。再度自動生成しても追加したページは上書きされません。

求められる知識

  • オブジェクト指向モデリング技術 (考え方)
  • ドメイン駆動設計 (考え方)
  • イテレーティブ開発、インクリメンタル開発、アジャイル開発 (考え方)
  • モデル駆動設計 (考え方)
  • Javaプログラミング
  • Java EE JPA(Java Persistence API)入門レベル
  • Wicket 入門レベル
  • Eclipseの基本操作

関連する知識

「パターン」という言葉を含む有用な知識があります。

  • アナリシスパターンは、ドメインモデルの具体例、パターンです。他のドメインでも参考になるようなドメインモデルの構造、パターンを示します。
  • デザインパターンは、オブジェクト指向設計パターン、実装パターンです。
  • ドメイン駆動設計は、モデリングについてのパターンです。
[目次に戻る]

開発ツール DDBuilder

機能

下図は、DDBuilderが自動生成する Java Web アプリケーションの構成図です。

DDD本 (p.70) のレイヤ化アーキテクチャに準じています。

開発者は、ドメインモデリングを行い、ドメインクラスを Javaで実装します。

DDBuilderは、この Javaの実装コードを読み込み、抽象構文木による解析を行い、ビュー層や永続化層のクラスを生成します。

※実装の前に、クラス図などを作成する場合もあれば、すぐに Javaコードで実装する場合があると思います。


DDBuilderが生成するアプリケーションのレイヤ化アーキテクチャ図


DDBuilder 固有のコンテナなどはなく、フレームワークとして次を使用しています。

ビュー層:Apache Wicket

永続化層 (インフラストラクチャ層):Java EE JPA/ Hibernate

また、上図中の「DDBuilder基底」部分には、DDBuilder固有のいくつかの基底クラスが存在しています。


Java Web アプリケーションは、1つの Eclipse プロジェクトになっていて、Eclipse にインポートして、編集・実行できます。

下図は、Java Web アプリケーションとして生成されたフォルダの内容です。

Eclipseプロジェクト構成

.settings, .classpath, .project は Eclipse 用の設定情報です。

src フォルダ配下に、開発者が実装するドメインモデル (Javaクラス) と、DDBuilderが生成したJavaクラスが作成されます。

lib フォルダの配下には、必要な OSS のライブラリが含まれています。

自作ページの追加

自動生成されたWebアプリケーションに自作のページを追加できます。

DDBuilderは、自作ページを上書きしません。

自作ページの作成は、自動生成されたページクラスを参考にすれば、効率よく自作できるでしょう。

ページサンプル : Wicket DataTable ソート列 アクション列 テーブル

ページサンプル : 改ページ付き・ソート列付き・1件複数行対応 テーブル

ロックインフリー

利用者チームは、開発時でも実行時でも、いつでもDDBuilderを切り離すことができます。

DDBuilderにロックインされることはありません。

ドメインモデルがある程度安定し、頻繁な繰り返しが必要ない段階になれば、DDBuilderは必要なくなるかもしれません。

そして、ドメインモデルを他の言語やアプリケーションフレームワークに移植することもあるでしょう。

DDBuilderはドメインモデルのインキュベータ (孵化器・育成器) です。

もちろん、そのまま運用することもできます。

Java のメリット

リファクタリングの中で、クラス名を変えたり、メソッド名を変えることは頻繁にあります。

Javaのような強い静的型付けの言語であれば、Eclipseなどのリファクタリング機能を使って安全に変更できます。

しかし (筆者の勉強不足かもしれませんが) 動的型付け言語では期待する結果を得られないのではないでしょうか。

予期せぬ名前まで置換えられたりしないでしょうか。

また、他の言語や他のフレームワークへ移植する場合にも、元の言語がJavaであればより安全に移植できるでしょう。

Apache Wicket のメリット

Wicketは、Java Web アプリケーションフレームワークです。

Struts系と違い、 オブジェクト指向プログラミングモデルを前提としたフレームワークです。

HTMLとの独立性が高く、基本的にすべてをJavaコードで実装するアーキテクチャは、 自動生成機能を実現するうえでも好都合で相性がよいと言えます。

ドメインモデルは利用者が実装しますが、Wicketを意識することはありません。

Wicketを意識するのは自作ページを追加する場合です。

Java JPA Hibernate のメリット

HibernateはJavaEE JPA実装の1つです。

DDBuilderが生成する動くソフトウェアは、デフォルトではJava標準のJavaDBを使用します。

Hibernateは、 PostgreSQL, MySQL, Oracle, SQLServer, DB2など代表的なデータベースをサポートしており、設定変更だけで他のデータベースに変更できます。

「動くソフトウェア」は、Hibernateを「Javaアプリケーションのスタンドアロン型」で使用します。Springなどのコンテナは不要です。

ドメインモデル層のクラスを実装するときには、JPA / Hibernateの知識が必要になります。

主に、関連を定義するためのアノテーションの書き方などの知識が必要になります。

自動生成されるクラス

DDBuilder利用者が実装するドメインクラス (エンティティ) と、DDBuilderが自動生成するクラスについて説明します。

例として、DDBuilder利用者が、Product.javaというドメインクラス (黄色の網掛けのクラス) を追加します。

次に、DDBuilderで自動生成を実行すると、下図ようなクラスとHTMLファイルが自動生成されます。(下図は、すべてではなく、主なファイルです)

view, service, domain, persistenceはパッケージ名でス。それぞれ、ビュー層、サービス層、ドメイン層、永続化層を構成します。

DDBuilder利用者は、Product.javaだけを実装すれば、他のクラスは自動的に生成できます。

自動生成されたものは、そのまま Java Web アプリケーションとして動作します。

Productの登録、更新、削除、一覧がWeb画面で操作できます。

Product.javaは、@Entityのような、JPAアノテーションを含みますが、POJO (Plain Old Java Object) です。

1つのドメインモデルから自動生成されるファイル

ビュー層の1つの画面は、同名のHTMLファイルとJavaクラスで構成されます。例: ProductPage.htmlとProductPage.java。

これは、ビュー層で使用している Apache Wicket フレームワークの仕様です。

Wicketは、JSP/Struts系のアーキテクチャではなく、Jave EE JSP 系のアーキテクチャです。

HTMLファイルは、独立していて、通常のHTMLと同様に編集できます。

つまり、JavaやJSPなどの知識が不要なので、デザイン専門のデザイナーが編集できます。

Wicketでは、画面は、完全に Java オブジェクト指向プログラミングで実装できます。

例えば、Productを使用するサービスが必要になった場合は、上図のSomeService.javaのように追加できます。もちろん、サービス名は適切な名前を付けます。数も制約されません。

自動生成された画面では不足する場合は、画面を追加できます。(上図では、CustomePage)

自動生成を再度実行しても、SomeServiceやCustomePageは上書きされません。

具体的な操作イメージは、次項の「DDBuilderを使った開発の流れ」を参照ください。

[目次に戻る]

トランザクション管理

Java EE JPA の永続化機能では、永続化コンテキスト (EntityManager) を使用します。

永続化コンテキストには、次の2種類があります。

(1) コンテナ管理

永続化コンテキストのライフサイクル (生成、破棄) は、EJBコンテナによって管理されます。

(2) アプリケーション管理

永続化コンテキストのライフサイクルは、アプリケーションによって管理されます。

EJBコンテナは必要ありません。


DDBuilderが生成するアプリケーションでは、アプリケーション管理を使用します。

従って、特別なコンテナを必要としません。

永続化コンテキストのライフサイクルは、自動生成されたコードによって行われます。

DDBuilder利用者は、特に意識する必要はありません。


トランザクションを管理するためには、トランザクションマネージャを使用します。

トランザクションマネージャには、次の2種類があります。

(1) JTAトランザクション

EJBコンテナが必要です。トランザクション管理は、EJBコンテナによって自動的に行われます。

(2) リソースローカルトランザクション


EJBコンテナは必要ありません。

DDBuilderが生成するアプリケーションでは、リソースローカルトランザクションを使用します。

従って、特別なコンテナを必要としません。

トランザクション管理は、自動生成されたコードによって行われます。

DDBuilder利用者は、特に意識する必要はありません。


永続化コンテキストのライフサイクル、トランザクション管理の流れを下図に示します。

トランザクション管理

[目次に戻る]

クラス図の扱い

DDBuilderにはクラス図をサポートする機能はありませんが、ドメイン駆動設計を実践するうえで、クラス図やそれに相当するものは必要です。

本記事では、分析時のクラス図は手書きラフスケッチとし、正式なクラス図はUMLツールなどで Java ソースからリバースエンジニアリングされることを推奨します。 リバースエンジニアリングであれば常に実装と一致させることができます。

もしも、表計算ソフトなどで作成してしまうと、不一致の原因や、イテレーションが回らなくなる原因にもなるでしょう。

[目次に戻る]

ダウンロード

ダウンロードページへ...

■ DDBuilderは、ネクストデザイン有限会社が公開する無料・無保証のソフトウェアツールです。

■ サポートとソースコード公開について

お問合せ

初期トラブルについてはお問合せください (無料) ※当社の都合で、お返事に時間がかかる場合があります。

※ DDBuilderの初期公開時において、jar ファイルの不足でビルドできないという不具合がありました。お詫び申し上げます。修正しましたので再度お試しください。

ユーザガイド

ユーザガイド [PDF][2.5MB]

動作確認済み環境

  • Java 8
  • Windows7 pro 32bit/64bit, Windows10 pro 64bit
  • Eclipse IDE for Java EE Developers 4.4, 4.3
  • Apache Tomcat 7.0, 8.0, 8.5, 9.0

インストールとアンインストール (zip解凍/削除)

DDBuilderはスタンドアロンのJavaデスクトップアプリケーションです。

インストールは、ダウンロードしたzipファイルを適当な場所に解凍してください。

アンインストールは、 解凍結果をエクスプローラなどで削除してください。

※ ダウンロードしたファイルを解凍すると次の説明ファイルが含まれていますので、参考にしてください。

「はじめにお読みください(使い方).txt」

「DDBuilder_Guide_Ver2.pdf」

ダウンロードページへ

謝辞

DDBuilderは次のソフトウェアを利用しています。

サポートとソースコードの公開

使いはじめで、ご不明な点がありましたら「お問合せフォーム」からご連絡ください。 (無料)

弊社の都合で返信が遅くなる場合がありますが、ご了承ください。

また、有料でのサポートも別途用意しております。詳しくはお問合せください。

DDBuilderのソースコードは、準備が整い次第、公開する方向です。

例 ドメインクラスの関連タイプと属性型

このドメインモデルは、DDBuilderがサポートする関連の種類、インスタンス属性型の種類を確認するためのサンプルです。

そのため、属性の種類が関心事で、ドメイン駆動設計のエンティティらしいメソッドが無いことに注意してください。

[サンプルコードをダウンロード]


UMLクラス図

関連の例


サンプル コードには次の5つのクラスが含まれています。

(1) 著者 @Entity

public class Author extends DdBaseEntity

(2) 書籍 @Entity

public class Book extends DdBaseEntity

(3) 版 @Entity

public class Edition extends DdBaseEntity

(4) ISBN @Entity

public class Isbn extends DdBaseEntity

(5) 書店 @Entity

public class Store extends DdBaseEntity


サンプルコードを表示

/*
 * DDBuilderで使える関連タイプと属性型に関する例です。ドメイン駆動設計的な観点の例ではありません。
 */
package jp.co.nextdesign.domain;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.OneToMany;
import javax.persistence.Transient;
import jp.co.nextdesign.domain.ddb.DdBaseEntity;

/**
 * 著者
 */
@Entity
public class Author extends DdBaseEntity {
    private static final long serialVersionUID = 1L;

    /** 名前 */
    private String name;
    
    /** 書籍リスト owning/parent */
    @OneToMany(mappedBy="author", cascade=CascadeType.ALL, orphanRemoval=true)
    private List<Book> bookList;

    /** 書籍リスト owning/parent */
    @OneToMany(mappedBy="author2", cascade=CascadeType.ALL, orphanRemoval=true)
    private List<Book> bookList2;

    /** コンストラクタ */
    public Author(){
        super();
        this.name = "---";
        this.bookList = new ArrayList<Book>();
        this.bookList2 = new ArrayList<Book>();
    }
    
    //OneToManyで双方向関連を維持するためのコードを含むgetBookList(),
    //setBookList(List<Book> bookList)の例
    @Transient
    private ArrayList<Book> latestBookList = new ArrayList<Book>();
    public List<Book> getBookList() {
        return this.bookList;
    }
    public void setBookList(List<Book> bookList) {
        for(Book newBook : bookList){
            if (!latestBookList.contains(newBook)){
                newBook.setAuthor(this);
            }
        }
        for(Book oldBook : latestBookList){
            if (!bookList.contains(oldBook)){
                oldBook.setAuthor(null);
            }
        }
        this.bookList = bookList;
        latestBookList = new ArrayList<Book>(this.bookList);
    }

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public List<Book> getBookList2() {
        return bookList2;
    }
    public void setBookList2(List<Book> bookList2) {
        this.bookList2 = bookList2;
    }
    
    @Override
    public String getDDBEntityTitle(){
        return this.name;
    }
    
    /** debug */
    public String getDebugInfo(){
        String info = "<" + this.getClass().getSimpleName() + ">";
        info += "\nname=" + this.getName();
        info += "\n</" + this.getClass().getSimpleName() + ">";
        return info;
    }
}


package jp.co.nextdesign.domain;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.Transient;
import jp.co.nextdesign.domain.ddb.DdBaseEntity;
import jp.co.nextdesign.domain.store.Store;

/**
 * 書籍
 */
@Entity
public class Book extends DdBaseEntity {
    private static final long serialVersionUID = 1L;
    
    /** 書名 */
    private String name;

    /** 書名2 */
    private String name2;

    /** 出版日 */
    private Date publishedAt;
    
    /** 出版日2 */
    private Date publishedAt2;
    
    /** 仕入価格 */
    private BigDecimal cost;
    
    /** 仕入価格2 */
    private BigDecimal cost2;
    
    /** キャンペーン1 isなし */
    private Boolean campaign1;
    
    /** キャンペーン12 isなし */
    private Boolean campaign12;
    
    /** キャンペーン2 is付き */
    private Boolean isCampaign2;
    
    /** キャンペーン22 is付き */
    private Boolean isCampaign22;
    
    /** 言語 */
    @Enumerated(EnumType.STRING)
    private EnumLanguage attEnum;
    
    /** 言語2 */
    @Enumerated(EnumType.STRING)
    private EnumLanguage attEnum2;

    /** ISBN one-to-one owning/parent Hibernate ORM 5.2 User Guide2.7と異なる */
    @OneToOne(cascade=CascadeType.ALL)
    @JoinColumn(name="isbn_id") //自分の列
    private Isbn isbn;
    
    /** ISBN2 one-to-one owning/parent Hibernate ORM 5.2 User Guide2.7と異なる */
    @OneToOne(cascade=CascadeType.ALL)
    @JoinColumn(name="isbn_id2") //自分の列
    private Isbn isbn2;
    
    /** 著者 */
    @ManyToOne
    private Author author;

    /** 著者2 */
    @ManyToOne
    private Author author2;

    /** 版 */
    @OneToMany(mappedBy="book", cascade=CascadeType.ALL, orphanRemoval=true)
    private List<Edition> editionList;
    
    /** 版2 */
    @OneToMany(mappedBy="book2", cascade=CascadeType.ALL, orphanRemoval=true)
    private List<Edition> editionList2;
    
    /**
     * 書店
     * Book編集画面で関連付ける書店を選択/解除しても書店側には反映されない。
     * setStoreList, getStoreListを参照。
     * 同期反映させるにはaddStore/removeStoreを使うかまたは、ManyToManyを
     * 2つのOneToManyで定義する。
     * Hibernate ORM 5.2 User Guide2.7.2 Bidirectional ManyToMany参照
     */
    @ManyToMany(cascade={CascadeType.PERSIST, CascadeType.MERGE}, fetch=FetchType.EAGER)
    private List<Store> storeList;
    
    /** 書店2
     * BookとStoreの間に2つのManyToManyを定義すると、
     * 次のようなBook_Storeテーブルが作成される。
     * このテーブルにinsertするためには、4つのxxxx_idが全てnot nullに限られるので、
     * insert時に(常に)例外が発生する。
     * 同じエンティティ間で複数のMantToMany関連を定義したい場合は
     * @JoinTableを使用する必要があると思われる。
     * ここでは未確認。
     * create table Book_Store (
     *   bookList2_id bigint not null,
     *   storeList2_id bigint not null,
     *   bookList_id bigint not null,
     *   storeList_id bigint not null
     * )
     */
//    @ManyToMany(cascade={CascadeType.PERSIST, CascadeType.MERGE})
//    private List<Store> storeList2;
    
    /** Integer型属性名 */
    private Integer integerAttribute;
    
    /** Integer型属性名2 */
    private Integer integerAttribute2;
    
    /** Byte型属性名 */
    private Byte attByte;
    
    /** Byte型属性名2 */
    private Byte attByte2;
    
    /** Short型属性名 */
    private Short attShort;
    
    /** Short型属性名2 */
    private Short attShort2;
    
    /** Long型属性名 */
    private Long attLong;
    
    /** Long型属性名2 */
    private Long attLong2;
    
    /** Float型属性名 */
    private Float attFloat;
    
    /** Float型属性名2 */
    private Float attFloat2;
    
    /** Double型属性名 */
    private Double attDouble;
    
    /** Double型属性名2 */
    private Double attDouble2;
    
    /** Character型属性名 */
    private Character attCharacter;
    
    /** Character型属性名2 */
    private Character attCharacter2;
    
    /** コンストラクタ */
    public Book(){
        super();
        this.name = "";
        this.storeList = new ArrayList<Store>();
//        this.storeList2 = new ArrayList<Store>();
        this.editionList = new ArrayList<Edition>();
        this.editionList2 = new ArrayList<Edition>();
    }
    
    //ManyToManyで双方向関連を維持するためのaddStore,removeStoreを含む。
    //owning側ではなくmappedBy側から使用するが、両側に実装する。
    public List<Store> getStoreList() {
        return storeList;
    }
    public void setStoreList(List<Store> storeList) {
        this.storeList = storeList;
    }
    public void addStore(Store store){
        if (store != null && !this.storeList.contains(store)){
            this.storeList.add(store);
            store.addBook(this);
        }
    }
    public void removeStore(Store store){
        if (store != null && this.storeList.contains(store)){
            this.storeList.remove(store);
            store.removeBook(this);
        }
    }
    
    //OneToManyで双方向関連を維持するためのコードを含むgetEditionList(),
    //setEditionList(List<Edition> editionList)の例
    @Transient
    private ArrayList<Edition> latestEditionList = new ArrayList<Edition>();
    public List<Edition> getEditionList() {
        return this.editionList;
    }
    public void setEditionList(List<Edition> editionList) {
        for(Edition newEdition : editionList){
            if (!latestEditionList.contains(newEdition)){
                newEdition.setBook(this);
            }
        }
        for(Edition oldEdition : latestEditionList){
            if (!editionList.contains(oldEdition)){
                oldEdition.setBook(null);
            }
        }
        this.editionList = editionList;
        latestEditionList = new ArrayList<Edition>(this.editionList);
    }
    
    /** DDBのviewが使用する */
    @Override
    public String getDDBEntityTitle(){
        String result = this.getName();
        result += this.getAuthor() != null ? this.getAuthor().getName() : "";
        return result;
    }
    
//    public List<Store> getStoreList2() {
//        return storeList2;
//    }
//    public void setStoreList2(List<Store> storeList2) {
//        this.storeList2 = storeList2;
//    }
    public Isbn getIsbn() {
        return isbn;
    }
    public void setIsbn(Isbn isbn) {
        this.isbn = isbn;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Author getAuthor() {
        return author;
    }
    public void setAuthor(Author author) {
        this.author = author;
    }
    public Date getPublishedAt() {
        return publishedAt;
    }
    public void setPublishedAt(Date publishedAt) {
        this.publishedAt = publishedAt;
    }
    //Eclipseのgetter/setter自動生成では(booleanではなく
    //Booleanの場合)is名前形式のgetterは生成されない。
    //Wicket はis名前形式のgetter以外にget名前形式でもよい。
    public Boolean getCampaign1() {
        return campaign1;
    }
    public void setCampaign1(Boolean campaign1) {
        this.campaign1 = campaign1;
    }
    public Boolean getIsCampaign2() {
        return isCampaign2;
    }
    public void setIsCampaign2(Boolean isCampaign2) {
        this.isCampaign2 = isCampaign2;
    }
    public Byte getAttByte() {
        return attByte;
    }
    public void setAttByte(Byte attByte) {
        this.attByte = attByte;
    }
    public Short getAttShort() {
        return attShort;
    }
    public void setAttShort(Short attShort) {
        this.attShort = attShort;
    }
    public Integer getIntegerAttribute() {
        return integerAttribute;
    }
    public void setIntegerAttribute(Integer integerAttribute) {
        this.integerAttribute = integerAttribute;
    }
    public Long getAttLong() {
        return attLong;
    }
    public void setAttLong(Long attLong) {
        this.attLong = attLong;
    }
    public Float getAttFloat() {
        return attFloat;
    }
    public void setAttFloat(Float attFloat) {
        this.attFloat = attFloat;
    }
    public Double getAttDouble() {
        return attDouble;
    }
    public void setAttDouble(Double attDouble) {
        this.attDouble = attDouble;
    }
    public Character getAttCharacter() {
        return attCharacter;
    }
    public void setAttCharacter(Character attCharacter) {
        this.attCharacter = attCharacter;
    }
    public EnumLanguage getAttEnum() {
        return attEnum;
    }
    public void setAttEnum(EnumLanguage attEnum) {
        this.attEnum = attEnum;
    }

    /** debug用 */
    public void debugPrint(){
        String info = "<" + this.getClass().getSimpleName() + ">";
        info += "\nname=" + this.getName();
        info += "\npublishedAt=" + this.getPublishedAt();
        if(this.author != null) info += "\n" + this.getAuthor().getDebugInfo();
        if(this.isbn != null) info += "\n" + this.getIsbn().getDebugInfo();
        for(Edition edition : this.getEditionList()){
            info += "\n" + edition.getDebugInfo();
        }
        for(Store bookStore : this.getStoreList()){
            info += "\n" + bookStore.getDebugInfo();
        }
        info += "\n</" + this.getClass().getSimpleName() + ">";
        System.out.println("--------------------------------------");
        System.out.println(info);
        System.out.println("--------------------------------------");
    }

    public String getName2() {
        return name2;
    }
    public void setName2(String name2) {
        this.name2 = name2;
    }
    public Date getPublishedAt2() {
        return publishedAt2;
    }
    public void setPublishedAt2(Date publishedAt2) {
        this.publishedAt2 = publishedAt2;
    }
    public Boolean getCampaign12() {
        return campaign12;
    }
    public void setCampaign12(Boolean campaign12) {
        this.campaign12 = campaign12;
    }
    public Boolean getIsCampaign22() {
        return isCampaign22;
    }
    public void setIsCampaign22(Boolean isCampaign22) {
        this.isCampaign22 = isCampaign22;
    }
    public EnumLanguage getAttEnum2() {
        return attEnum2;
    }
    public void setAttEnum2(EnumLanguage attEnum2) {
        this.attEnum2 = attEnum2;
    }
    public Isbn getIsbn2() {
        return isbn2;
    }
    public void setIsbn2(Isbn isbn2) {
        this.isbn2 = isbn2;
    }
    public Author getAuthor2() {
        return author2;
    }
    public void setAuthor2(Author author2) {
        this.author2 = author2;
    }
    public List<Edition> getEditionList2() {
        return editionList2;
    }
    public void setEditionList2(List<Edition> editionList2) {
        this.editionList2 = editionList2;
    }
//    public List<Store> getStoreList2() {
//        return storeList2;
//    }
//    public void setStoreList2(List<Store> storeList2) {
//        this.storeList2 = storeList2;
//    }
    public Integer getIntegerAttribute2() {
        return integerAttribute2;
    }
    public void setIntegerAttribute2(Integer integerAttribute2) {
        this.integerAttribute2 = integerAttribute2;
    }
    public Byte getAttByte2() {
        return attByte2;
    }
    public void setAttByte2(Byte attByte2) {
        this.attByte2 = attByte2;
    }
    public Short getAttShort2() {
        return attShort2;
    }
    public void setAttShort2(Short attShort2) {
        this.attShort2 = attShort2;
    }
    public Long getAttLong2() {
        return attLong2;
    }
    public void setAttLong2(Long attLong2) {
        this.attLong2 = attLong2;
    }
    public Float getAttFloat2() {
        return attFloat2;
    }
    public void setAttFloat2(Float attFloat2) {
        this.attFloat2 = attFloat2;
    }
    public Double getAttDouble2() {
        return attDouble2;
    }
    public void setAttDouble2(Double attDouble2) {
        this.attDouble2 = attDouble2;
    }
    public Character getAttCharacter2() {
        return attCharacter2;
    }
    public void setAttCharacter2(Character attCharacter2) {
        this.attCharacter2 = attCharacter2;
    }
    public BigDecimal getCost() {
        return cost;
    }
    public void setCost(BigDecimal cost) {
        this.cost = cost;
    }
    public BigDecimal getCost2() {
        return cost2;
    }
    public void setCost2(BigDecimal cost2) {
        this.cost2 = cost2;
    }
    
//setStoreList,getStoreList,setEditionList,getEditionListに
//対策コードを追加したので以下は使用しない。
//    /*
//     * getter/setterに加えて、このメソッドを追加する理由
//     * OneToManyの関連に関連先を追加するためには、
//     * book.getEditionList().add(newEdition)として、persist(book)としても追加されない。
//     * newEdition.setBook(book)としてから、persist(book)しなければならない。
//     * ただ、シーケンスとしてbook側を変更するだけにしたい場合もあるので、
//     * 以下のようなaddEdition(newEdition)を実装した。
//     * ただし、双方向維持のための常套コードのように
//     * edition.setBook(book)からbook.addEdition(edition)とすると、
//     * 復元時に"復元中にコレクションが変更された"という例外が発生するので、
//     * edition.setBook(book)からbook.addEdition(edition)は使用しないようにした。
//     */
//    public void addEdition(Edition edition){
////        if(edition!=null && !this.editionList.contains(edition)){
////            edition.setBook(this);
////            this.editionList.add(edition);
////        }
//        //Hibernate ORM 5.2 User Guide2.7.2 Bidirectional @OneToMany 例を参考
//        this.editionList.add(edition);
//        edition.setBook(this);
//    }
//
//    /**
//     * Hibernate ORM 5.2 User Guide2.7.2 Bidirectionaln@OneToMany 例を参考
//     * @param edition
//     */
//    public void removeEdition(Edition edition){
//        this.editionList.remove(edition);
//        edition.setBook(null);
//    }
//
//    /**
//     * ManyToManyのowning sideなので自分のリストのみ更新する。
//     * Store(mappedBy側)との間でaddBookから折り返すと復元時に
//     * "復元中にコレクションが変更された"例外が発生すると思われる
//     */
//    public void addStore(Store store){
//        //Hibernate ORM 5.2 User Guide2.7.2 Bidirectionaln@ManyToManyでは
//        //Store(mappedBy側)にHelperメソッドは無い。
////        if (store != null && !this.storeList.contains(store)){
////            this.storeList.add(store);
////        }
//        this.storeList.add(store);
//        store.getBookList().add(this);
//    }
//    
//    /**
//     * 双方向関連を整合させるためのアプリケーションコード
//     */
//    public void removeStore(Store store){
//        this.storeList.remove(store);
//        store.getBookList().remove(this);
//    }
}


package jp.co.nextdesign.domain;
import javax.persistence.Entity;
import javax.persistence.ManyToOne;
import jp.co.nextdesign.domain.ddb.DdBaseEntity;

/**
 * 版
 */
@Entity
public class Edition extends DdBaseEntity {
    private static final long serialVersionUID = 1L;

    /** 版番号 */
    private Integer editionNumber;
    
    /** 版名 */
    private String name;
    
    /** 書籍 */
    @ManyToOne
    //@JoinColumn(name="book_id") //省略可
    private Book book;

    /** 書籍 */
    @ManyToOne
    //@JoinColumn(name="book_id") //省略可
    private Book book2;

    public Edition(){
        super();
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getEditionNumber() {
        return editionNumber;
    }

    public void setEditionNumber(Integer editionNumber) {
        this.editionNumber = editionNumber;
    }

    public Book getBook() {
        return book;
    }

    //Book#addEdition,removeEditionから使用する
    public void setBook(Book book) {
        this.book = book;
    }    

    public Book getBook2() {
        return book2;
    }

    public void setBook2(Book book2) {
        this.book2 = book2;
    }

    @Override
    public String getDDBEntityTitle(){
        return this.name;
    }

    /** debug */
    public String getDebugInfo(){
        String info = "<" + this.getClass().getSimpleName() + ">";
        info += "\neditionNumber=" + this.getEditionNumber();
        info += "\n</" + this.getClass().getSimpleName() + ">";
        return info;
    }
}


package jp.co.nextdesign.domain;
public enum EnumLanguage {
    JA("日本語"),
    EN("英語");

    private String fullName;
    private EnumLanguage(String fullName){
        this.fullName = fullName;
    }

    @Override
    public String toString(){
        return this.fullName;
    }
}


package jp.co.nextdesign.domain;
import java.util.Date;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import jp.co.nextdesign.domain.ddb.DdBaseEntity;

/**
 * ISBN
 */
@Entity
public class Isbn extends DdBaseEntity {
    private static final long serialVersionUID = 1L;
    
    /** 書籍 one-to-one non-owning/child Hibernate ORM 5.2 User Guide2.7と異なる */
    @OneToOne(mappedBy="isbn") //相手側(owning)の属性
    private Book book;

    /** 書籍 one-to-one non-owning/child Hibernate ORM 5.2 User Guide2.7と異なる */
    @OneToOne(mappedBy="isbn2") //相手側(owning)の属性
    private Book book2;

    /** グループ記号 */
    private String groupCode;
    
    /** 出版社記号 */
    private Integer publisherCode;
    
    /** 書名記号 */
    private String itemCode;
    
    /** チェックディジット */
    private String checkDigit;
    
    /** 決定日 */
    private Date determinatedAt;
    
    /** 旧ISBN */
    private Boolean isOldIsbn;
    
    /** コンストラクタ */
    public Isbn(){
        super();
    }

    public Book getBook() {
        return book;
    }

    public void setBook(Book book) {
        this.book = book;
    }
    
    public Book getBook2() {
        return book2;
    }

    public void setBook2(Book book2) {
        this.book2 = book2;
    }

    public String getGroupCode() {
        return groupCode;
    }

    public void setGroupCode(String groupCode) {
        this.groupCode = groupCode;
    }

    public Integer getPublisherCode() {
        return publisherCode;
    }

    public void setPublisherCode(Integer publisherCode) {
        this.publisherCode = publisherCode;
    }

    public String getItemCode() {
        return itemCode;
    }

    public void setItemCode(String itemCode) {
        this.itemCode = itemCode;
    }

    public String getCheckDigit() {
        return checkDigit;
    }

    public void setCheckDigit(String checkDigit) {
        this.checkDigit = checkDigit;
    }
    
    public Date getDeterminatedAt() {
        return determinatedAt;
    }

    public void setDeterminatedAt(Date determinatedAt) {
        this.determinatedAt = determinatedAt;
    }

    public Boolean getIsOldIsbn() {
        return isOldIsbn;
    }

    public void setIsOldIsbn(Boolean isOldIsbn) {
        this.isOldIsbn = isOldIsbn;
    }

    @Override
    public String getDDBEntityTitle(){
        return "ISBN" + this.getGroupCode() + "-" 
        + this.getPublisherCode() + "-" + this.getItemCode();
    }
    
    /** debug */
    public String getDebugInfo(){
        String info = "<" + this.getClass().getSimpleName() + ">";
        info += "\ngroupCode=" + this.getGroupCode();
        info += "\npublisherCode=" + this.getPublisherCode();
        info += "\nitemNumber=" + this.getItemCode();
        info += "\n</" + this.getClass().getSimpleName() + ">";
        return info;
    }
}


package jp.co.nextdesign.domain.store;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.ManyToMany;
import jp.co.nextdesign.domain.Book;
import jp.co.nextdesign.domain.ddb.DdBaseEntity;

/**
 * 書店
 */
@Entity
public class Store extends DdBaseEntity {
    private static final long serialVersionUID = 1L;
    
    /** 書店名 */
    private String name;
    
    /** 書籍リスト */
    @ManyToMany(mappedBy="storeList", fetch=FetchType.EAGER)
    private List<Book> bookList;

//    /** 書籍リスト Book側のコメントを参照 */
//    @ManyToMany(mappedBy="storeList2")
//    private List<Book> bookList2;

    /** コンストラクタ */
    public Store(){
        super();
        this.name = "";
        this.bookList = new ArrayList<Book>();
//        this.bookList2 = new ArrayList<Book>();
    }

    /** DDBのviewが使用する */
    @Override
    public String getDDBEntityTitle(){
        return this.name;
    }
    
    //ManyToManyで双方向関連を維持するためのaddStore,removeStoreを含む。
    //owning側ではなくmappedBy側から使用するが、両側に実装する。
    public List<Book> getBookList() {
        return bookList;
    }
    public void setBookList(List<Book> bookList) {
        this.bookList = bookList;
    }
    public void addBook(Book book){
        if (book != null && !this.bookList.contains(book)){
            this.bookList.add(book);
            book.addStore(this);
        }
    }
    public void removeBook(Book book){
        if (book != null && this.bookList.contains(book)){
            this.bookList.remove(book);
            book.removeStore(this);
        }
    }secti

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }

    /** debug */
    public String getDebugInfo(){
        String info = "<" + this.getClass().getSimpleName() + ">";
        info += "\nname=" + this.getName();
        info += "\n</" + this.getClass().getSimpleName() + ">";
        return info;
    }
}


ドメインモデルのサンプルとして

この例でサンプルとして使用したドメインモデルは、DDBuilderがサポートする属性型と関連の型を示すためのものです。

そのため、ドメインモデルとしての重要な責務 (メソッド) が欠落しています。

このサンプルモデルはシンプルですが、現実には、集約 (AGGREGATES) についても注意深くモデリングする必要があります。

以下にDDD本から引用します。

※引用:ここから (DDD本 p.123)

関係を最小限に抑えるように設計することにより、 関連を辿る処理は単純化され、 関係性の爆発的増加もある程度は制限される。 しかし、ほとんどのビジネスドメインは非常に強く相互に結びついているので、 結局はオブジェクトの参照を通じて、長くて深い経路を辿ることになる。 ある意味で、こうしたもつれはこの世界の現実を反映している。 現実には、はっきりした境界が引いてもらえることはめったにないのだ。 これはソフトウェアの設計における問題である。

※引用:ここまで (DDD本 p.123)

[DDBuilder ダウンロードページへ]

[目次に戻る]

例 DDBuilderを使った開発の流れ

DDBuilderを使って、ドメイン駆動設計を実践する流れを示します。

この例のドメインは、簡単な在庫管理業務とします。

倉庫、製品、在庫、在庫管理サービスといったドメインモデルを作成する流れを示します。

この節は、次の10ステップで順に説明します。

  • [1/10] DDBuilder の起動
  • [2/10] 基本情報の設定
  • [3/10] 初回の生成
  • [4/10] 初回のWebアプリケーションの実行確認
  • [5/10] ドメインモデルを追加
  • [6/10] Webアプリケーションに反映
  • [7/10] Eclipse プロジェクトをリフレッシュ
  • [8/10] 再度 Webアプリケーションを動かしてテスト
  • [9/10] ドメインモデルをさらに追加
  • [10/10] ドメインモデルの実装コード例(例:StockService, Warehouse, Stock)

[1/10] DDBuilder の起動

(1) 事前準備として、DDBuilder 圧縮ファイル (jp-co-nextdesign-ddbuilder.zip) をダウンロードします。 [ダウンロードページへ]

(2) DDBuilder 圧縮ファイルを適当な場所に解凍します。

(3) 解凍後フォルダ (jp-co-nextdesign-ddbuilder) の直下の startDdbuilder.bat を起動します。

(4) 起動すると下図の画面が表示されます。

※ なお、DDBuilder は Java で作成されたスタンドアロンアプリケーションです。Eclipseのプラグインではありません。Eclipseとは関係なく独立して動作します。

DDBuilderを最初に起動したときの画面

[2/10] 基本情報の設定

起動したら、Webアプリケーションの作成場所やアプリケーション名などの基本情報を入力します。

ここで設定した基本情報は、生成される Java Web アプリケーションの画面ラベル等にも反映されます。

より詳細な設定は、「他の設定」ボタンを押してから設定してください。

下図は入力例です。

生成するWebアプリケーションの基本情報の入力例

[3/10] 初回の生成

基本情報を入力し、作成/更新ボタンを押下するとWebアプリケーションの基本形が作成されます。

この時点ではドメインモデルはひとつも定義されていませんので、生成されたWebアプリケーションは最小限の画面 (トップ画面など) を含むだけです。

次に、生成されたWebアプリケーションを、Eclipseにインポートします。

インポートしたら、Eclipseから、ドメインモデルを Java で実装したり、Web アプリケーションとしてデバッグ実行して、ドメインモデルを検証します。

※Webアプリケーションは、インポート可能なEclipseプロジェクトとして生成されています。

Eclipse画面例


※ DDBuilder は起動したままでも構いません。一度終了してから、必要なときに再起動しても構いません。

※ 再起動した場合も、前回入力されたDDBuilder画面の基本情報は維持されています。

[4/10] 初回のWebアプリケーションの実行確認

インポートが完了すると、上図のように「プロジェクトエクスプローラ」ビューに表示されます。

確認のために、Eclipseで Web アプリケーションを実行してみます。

この時点では、ドメインモデルは1つもありませんが、基本的な画面遷移の動作確認はできます。

下図はEclipse内で実行した時の例です。

デバッグ実行画面


上図はEclipse内蔵のブラウザで表示された状態です。

Eclipse外のブラウザからもアクセスしてテストできます。

下図はChromeからアクセスした例です。例:http://localhost:8080/zaiko/

Chromeブラウザでアクセスした例


ここまでで、Webアプリケーションの基本動作ができました。(ドメインモデルが無い空のアプリケーション)

この後は、ドメイン駆動設計による分析、ドメインモデルの抽出、設計、実装、検証する作業の繰り返しが始まります。

つまり、初回のイテレーションの開始です。

一気にドメイン全体をカバーしようとするのではなく、1週間程度で行える範囲や優先度の高いユースケースが通る範囲などをイテレーションのゴールとして設定します。(1週間が妥当かどうかは要検討です)

期間の目安や、ユースケースの優先度などはプロジェクトによって違ってくるでしょう。

反復型開発プロセスやアジャイルなどの手法解説を参考にしてください。

[5/10] ドメインモデルを追加

まず、もっとも簡単な例として、下図のProductオブジェクトを1つだけ作成してみます。

従って、イテレーションのゴールは、以下の製品Productクラスの初版を実装し、Webアプリケーションの形で振る舞いを検証することです。


サンプル クラス図

単純なドメインモデルのサンプル


サンプル コード


    /**
     * 製品
     */
    @Entity <------------- JPAアノテーション
    public class Product extends DdBaseEntity {   <------------- DDBuilderエンティティ基底クラス
        private static final long serialVersionUID = 1L;

        /**
         * 製品名
         */
        private String name;

        /**
         * コンストラクタ
         */
        public Product(){
            super();
            this.name = "";
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }

実装作業は、下図のように、EclipseでProductクラスを実装します。

domainパッケージの直下にProduct.javaを追加します。サブパッケージを追加することもできます。

EclipseでProduct.javaを作成するときの例

[6/10] Webアプリケーションに反映

これで、Productクラスは追加できました。

しかしまだ、Productクラスのインスタンスを登録したり、表示したりするためのビュー層 (ユーザインタフェース層) や永続化層 (インフラストラクチャ層) はありません。

これらは、DDBuilderが自動生成します。

そのために、DDBuilderでWebアプリケーションを更新します。

DDBuilderはProduct.javaファイルを読み込み、ビュー層のクラスなどを追加します。

DDBuilder 操作画面

[7/10] Eclipse プロジェクトをリフレッシュ

作成/更新ボタンを押下し、完了ダイアログが表示されたら、Webアプリケーションの中に必要なjavaファイルが追加されています。

しかし、Eclipseは、追加されたファイルを検知していません。

そのために必ず、Eclipse側でプロジェクトをリフレッシュしてください。(忘れがちです)

[8/10] 再度 Webアプリケーションを動かしてテスト

リフレッシュしたら、再度Webアプリケーションを起動してテストします。

下図はChromeからアクセスした例です。

[ アプリケーショントップ画面 ]

ブラウザからアクセスしたトップ画面の例


ここで、ドメインクラス一覧ボタンをクリックすると次画面に遷移します。

[ ドメインクラス一覧画面 ]

ドメインクラス一覧画面の例


次に、製品Productリンクをクリックすると次画面に遷移します。

[ ドメインクラス別のインスタンス一覧・新規作成画面 ]

インスタンス一覧画面の例


次に、新規作成リンクをクリックすると次画面に遷移します。

[ ドメインクラス別の新規登録画面 ]

インスタンスの編集画面の例


次に、製品名を入力し保存ボタンを押下すると、Productインスタンスが永続化され、次画面に遷移します。

エンティティインスタンスの一覧画面の例


1件登録され、一覧に追加されています。

次に、編集リンクをクリックすると次画面に遷移します。

エンティティの編集画面の例


次に、製品名を変更すると次画面に遷移します。

ブラウザからアクセスしたエンティティ編集画面の例


次に、保存すると次画面に遷移します。

ブラウザからアクセスしたエンティティ一覧画面例

[9/10] ドメインモデルをさらに追加

ここまではProductクラスだけでしたが、下図のようにドメインモデルに1つのサービスと2つのエンティティを追加します。

値オブジェクト (バリューオブジェクト) はこの例では登場しません。


ドメインモデルのクラス図サンプル


Productを追加した時と同様の手順で、Webアプリケーションを更新し、テスト実行します。

[ アプリケーショントップ画面 ]

動くソフトウェアのトップ画面


ドメインクラス一覧ボタン → 製品Productリンクをクリックします。

なお、ここでは説明を省略しますが、以降の説明の中では、下図のようなテストデータが作成済みとします。

テストデータの作成については ユーザーガイド(PDF)を参照してください。

ドメインクラス一覧ボタンを押下します。

[ ドメインクラス一覧画面 ]

ドメインクラスの一覧画面


製品 Productリンクをクリックします。

[ ドメインクラス別インスタンス一覧画面 ]

インスタンスの一覧画面


例として、2行目の編集リンクをクリックします。

[ ドメインクラス別編集画面 ] (製品(1)---(*)在庫)

インスタンスの編集画面


編集して保存するか、キャンセルで戻ります。

次に、サービスメソッドを実行してみます。

このサービスは、ある倉庫から別の倉庫に在庫を移動させる業務を実現します。

サービス一覧またはホーム → サービス一覧で下図の画面が表示されます。

[ サービスメソッド一覧画面 ]

サービスメソッドの一覧画面


実行リンクをクリックします。

下図の画面が表示されたら、このサービスメソッドに渡す引数を指定します。

from移動元倉庫:選択ボタンを押下します。

[ サービスメソッド実行画面 (引数設定画面) ]

サービスメソッドの実行画面


例として「福岡センター」を選択します。

サービスメソッドの引数設定画面(1)


仮決定ボタンを押下します。

サービスメソッドの引数設定画面(2)


同様に移動先の倉庫と移動する製品と数量を指定します。

サービスメソッドの引数設定画面(3)


サービス実行を押下すると、下図の完了画面が表示されます。

倉庫間移動メソッドの詳細は実装していませんので、戻り値としてtruetrueが表示されています。

[ サービスメソッド実行結果画面 ]

サービスメソッドの実行結果画面(4)

[10/10] ドメインモデルの実装コード例(例:StockService, Warehouse, Stock)

サンプル コードには次の4つのクラスが含まれています。

(1) 製品 @Entity

public class Product extends DdBaseEntity

(2) 在庫 @Entity

public class Stock extends DdBaseEntity

(3) 倉庫 @Entity

public class Warehouse extends DdBaseEntity

(4) 在庫管理サービス

public class StockService extends DdBaseService


サンプルコードを表示

package jp.co.nextdesign.domain;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.OneToMany;
import javax.persistence.Transient;
import jp.co.nextdesign.domain.ddb.DdBaseEntity;
/**
 * 製品
 */
@Entity
public class Product extends DdBaseEntity {
    private static final long serialVersionUID = 1L;

    /** 製品名 */
    private String name;
    
    /** 在庫 */
    @OneToMany(mappedBy="product", cascade=CascadeType.ALL, orphanRemoval=true)
    private List<Stock> stockList = new ArrayList<Stock>();
    
    /** コンストラクタ */
    public Product(){
        super();
        this.name = "";
    }

    //OneToManyで双方向関連を維持するためのコードを
//含むgetStockList(),setStockList(List<Stock> stockList)の例
    @Transient
    private ArrayList<Stock> latestStockList = new ArrayList<Stock>();
    public List<Stock> getStockList() {
        return this.stockList;
    }
    public void setStockList(List<Stock> stockList) {
        for(Stock newStock : stockList){
            if (!latestStockList.contains(newStock)){
newStock.setProduct(this);
            }
        }
        for(Stock oldStock : latestStockList){
            if (!stockList.contains(oldStock)){
                oldStock.setProduct(null);
            }
        }
        this.stockList = stockList;
        latestStockList = new ArrayList<Stock>(this.stockList);
    }

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    
    /** DDB一覧表示用タイトル */
    @Override
    public String getDDBEntityTitle(){
        return "製品名:" + this.getName();
    }
}


package jp.co.nextdesign.domain;
import java.util.Calendar;
import java.util.Date;
import javax.persistence.Entity;
import javax.persistence.ManyToOne;
import jp.co.nextdesign.domain.ddb.DdBaseEntity;
/**
 * 在庫
 */
@Entity
public class Stock extends DdBaseEntity {
    private static final long serialVersionUID = 1L;
        
    /** 数量 */
    private Integer quantity;
        
    /** 製品 */
    @ManyToOne
    private Product product;
        
    /** 倉庫  */
    @ManyToOne
    private Warehouse warehouse;
        
    /** コンストラクタ */
    public Stock(){
        super();
        this.quantity = 0;
    }
        
    /**
     * 次月の入庫予定日を応答する
     * @return 次月の入庫予定日
     */
    public Date getNextMonthWarehousingDate(){
        Date result = Calendar.getInstance().getTime();
        //何らかの実装
        return result;
    }
        
    /** DDB一覧表示用タイトル */
    @Override
    public String getDDBEntityTitle(){
        String result = "製品名=";
        result += this.getProduct() != null ? this.getProduct().getName() : "";
        result += " 倉庫名=";
        result += this.getWarehouse() != null ? this.getWarehouse().getName() : "";
        result += " 数量=" + this.getQuantity();
        return result;
    }
    public Integer getQuantity() {
        return quantity;
    }
    public void setQuantity(Integer quantity) {
        this.quantity = quantity;
    }
    public Product getProduct() {
        return product;
    }
    public void setProduct(Product product) {
        this.product = product;
    }
    public Warehouse getWarehouse() {
        return warehouse;
    }
    public void setWarehouse(Warehouse warehouse) {
        this.warehouse = warehouse;
    }       
}


package jp.co.nextdesign.domain;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.OneToMany;
import javax.persistence.Transient;
import jp.co.nextdesign.domain.ddb.DdBaseEntity;
/**
 * 倉庫
 */
@Entity
public class Warehouse extends DdBaseEntity {
    private static final long serialVersionUID = 1L;

    /** 倉庫名 */
    private String name;
    
    /** 在庫リスト */
    @OneToMany(mappedBy="warehouse", cascade=CascadeType.ALL, orphanRemoval=true)
    private List<Stock> stockList = new ArrayList<Stock>();

    /** コンストラクタ */
    public Warehouse(){
        super();
        this.name = "";
    }
    
    /** 製品在庫を追加する */
    public boolean addStock(Product product, int quantity){
        //処理(この例では省略)
        return true;
    }

    /** 製品在庫を削減する */
    public boolean removeStock(Product product, int quantity){
        //処理(この例では省略)
        return true;
    }

    //OneToManyで双方向関連を維持するためのコードを
//含むgetStockList(),setStockList(List<Stock> stockList)の例
    @Transient
    private ArrayList<Stock> latestStockList = new ArrayList<Stock>();
    public List<Stock> getStockList() {
        return this.stockList;
    }
    public void setStockList(List<Stock> stockList) {
        for(Stock newStock : stockList){
            if (!latestStockList.contains(newStock)){
                newStock.setWarehouse(this);
            }
        }
        for(Stock oldStock : latestStockList){
            if (!stockList.contains(oldStock)){
                oldStock.setWarehouse(null);
            }
        }
        this.stockList = stockList;
        latestStockList = new ArrayList<Stock>(this.stockList);
    }

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    
    /** DDB一覧表示用タイトル */
    @Override
    public String getDDBEntityTitle(){
        return "倉庫名:" + this.getName();
    }
}


package jp.co.nextdesign.service;
import jp.co.nextdesign.domain.Product;
import jp.co.nextdesign.domain.Warehouse;
import jp.co.nextdesign.service.ddb.DdBaseService;
/**
 * 在庫管理サービス
 */
public class StockService extends DdBaseService {

    /**
     * 倉庫間移動
     * @param from 移動元倉庫
     * @param to 移動先倉庫
     * @param product 移動する商品
     * @param quantity 移動する数量
     * @return
     */
    public Boolean transferStock(Warehouse from, Warehouse to, Product product, Integer quantity) {
        boolean result = false;
        try {
            startService(); //サービス初期化処理
            begin(); //トランザクション開始
            
            // 倉庫間移動処理
            if (from.removeStock(product, quantity)){
                if (to.addStock(product, quantity)){
                    result = true;
                }
            }
            if (result){
                commit(); //トランザクションcommit
            } else {
                rollback(); //トランザクションrollback
            }
        } catch (Exception e) {
            rollback();
        } finally {
            endService(); //サービス終了処理
        }
        return result;
    }
}


詳細は ユーザガイド(PDF)を参照ください。

[ダウンロードページへ]

[目次に戻る]

ドメイン駆動設計の入門に活用

DDBuilderは、ドメイン駆動設計の入門用や学習用として役立ちます。

例えば、入門者の場合は、ドメインモデルをイメージできるまでに、時間がかかることがあります。

DDBuilderを活用すれば、簡単なドメインオブジェクトを実装して、それを動かしてみることができるので、イメージしやすくなるでしょう。

簡単なドメインオブジェクトとは、例えば、病院システムの患者クラスや、在庫管理システムの倉庫クラス等です。

1つのクラスで構いません。

ドメインモデルを動かしてみるまでのハードルは、高くありません。

オブジェクト指向、モデル駆動設計、アジャイル開発、ドメイン駆動設計などの入門者にとって、机上だけで、ドメイン駆動設計やドメインモデルを理解することは、簡単ではありません。

しかし、実際に実装して、具体的に動かしてみれば、理解しやすくなります。

DDBuilderが生成した Java Web アプリケーションを見れば、「利口なUI」や「トランザクションスクリプト」などのアンチパターンとの違いも分かりやすいと思います。

ドメインがアプリケーションの本質であるという考え方や、ドメインモデルを中心にした開発方法、イテレーティブな開発についても理解いやすくなるでしょう。

抽象的な議論ばかりしていると、時間ばかりが経ってしまいます。

具体的にイテレーションを繰り返しながら、段階的にドメイン駆動設計を理解し、習得していかれることを推奨します。

ドメイン駆動設計が示す内容は広く深いです。

その内容を深掘りしすぎて、何も始められないといったケースも少なくありません。

例えば、オブジェクト指向の基礎とDDD本の第5章位までを読んだら、後は、実際に試しながら理解を深めていく、といった方法を推奨します。

[目次に戻る]


最後までお読みいただきありがとうございます。

本記事やドメイン駆動設計支援ツールがご参考になれば幸いです。


また当社では、

Javaソースからメトリクスなどの情報抽出を行うためのツール「Jクラスレポート」を公開[無料]しています。ご参考ください。

[目次に戻る]