2.4. アプリケーションのレイヤ化


本ガイドラインでは、アプリケーションを、次の3レイヤに分割する。

  • アプリケーション層

  • ドメイン層

  • インフラストラクチャ層

各層には、以下のコンポーネントが含まれる。

application layers
アプリケーション層とインフラストラクチャ層は、ドメイン層に依存するが、ドメイン層が、他の層に依存してはいけない。
ドメイン層の変更によって、アプリケーション層に変更が生じるのは良いが、
アプリケーション層の変更によって、ドメイン層の変更が生じるべきではない。

各層について、説明する。

Note

アプリケーション層、ドメイン層、インフラストラクチャー層はEric Evansの”Domain-Driven Design (2004, Addison-Wesley)”で説明されている用語である。ただし、用語は使用しているが以後”Domain-Driven Design”の考えにのっとっているわけではない。


2.4.1. レイヤの定義

入力から出力までのデータの流れは、アプリケーション層→ドメイン層→インフラストラクチャ層であるため、この順に説明する。


2.4.1.1. アプリケーション層

アプリケーション層は、クライアントとのデータの入出力を制御する層である。

この層では、

  • データの入出力を行うUI(User Interface)の提供

  • クライアントからのリクエストハンドリング

  • 入力データの妥当性チェック

  • リクエスト内容に対応するドメイン層のコンポーネントの呼び出し

などの実装を行う。

この層で行う実装は、できるだけ薄く保たれるべきであり、ビジネスルールを含んではいけない。


2.4.1.1.1. Controller

Controllerは、主に以下の役割を担う。

  • 画面遷移の制御(リクエストマッピングと処理結果に対応するViewを返却する)

  • ドメイン層のServiceの呼び出し (リクエストに対応する主処理を実行する)

Spring MVCでは、@Controllerアノテーションが付与されているPOJOクラスが該当する。

Note

クライアントとの入出力データをセッションに格納する場合は、セッションに格納するデータのライフサイクルを制御する役割も担う。


2.4.1.1.2. View

Viewは、クライアントへの出力(UIの提供を含む)を担う。HTML/PDF/Excel/JSONなど、様々な形式で出力結果を返す。

Spring MVCでは、Viewクラスが該当する。

Tip

REST APIやAjax向けのリクエストでJSONやXML形式の出力を行う場合は、HttpMessageConverterクラスがViewの役割を担う。

詳細は、「RESTful Web Service」を参照されたい。


2.4.1.1.3. Form

Formは、主に以下の役割を担う。

  • HTMLのフォームを表現(フォームのデータをControllerに渡したり、処理結果をフォームに出力する)

  • 入力チェックルールの宣言 (Bean Validationのアノテーションを付与する)

Spring MVCでは、Formオブジェクトは、リクエストパラメータを保持するPOJOクラスが該当する。form backing beanと呼ばれる。

Note

ドメイン層がアプリケーション層に依存しないようにするために、以下の変換処理をアプリケーション層で行う。

  • FormからDomain Object(Entity等)への変換処理

  • Domain ObjectからFormへの変換処理

これらの変換処理をController内で行うと、ソースコードが長くなり、 本来のControllerの処理(画面遷移など)の見通しが、悪くなりがちである。

変換処理のコードが多くなる場合は、以下のいずれか又は両方の対策を行い、Controller内のソースコードをシンプルな状態に保つこと推奨する。

  • Helperクラスを作成して変換処理を委譲する

  • MapStructを使用する

Tip

REST APIやAjax向けのリクエストでJSONやXML形式の入力を受ける場合は、ResourceクラスがFormの役割を担う。また、JSONやXML形式の入力データをResourceクラスに変換する役割は、HttpMessageConverterクラスが担う。

詳細は、「RESTful Web Service」を参照されたい。


2.4.1.1.4. Helper

Helperは、Controllerを補助する役割を担う。

Helperの作成はオプションである。必要に応じて、POJOクラスとして作成すること。

Note

Controllerの役割はルーティング(URLマッピングと遷移先の返却)であり、それ以外の処理(JavaBeanの変換等)が必要になったらHelperに切り出して、そちらに処理を委譲することを推奨する。

HelperはControllerの見通しを良くするためのものであるため、HelperはControllerの一部として扱ってよい。(Controller内のprivateメソッドみたいなものである)


2.4.1.2. ドメイン層

ドメイン層は、アプリケーションのコアとなる層であり、ビジネスルールを実行(業務処理を提供)する。

この層では、

  • Domain Object

  • Domain Objectに対するビジネスルールのチェック(口座へ入金する場合に、残高が十分であるかどうかのチェックなど)

  • Domain Objectに対するビジネスルールの実行(ビジネスルールに則った値の反映)

  • Domain Objectに対するCRUD操作

などの実装を行う。

ドメイン層は、他の層からは疎であり、再利用できる。


2.4.1.2.1. Domain Object

Domain Objectはビジネスを行う上で必要な資源や、ビジネスを行っていく過程で発生するものを表現するモデルである。

Domain Objectは、大きく分けて、以下3つに分類される。

  • EmployeeやCustomer, Productなどのリソース系モデル(一般的には、名詞で表現される)

  • Order, Paymentなどイベント系モデル(一般的には動詞で表現される)

  • YearlySales, MonthlySalesなどのサマリ系モデル

データベースのテーブルの1レコードを表現するクラスであるEntityは、Domain Objectである。

Note

本ガイドラインでは主に、状態のみもつモデルを扱う。

Martin Fowlerの”Patterns of Enterprise Application Architecture (2002, Addison-Wesley)”では、Domain Modelは、状態と振る舞いをもつものと定義されているが、厳密には触れない。

Eric Evansの提唱するようなRichなドメインモデルも、本ガイドラインでは扱わないが、分類上はここに含まれる。


2.4.1.2.2. Repository

Domain Objectのコレクションのような位置づけであり、Domain Objectの問い合わせや、作成、更新、削除のようなCRUD処理を担う。

この層では、インタフェースのみ定義する。

実体はインフラストラクチャ層のRepositoryImplで実装するため、どのようなデータアクセスが行われているかについての情報は持たない。


2.4.1.2.3. Service

業務処理を提供する。

本ガイドラインでは、Serviceのメソッドをトランザクション境界にすることを推奨している。

Note

Serviceでは、FormやHttpRequestなど、Webに関わる情報を扱うべきではない。

これらの情報は、Serviceのメソッドを呼び出す前に、アプリケーション層でドメイン層のオブジェクトに変換すべきである。


2.4.1.3. インフラストラクチャ層

インフラストラクチャ層は、ドメイン層(Repositoryインタフェース)の実装を提供する層である。

データストア(RDBMSや、NoSQLなどのデータを格納する場所)への永続化や、メッセージの送信などを担う。


2.4.1.3.1. RepositoryImpl

RepositoryImplは、Repositoryインタフェースの実装として、Domain Objectのライフサイクル管理を行う処理を提供する。

RepositoryImplの実装はRepositoryインタフェースによって隠蔽されるため、ドメイン層のコンポーネント(Serviceなど)では、どのようにデータアクセスされているか意識しなくて済む。

要件によっては、この処理もトランザクション境界となりうる。

Tip

MyBatis3を使用する場合は、RepositoryImplの実体を(一部)自動で作成する仕組みが提供されている。


2.4.1.3.2. O/R Mapper

O/R Mapperは、データベースとEntityの相互マッピングを担う。

MyBatis / Spring JDBCが、本機能を提供する。

具体的には、

  • MyBatis3を用いる場合は、MapperインタフェースやSqlSession

  • Spring JDBCを用いる場合は、JdbcTemplate

が、O/R Mapperに該当する。

O/R Mapperは、Repositoryインタフェースの実装に用いられる。

Note

MyBatis, Spring JDBCは「O/R Mapper」というより、「SQL Mapper」と呼んだ方が正確であるが、本ガイドラインでは「O/R Mapper」に分類する。


2.4.1.3.3. Integration System Connector

Integration System Connectorは、データベース以外のデータストア(メッセージングシステム、Key-Value-Store、Webサービス、既存システム、外部システムなど)との連携を担う。

Integration System Connectorは、Repositoryインタフェースの実装に用いられる。


2.4.2. レイヤ間の依存関係

冒頭で説明したとおり、ドメイン層がコアとなり、アプリケーション層、インフラストラクチャ層がそれに依存する形となる。

本ガイドラインでは、実装技術として、

  • アプリケーション層にSpring MVC, Thymeleaf

  • インフラストラクチャ層にMyBatis

を使用することを想定しているが、本質的には、実装技術が変わっても、それぞれの層で違いが吸収され、ドメイン層には影響を与えない。
レイヤ間の結合部は、インタフェースとして公開することで、各層が使用している実装技術に依存しない形式とすることができる。

レイヤ化を意識して、疎結合な設計を行うことを推奨する。

../_images/LayerDependencies.png

各レイヤのオブジェクトの依存関係は、DIコンテナによって解決される。

../_images/LayerDependencyInjection.png

2.4.2.1. Repositoryを使用する時の処理の流れ

入力から出力までの流れで表現すると、次の図のようになる。

Data flow from request to response

更新系の処理を例に、シーケンスを説明する。

項番

説明

Controllerが、Requestを受け付ける

(Optional) Controllerは、Helperを呼び出し、Formの情報を、Domain ObjectまたはDTOに変換する

Controllerは、Domain ObjectまたはDTOを用いて、Serviceを呼び出す

Serviceは、Repositoryを呼び出して、業務処理を行う

Repositoryは、O/R Mapperを呼び出し、Domain ObjectまたはDTOを永続化する

(実装依存) O/R Mapperは、DBにDomain ObjectまたはDTOの情報を保存する

Serviceは、業務処理結果のDomain ObjectまたはDTOを、Controllerに返却する

(Optional) Controllerは、Helperを呼び出し、Domain ObjectまたはDTOを、Formに変換する

Controllerは、遷移先のView名を返却する

Viewは、Responseを出力する。


各コンポーネント間の呼び出し可否を、以下にまとめる。

コンポーネント間の呼び出し可否

Caller/Callee

Controller

Service

Repository

O/R Mapper

Controller

../_images/cross.png ../_images/tick.png ../_images/cross.png ../_images/cross.png

Service

../_images/cross.png ../_images/exclamation.png ../_images/tick.png ../_images/cross.png

Repository

../_images/cross.png ../_images/cross.png ../_images/cross.png ../_images/tick.png
注意するべきことは、基本的にServiceからServiceの呼び出しは、禁止している点である。
もし他のサービスからも利用可能なサービスが必要な場合は、呼び出し可否を明確にするために、SharedServiceを作成すること。
詳細については、ドメイン層の実装を参照されたい。

Note

この呼び出し可否ルールを守ることは、アプリケーション開発の初期段階では、煩わしく感じられるかもしれない。

確かに、一つの処理だけみると、たとえばControllerから直接Repositoryを呼び出したほうが、速くアプリケーションを作成できる。

しかし、ルールを守らない場合、開発規模が大きくなった際に、修正の影響範囲が分かりにくくなったり、横断的な共通処理を追加しにくくなるなど、

保守性に大きな問題が生じることが多い。後で問題にならないように、初めから依存関係に気を付けて開発することを強く推奨する。


2.4.2.2. Repositoryを使用しない時の処理の流れ

Repositoryを作成することにより、永続化技術を隠蔽できたり、データアクセス処理を共通化できるなどのメリットがある。

しかし、プロジェクトのチーム体制によっては、データアクセスの共通化が難しい場合がある(複数の会社が、別々に業務処理を実装し、共通化のコントロールが難しい場合など)。
その場合、データアクセスの抽象化が必要ないのであれば、Repositoryは作成せず、以下の図のように、Serviceから直接O/R Mapperを呼び出すようにすればよい。
Data flow from request to response (without Repository)

各コンポーネント間の呼び出し可否を、以下にまとめる。

コンポーネント間の呼び出し可否 (without Repository)

Caller/Callee

Controller

Service

O/R Mapper

Controller

../_images/cross.png ../_images/tick.png ../_images/cross.png

Service

../_images/cross.png ../_images/exclamation.png ../_images/tick.png

2.4.3. プロジェクト構成

上記のように、アプリケーションのレイヤ化を行った場合に推奨する構成について、説明する。

ここでは、Mavenの標準ディレクトリ構造を前提とする。

基本的には、以下の構成でマルチプロジェクトを作成することを推奨する。


プロジェクト名

説明

[projectName]-domain

ドメイン層に関するクラス・設定ファイルを格納するプロジェクト

[projectName]-web

アプリケーション層に関するクラス・設定ファイルを格納するプロジェクト

[projectName]-env

環境に依存するファイル等を格納するプロジェクト

([projectName]には、対象のプロジェクト名を入れること)

Note

RepositoryImplなどインフラストラクチャ層のクラスも、project-domainに含める。

本来は、[projectName]-infraプロジェクトを別途作成すべきであるが、通常infraプロジェクトを隠蔽化する必要がなく、domainプロジェクトに格納されている方が開発しやすいためである。

必要であれば、[projectName]-infraプロジェクトを作成してよい。

Tip

マルチプロジェクト構成の例として、サンプルアプリケーション共通ライブラリのテストアプリケーションを参照されたい。


2.4.3.1. [projectName]-domain

[projectName]-domainのプロジェクト推奨構成を、以下に示す。

[projectName]-domain
  └src
      └main
          └java
              └com
                  └example
                      ├domain ...(1)
                      │  ├model ...(2)
                      │  │  ├Xxx.java
                      │  │  ├Yyy.java
                      │  │  └Zzz.java
                      │  ├repository ...(3)
                      │  │  ├xxx
                      │  │  │  └XxxRepository.java
                      │  │  ├yyy
                      │  │  │  └YyyRepository.java
                      │  │  └zzz
                      │  │      ├ZzzRepository.java
                      │  │      └ZzzRepositoryImpl.java
                      │  └service ...(4)
                      │      ├aaa
                      │      │  ├AaaService.java
                      │      │  └AaaServiceImpl.java
                      │      └bbb
                      │          ├BbbService.java
                      │          └BbbServiceImpl.java
                      └config
                          └app
                             ├[projectName]CodeListConfig.java ...(5)
                             ├[projectName]DomainConfig.java ...(6)
                             └[projectName]InfraConfig.java ...(7)

項番

説明

(1)

ドメイン層の構成要素を格納するパッケージ。

(2)

Domain Objectを格納するパッケージ。

(3)
リポジトリを格納するパッケージ。

エンティティごとにパッケージを作成する。
関連するエンティティがあれば、主となるエンティティのパッケージに、従となるエンティティ(OrderとOrderLineの関係であればOrderLine)のRepositoryも配置する。
また、検索条件などを保持するDTOなどが必要な場合は、このパッケージに配置する。

RepositoryImplは、インフラストラクチャ層に属するが、通常、このプロジェクトに含めても問題ない。
異なるデータストアを使うなど、複数の永続化先があり、実装を隠蔽したい場合は、別プロジェクト(またはパッケージ)に、RepositoryImplを実装するようにする。
(4)

サービスを格納するパッケージ。

業務(またはエンティティ)ごとに、パッケージインタフェースと実装を、同じ階層に配置する。入出力クラスが必要な場合は、このパッケージに配置する。

(5)

コードリストのBean定義を行う。

(6)

ドメイン層に関するBean定義を行う。

(7)

インフラストラクチャ層に関するBean定義を行う。


2.4.3.2. [projectName]-web

[projectName]-webのプロジェクト推奨構成を、以下に示す。

[projectName]-web
  └src
      └main
          ├java
          │  └com
          │      └example
          │          ├app ...(1)
          │          │  ├abc
          │          │  │  ├AbcController.java
          │          │  │  ├AbcForm.java
          │          │  │  └AbcHelper.java
          │          │  └def
          │          │      ├DefController.java
          │          │      ├DefForm.java
          │          │      └DefOutput.java
          │          └config
          │              ├app
          │              │  └ApplicationContextConfig.java ...(2)
          │              └web
          │                 ├SpringMvcConfig.java ...(4)
          │                 └SpringSecurityConfig.java ...(5)
          ├resources
          │  ├META-INF
          │  │  └spring
          │  │      └application.properties ...(3)
          │  ├i18n
          │  │   └application-messages.properties ...(6)
          │  └ValidationMessages.properties
          └webapp
              ├resources ...(7)
              └WEB-INF
                  ├views ...(8)
                  │  ├abc
                  │  │ ├list.jsp
                  │  │ └createForm.jsp
                  │  └def
                  │     ├list.jsp
                  │     └createForm.jsp
                  └web.xml ...(9)

項番

説明

(1)

アプリケーション層の構成要素を格納するパッケージ。

(2)

アプリケーション全体に関するBean定義を行う。

(3)

アプリケーションで使用するプロパティを定義する。

(4)

Spring MVCの設定を行うBean定義を行う。

(5)

SpringSecurityの設定を行うBean定義を行う。

(6)

画面表示用のメッセージ(国際化対応)定義を行う。

(7)

静的リソース(css、js、画像など)を格納する。

(8)

View(jsp)を格納する。

(9)

Servletのデプロイメント定義を行う。


2.4.3.3. [projectName]-env

[projectName]-envのプロジェクト推奨構成を、以下に示す。

[projectName]-env
  ├configs ...(1)
  │   └[envName] ...(2)
  │       ├java ...(3)
  │       └resources ...(3)
  └src
      └main
          ├java ...(4)
          │   └com
          │       └example
          │           └config
          │               └app
          │                   └[projectName]EnvConfig.java ...(5)

          └resources ...(4)
             ├META-INF
             │  └spring
             │      └[projectName]-infra.properties ...(6)
             └logback.xml ...(7)

項番

説明

(1)

全環境の環境依存ファイルを管理するためのディレクトリ。

(2)

環境毎の環境依存ファイルを管理するためのディレクトリ。

ディレクトリ名は、環境を識別する名前を指定する。

(3)

環境毎の設定ファイルを管理するためのディレクトリ。

サブディレクトリの構成や管理する設定ファイルは、(4)と同様。

(4)

ローカル開発環境用の設定ファイルを管理するためのディレクトリ。

(5)

ローカル開発環境用のBean定義(DataSource等)を行う。

(6)

ローカル開発環境用のプロパティを定義する。

(7)

ローカル開発環境用のログ出力定義を行う。

Note

[projectName]-domainと[projectName]-webを別プロジェクトに分ける理由は、依存関係の逆転を防ぐためである。

[projectName]-webが[projectName]-domainを使用するのは当然であるが、[projectName]-domainが[projectName]-webを参照してはいけない。

1つのプロジェクトに[projectName]-webと[projectName]-domainの構成要素をまとめてしまうと、誤って不正な参照をしてしまうことがある。

プロジェクトを分けて参照順序をつけることで[projectName]-domainが[projectName]-webを参照できないようにすることを強く推奨する。

Note

[projectName]-envを作成する理由は環境に依存する情報を外出し、環境毎に切り替えられるようにするためである。

たとえばデフォルトではローカル開発環境用の設定をして、アプリケーションビルド時には[projectName]-envを除いてwarを作成する。結合テスト用の環境やシステムテスト用の環境を別々のjarとして作成すると、そこだけ差し替えてデプロイするということが可能である。

また使用するRDBMSが変わるようなプロジェクトの場合にも影響を最小限に抑えることができる。

この点を考慮しない場合は、環境ごとに設定ファイルの内容を行いビルドしなおすという作業が入る。