ドメイン駆動設計 ドメインモデルの動かし方

更新日

はじめに


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

もしも、DDD本の内容を必要以上に難解に考え過ぎたり、すべてを忠実に実践しようとすると、うまくいかないでしょう。

最初は、ドメイン内の小さなコアモデルから始めて、リファクタリングを繰り返しながら、ドメイン駆動設計スキルとドメインモデルの両方を、段階的に洗練していくことが良策です。


そのためには、動作可能な検証モデルを維持しながら、短いサイクルでイテレーションを繰り返すことができる仕組みが必要です。

つまり、イテレーションを軽快に繰り返せる開発環境が必要です。

ドメイン駆動設計ではイテレーティブな開発が推奨されます。

しかし実際には、繰り返されるたびにイテレーションは重くなりがちです。その結果、繰り返しが不可能になってしまうプロジェクトも多々あります。


本記事では始めに、ドメイン駆動設計の基本的な概念や考え方を共有し、次に、実践的な開発プロセスとして「Javaドメインモデルファースト開発」を提案します。 その中で「Javaドメインモデルファースト開発」の支援ツールとして DDBuilder (自動生成ツール) を紹介します。 支援ツールはフリーソフトとして本記事内で公開しています。

先に DDBuilder (自動生成ツール) を知りたい方は、記事の前半は飛ばして 「Javaドメインモデルファースト開発」に進んでください。

※ 初期公開時のDDBuilderにおいて、起動しないという不具合がありました。修正しましたので再度お試しください。


 

ドメイン駆動設計の考え方と主要なキーワード

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

※[英] Domain-driven design [略] DDD

※日本語版: 翔泳社 (ISBN 9784798121963) (本記事では DDD本 と略記します)


DDD本の初版は2003年の出版です。 少し遅れて、日本語版も2011年に出版されました。

すでに日々実践されている方も多いでしょう。

一方、ドメイン駆動設計は初めてという方や、オブジェクト指向にも馴染がなかったという方の場合は、少し時間がかかるかもしれません。

あるいは、乾いた砂が水をどんどん吸うように、受け容れることができるかもしれません。


本記事ではまず、ドメイン駆動設計の広範囲にわたる概念や指針の中から、 特に基本と考える部分について述べ、認識や知識を共有したいと思います。

 

ドメイン駆動設計の基本的な考え方

ドメイン駆動設計は、

・アプリケーションの本質はドメイン (業務領域) であり、そのドメインを現したドメインモデルを中心に設計・構築すること。

を原則とするソフトウェア設計手法です。


手法的には、

・モデル駆動開発

・オブジェクト指向技術

・アジャイル開発

といった、すでに実績のある手法と、そこで培われたノウハウを活かします。


そして、いくつかの制約を加えます。

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

・「分析モデル」や「設計モデル」のように、モデルの役割や位置づけを分離しません。1つのモデルを作成し、それを使い続けます。

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

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


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

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

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

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

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

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


DDD本には、かなり多くのことが書かれています。

そのため、ドメイン駆動設計を一気に網羅したり、簡潔にまとめることは容易ではありません。

また、読者一人ひとりによっても、視点や重要と思うポイントは様々でしょう。

ここではドメイン駆動設計を、ソフトウェアの開発方法論として見た場合と、パターン集として見た場合の2つの視点で考えてみます。


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

その代わりに、すでに多くの実績やノウハウが蓄積された既知の手法を活かします。

既知の手法とは、特にモデル駆動開発、オブジェクト指向モデリング・プログラミング、アジャイル手法です。


ドメイン駆動設計において、オブジェクト指向は必須ではないようですが、実績やモデル駆動との相性を考えると、事実上最適なパラダイムと言えます。

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

DDDで言うモデル駆動設計 (Model-Driven Design) では、分析モデルと設計モデルといった分け方をしません。両方の目的で使える唯一のモデルを作成します。

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

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

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


つぎに、設計パターン集や設計構築のガイドライン集という視点で見ると、 一覧形式にはなってはいませんが、 DDD本には多くの有用なパターンやガイドラインが書かれています。

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


ドメイン駆動設計のポイントをまとめてみます。

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

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

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

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

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


ドメイン駆動設計に関する情報源としては、DDD本以外にも多くの記事や書籍かあります。

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

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

A4サイズで90ページ程で、要約本としてよくまとまっていて、日本語も分かり易く、ドメイン駆動設計のエッセンスだと思います。


先にも書きましたが、DDD本には多くの事が書かれています。

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

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

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

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

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

以降では、まず始めるために、押さえておくべき主要なキーワードついて知識を共有したいと思います。

[目次に戻る]
 

利口なUI (アンチパターン) の罠とは

いきなり、アンチパターン (やってはいけない作り方) から始めますが、 オブジェクト指向技術やモデル駆動設計を実践してきた技術者にとって、DDD本は、ツボを得た考察やモデリングパターンをたくさん示してくれます。

つまり、向かうべき方向を示してくれる心強い本です。

しかし一方、「オブジェクト思考」ではない技術者にとっては、DDD本を読んで理解していくのは、相当の時間を要するかもしれません。

念のためですが、オブジェクト指向言語を使っていても、オブジェクト思考になっているとは限りません。

ドメイン駆動設計を始めるにあたっては、アンチパターンの代表として「利口なUI」パターンを理解しておくことが重要です。


例えば、Struts系のフレームワークを使ったWebアプリケーションが、下図のような構造になっているならば、オブジェクト思考ではありません。

ドメイン駆動設計的にはアンチパターンですが、実際の開発現場では多く使われているのが現実です。

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

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

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

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


アプリケーションアーキテクチャの例


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

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

(C) ではModelと称するクラスがありますが、実態はDAOのような役割しか持ちません。

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

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


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

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

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

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

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

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


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


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

利口なUI の利点

※引用:ここから (同じくDDD本の第4章から引用)

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

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

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


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

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

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

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

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

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

次項の「オブジェクト指向やドメイン駆動設計を導入する時の傾向」を参考ください。


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

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

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

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


利口なUI の欠点

※引用:ここから (同じくDDD本の第4章から引用)

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

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

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

[目次に戻る]
 

オブジェクト指向やドメイン駆動設計を導入する時の傾向

ドメイン駆動設計やオブジェクト指向を導入しても、途中で挫折したり、手法を変更したりするケースは少なくありません。

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

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

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


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


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

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

この問題の対策として、本記事の後半で「Javaドメインモデルファースト開発」を提案します。

[目次に戻る]
 

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

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

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

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

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

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


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

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

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

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


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

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

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

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

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

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

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

[目次に戻る]
 

ユビキタス言語

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

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

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


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

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

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


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

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

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

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

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

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


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

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

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

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


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

[目次に戻る]
 

ドメインモデル

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

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

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

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

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

[目次に戻る]
 

エンティティ

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

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

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

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

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

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

ドメイン駆動設計はオブジェクト指向モデリングを必須とはしませんが、多くのチームが、オブジェクト指向モデリングを採用するでしょう。

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

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

上にも述べましたが、DOA (データ中心アプローチ) になれている方の場合、1つのテーブルとして抽出した概念やモノは、多くの場合、エンティティとして適切か、有力な候補になるはずです。

[目次に戻る]
 

値オブジェクト

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

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

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

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

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

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

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

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

[目次に戻る]
 

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

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

集約の例

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

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

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

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


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

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

集約の例

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


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

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

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

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

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

[目次に戻る]
 

サービス

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

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

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

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

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


本記事の後半で、次のようなサンプルモデルとJavaコードを紹介しています。

例1 「Javaドメインモデルファースト開発」を使ったドメイン駆動設計の流れ

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


[目次に戻る]
 

モジュール

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

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

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

[目次に戻る]
 

集約 (AGGREGATES)

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

※引用:ここから (DDD本 P123 から引用)

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

※引用:ここまで (DDD本 P123 から引用)


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

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

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

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

集約の例

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

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

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

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

[目次に戻る]
 

他の手法との関係

ドメイン駆動設計では、新しいモデリング技術や特別な設計プロセスは提唱していません。

ドメイン駆動設計は「アプリケーション構築において最も重要なものはドメインモデルである」とし、ドメインモデルを中心に置く設計手法です。

ドメインモデリングには、モデル駆動設計 (MDD) や、オブジェクト指向 (OO) の世界で培われてきたノウハウを最大限活用します。

ドメイン駆動設計は、モデル駆動設計やオブジェクト指向技術と協調します。

モデル中心の考え方はモデル駆動設計から取り入れ、 そのモデルを表現する手段としてオブジェクト指向のパラダイムを使用します。

(オブジェクト指向は必須ではありませんが、事実上使用されます)

モデル駆動設計やオブジェクト指向はモデル化する対象を特に限定しませんが、ドメイン駆動設計はアプリケーションのドメインを中心に置きます。

例えば、MVCモデルのビュー層や永続化層は、ドメイン駆動設計において、主要な関心事ではありません。

ドメイン駆動設計では、開発プロセスとしてアジャイル開発が基本です。

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

[目次に戻る]
 

モデリングパラダイム

ドメイン駆動設計、モデル駆動設計では、モデリングパラダイムを限定しませんが、 実際に主流となっているのは、オブジェクト指向パラダイムです。 DDD本以外にも、ドメインモデル作成の最初の取っ掛かりや、その後のリファクタリングに役立つモデリング手法があります。 参考になりそうな手法を次に挙げてみます。

[目次に戻る]
 

アナリシスパターン

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

[目次に戻る]
 

デザインパターン

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

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

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

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

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

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

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

[目次に戻る]
 

CRC (Class Responsibility Collaborator)

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

[目次に戻る]
 

Naked Objects パターン

ここで、ドメイン駆動設計に通じるコンセプトを持つ Naked Objects パターンを紹介します。 Naked Objects パターンでは、ドメイン駆動設計と同様にドメインモデルに重点を置きます。 アプリケーションを開発する時には、ドメインオブジェクトだけを作成します。 ユーザインタフェース層やデータアクセス層は、ドメインオブジェクトから自動で作成します。 十分に洗練され深化したドメインモデルがあれば、 ドメインモデルの各インタフェース (サービスやクラスのメソッド、プロパティ) をビューとしてユーザに公開 (画面など) するだけで、 アプリケーションとして機能する、という考えです。 大きなアプリケーションでの事例もあるようです。 本記事で紹介するドメイン駆動設計支援ツール DDBuilderは、Naked Objects パターンにインスパイア (触発) されたものです。

[目次に戻る]
 

実践の流れと工夫すべきポイント

ドメイン駆動設計では反復型開発 (イテレーティブ開発) が基本です。

リファクタリング (洗練) と検証を繰り返し、段階的にモデルの完成度を上げていく手法です。

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

ドメインモデルと実装を常に結びつけて繰り返すことが重要です。


[反復型開発の流れ]

反復型開発の流れ図

 

ドメインモデルをいつでも動かせること

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

ソフトウェアで表現されたドメインモデル (例: Javaで実装されたドメインモデル) も、実際に動かすためには単体テストコードやドライバのようなものが必要です。

本記事では「動くソフトウェアモデル」とは、ドメインモデルのサービスやメソッドをブラウザ画面から実行できる「Java Webアプリケーション」を指すこととします。

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

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

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

 

質の高いイテレーション

イテレーションは反復型開発のコアプロセスです。 動くソフトウェアモデルで検証すればイテレーションの品質・精度が上がります。 イテレーションを軽快に繰り返すことができれば、チーム (ユーザ、ドメインエキスパート、技術者など) の関心事をドメインモデルに集中できます。

ただ現実には、イテレーションを何度も繰り返すなかで、動くソフトウェアモデルを ”いつでも動かせる状態” に維持することは、簡単ではありません。 ドメインモデルを動かすために、想定以上に時間を要するケースが多いからです。 例えば、ドメインモデルをWebアプリケーションで動かそうとする場合、イテレーションの度に、即ちドメインモデルをリファクタリングする度に、ユーザインタフェース層やインフラストラクチャ層などの更新も必要になるからです。 イテレーションの本来の目的はドメインモデルのリファクタリング (洗練) です。しかし、動くソフトウェアモデルの維持に手間がかかって、簡単には繰り返せなくなることがあります。 1回1回のイテレーションが重くて手間がかかるようでは、反復型開発のメリットは得られません。 この問題を解決するためには、開発プロセスの一部を自動化するなど、何らかの対策や工夫が必要です。

 

検証モデル (アプリケーション) 自動生成などの活用

適切なツール等の活用によって、次のような効果が期待できます。
  • イテレーションを軽量化できます。その結果、短いサイクルで何度でも繰り返せます。
  • リファクタリング (洗練) したドメインモデルを、素早く検証し結果を確認できます。
  • 開発チーム (ユーザ、ドメインエキスパート、技術者など) がドメイン以外の問題に煩わされないで、ドメインモデルに集中できます。
  • ドメインモデルをコードファーストでリファクタリングすれば、すぐに動かしてみることができるので、ドメイン駆動設計の入門や学習にも役立ちます。

各イテレーションで何をどのように行うかは、開発の成否において重要なファクターになります。

もし、イテレーションの中でやらなくてはいけない「ドメイン以外の作業」が多いと、開発プロセスの速度と質は低下します。

例えば、イテレーションサイクルが長いと、ドメインモデルのリファクタリング (洗練) に集中できなくなる、などです。

実際、このような速度と質の低下が発生しがちです。

後述のDDBuilderは、ドメインモデルから「動くソフトウェアモデル」 を自動生成することで、イテレーションに含まれるドメイン以外の問題から開発チームを開放し、 開発プロセスに生じる速度や質の低下など、実践時の現実的な問題解消に役立ちます。

【補足】ドメインモデル以外の作業とは

ドメインモデルのサービスやメソッドを検証するためのインタフェース画面やテストコードの実装作業、 ドメインモデルの永続化の実装作業などのリファクタリング (洗練) に伴う諸々の作業です。

[目次に戻る]
 

Javaドメインモデルファースト開発

「Javaドメインモデルファースト開発」では、ドメイン駆動設計の Java ドメインモデルをコードファーストでイテレーティブに開発します。

基本的に、モデル図よりも、コードで表現したモデル (Javaコード) を先行させる開発スタイルです。

もちろん、モデル図で考えた方が分かり易い部分は、モデルファーストで行います。

コードファーストのメリットは、モデル図はそのまま動かすことはできませんが、 コードファーストであれば、(ツール等の支援があれば) いつでも動かして、検証できることです。

また、Javaコードからモデル図を作成するツールを利用すれば、常に、実装とモデルが一致し、動作可能で検証可能なドメインモデルを維持することができます。

 

ドメイン駆動設計支援ツール DDBuilder

DDBuilderは、Java ドメインモデルから Web アプリケーションを自動生成するソフトウェアツールです。

DDBuilderを活用すると、ドメインモデリングを Java コードファーストでイテレーティブに実践できます。

この記事でのコードファーストとは、モデル図を必要としないという意味ではありません。 モデル図で検討する方が良いと思われる部分は、コードよりも先にモデル図を作成するべきです。 ただ、そのモデル図はスケッチの方が良いかもしれません。 正確なモデル図は、新規作成にも変更時にも手間がかかります。 UMLツール等でコードから生成するようにしないと、モデル図と実装を一致させることは困難です。 なによりツール無しでは工数がかかります。 ドメインモデルからアプリケーションを自動生成するソフトウェアツールがあれば、ドメインモデルの リファクタリング → 動くソフトウェアモデルで検証 → リファクタリング → … という繰り返しがスムーズに実践できます。

ドメイン駆動設計では反復型開発が基本です。リファクタリング (洗練) と検証を繰り返し、段階的にモデルの完成度を上げていく手法です。

反復型ではイテレーションの速度と質が重要になってきます。


DDBuilderは、ドメインモデルから「動くソフトウェアモデル」を自動生成することで、イテレーションを軽量化・高速化し、 反復サイクルを短くします。

開発者はドメイン以外の問題から解放され、本来のドメインに集中できるようになり、イテレーションの質を向上できます。

DDBuilderのポイントは、入力が Java で実装されたドメインモデル、出力が Java Web アプリケーションであることです。

開発者はドメインモデルを Java で実装し、 DDBuilderは、ドメインモデル (Java ソースコード) を読込み、Java Web アプリケーション (Java ソースコード) を生成します。


「ドメインモデルファースト開発」の流れ図


【DDBuilderの役割】

DDBuilderはドメインモデルから Web アプリケーションを自動生成することで繰り返し (反復、イテレーション) を軽量化します。

上図に示すように、開発チーム が設計し、Java で実装したドメインモデル から、検証可能な動くソフトウェアモデル (Java Web アプリケーション) を生成することで、 動くソフトウェアモデルの実装時間と手間を軽減します。

イテレーションの度に行っていたユーザインタフェース層やインフラストラクチャ層の実装作業がドメイン中心になり、 開発チーム (ユーザ、ドメインエキスパート、技術者など) はドメインモデルの分析・設計・実装・評価に集中できるようになります。

単純な実装ミスも減少します。

 

成果物はソフトウェアで表現されたドメインモデル

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

本当の成果物は、「ソフトウェアで表現されたドメインモデル」です。

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

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

ドメインモデルは容易に切り離せます。

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

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

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

 

ドメインモデルをJavaコードファーストでリファクタリング

イテレーションの流れ図



[DDBuilderとEclipseの関係]

DDBuilderとEclipseの役割図


[1]生成/読込み更新

   初回は何も存在しませんので生成動作です。

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

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

   この処理は、DDBuilder画面のボタン押下で実行されます。

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

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

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

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

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

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

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

[目次に戻る]
 

期待できる効果

  • イテレーションを軽量化(工数削減・期間短縮)できます。
  • 手組み実装によるミスとロスを排除できます。
  • 開発チームは、非本質的な作業から解放されます。
  • コード中心・実装一致で実践できます。
[目次に戻る]
 

求められる知識

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

関連する知識

パターンという観点で簡単に区別してみます。

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

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

DDBuilderは、実際の開発プロセスだけではなく、入門や練習にも効果的です。

入門者でも、まずは試してみることができます。 はじめるときのハードルも高くありません。 実際にドメインモデルを Java で実装してみて、アプリケーションとしての振る舞いを、あまり手間をかけずに、具体的に試してみることができます。 そうすれば、トランザクションスクリプトとの違いを具体的に理解できて、ドメインモデルの役割も捉えやすいはずです。

また、開発着手時の生産性の問題の解消策にもなります。

オブジェクト指向、モデル駆動設計、アジャイル開発、ドメイン駆動設計などの入門者にとって、 机上だけでドメインモデルを理解したり、イメージしたりすることは、難しく、時間がかかるかもしれません。 実際に実装し、動かしてみることができれば、理解もしやすいでしょう。

ドメイン駆動設計が示す内容は広く深いです。 その内容に拘り過ぎて、何も始められないといったケースも少なくありません。 例えば、オブジェクト指向の基礎とDDD本の第5章位までを読んだら、後は、 実際に試しながら理解を深めていく、といった方法を推奨します。

[目次に戻る]
 

ユーザガイド

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

[目次に戻る]
 

ダウンロード

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

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

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

お問合せ

[目次に戻る]
 

動作確認済み環境

  • 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のソースコードは、準備が整い次第、公開する方向です。

[目次に戻る]

参考

例1 「Javaドメインモデルファースト開発」を使ったドメイン駆動設計の流れ

「Javaドメインモデルファースト開発」でドメイン駆動設計を実践する流れを示します。

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

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

DDBuilderとEclipseの関係

下図の内容を画面キャプチャを使って説明します。

「Javaドメインモデルファースト開発」でのDDBuilderとEclipseの役割図

DDBuilder の起動

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

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

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

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

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

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

基本情報の設定

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

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

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

下図は入力例です。

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

初回の生成

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

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

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

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

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

Eclipse画面例


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

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

Webアプリケーションの実行確認

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

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

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

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

デバッグ実行画面


上図はEclipse内蔵のブラウザで表示された状態ですが、Eclipse外のブラウザからもアクセスしてテストできます。

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

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


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

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

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

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

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

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

ドメインモデルを作成

まず、もっとも簡単な例として、下図の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を作成するときの例

ドメインモデル (Productクラス) を Webアプリケーションに反映

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

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

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

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

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

DDBuilder 操作画面

Eclipse プロジェクトをリフレッシュ

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

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

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

再度 Webアプリケーションを動かしてテスト

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

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

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

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


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

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

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


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

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

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


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

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

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


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

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


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

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

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


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

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


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

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

ドメインモデルを追加

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

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


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


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

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

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


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

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

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

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

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

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


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

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

インスタンスの一覧画面


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

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

インスタンスの編集画面


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

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

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

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

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

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


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

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

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

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

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


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

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


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

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


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

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


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

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

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

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

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


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)をご覧ください。

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

[戻る]



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

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

DDD本に示されているレイヤ化アーキテクチャを参考にしています。

「開発者作成」: 開発者は、ドメインモデリングして、Javaで実装します。DDBuilderはこのJavaクラス群を読み込みビュー層などを生成します。

※ドメインモデリングでは、モデリング対象領域の複雑度や、チームの方針などによって、クラス図などを作成する場合もあれば、すぐにJavaコードで実装する場合があると思います。


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


DDBuilderが生成する「動くソフトウェア」は、Java Web アプリケーションです。

Eclipseのプロジェクト形式になっていますので、Eclipseにインポートして、編集・実行できます。

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

ビュー層: Apache Wicket

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

※上図中の「DDBuilder基底」部分に、DDBuilder固有の簡単な基底クラスが存在します。


ロックインフリー

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

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

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

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

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

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


自作ページの追加

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

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

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

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

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


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の知識が必要になります。

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


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

[目次に戻る]



例3 DDBuilderがサポートする関連タイプと属性型のサンプル

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

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

[コードをダウンロード]


UMLクラス図

関連の例


DDBuilder 関連タイプと属性型のコードサンプル


/*
 * 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本 P123 から引用)

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

※引用:ここまで (DDD本 P123 から引用)

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

[目次に戻る]


例4 クラス図の扱い方

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

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

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


また当社では、

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

[目次に戻る]


Copyright 2001-2018, All Rights Reserved