ここでは、ヘキサゴナルアーキテクチャを適用したドメイン駆動設計を、Javaを適用してどう実現するのかについて次の観点で解説します。
値オブジェクトの実装
何かを計測、説明するオブジェクトで不変性、等価性を持ち、交換可能なものは値オブジェクトとして実装し、それらの性質を保証します。
値オブジェクトは、交換可能(置換え可能)なので、参照透過です。
値オブジェクトは、他のオブジェクトに組み込み可能(@Embeddable)で、直列化可能(Serializable)かつ複製可能(Cloneable)なオブジェクトとして実現します。
直列化可能(Serializable)にしておくことでバイト配列として保存することができます。
@Embeddable
public class FullName implements Serializable, Cloneable{
}
不変性を実現するためには、private、あるいは、protectedなセッターを使って、状態変数であるフィールドを初期化します。
これを自己カプセル化といいます。
public FullName(String firstName, String lastName) {
this.setFirstName(firstName);
this.setLastName(lastName);
}
なお、JPAの@Entityで値オブジェクトを使用する(@Embedded)ためにはデフォルトのコンストラクターが必要です。
protected FullName() {}
等価性を実現するためには、clone()で複製可能にし、equals()を参照の比較(==)ではなく、内容の比較(equals)で行うように実装し、JUnitで、その検証ができるようにします。
Email、FullName、Address、URLなど、データドメイン(属性の定義域)が明確であるオブジェクトは、値オブジェクトとして実装してドメインを保証するようにします。
値オブジェクトをモデル化するときは、次の図のように、UMLのクラスのステレオタイプを使います。
エンティティの実装
値オブジェクトと異なり同一性と連続性(状態が変化する)を持つオブジェクトはエンティティとして実現します。
※エンティティ(実体)とは
エンティティは、JPAの@Entityで実現します。
JavaBeansに対応できるよう、Lombokの@Dataなどを使ってアクセサー(getter/setter)を実装します。
@Entity
@Table(name = “corporations”)
@Data
public class Corporation {
}
プログラムコードの中にプログラムが満たすべき仕様についての記述を盛り込む事で設計の安全性を高める技法に、契約による設計(Design By Contract)があります。
ここでは、契約による設計に従って、エンティティの仕様を、不変条件、事前条件、事後条件に分けてみていきましょう。
エンティティの不変条件
エンティティの不変条件は、同一性の保証です。
エンティティの識別子を、上記値オブジェクトとして実装することで(識別子オブジェクト)、後述するリポジトリで識別子を生成するときに、一意性、完全性(NotNull)を担保できれば、その内容を保証することができます。
なお、識別子オブジェクトを初期化するときのセッターでNullや空文字を防ぐようにし、JUnitで、その検証ができるようにすることで完全性を確保します。
@Entity
public class Corporation {
@Id
private CorporateId corporateId;
}
さらに、後述するファクトリで、エンティティの識別子をコンストラクタに渡すことでエンティティを初期化し、それをJUnitで検証することで、識別子の設定漏れを防ぐことができます。
エンティティの事前条件
エンティティの状態は、プロパティであるフィールドで実装します。
プロパティの値は値を渡されることで変化するので、値を受け取ったとき、値を渡す側が事前条件を守っているか、その妥当性を検証(バリデーション)する必要があります。
なので、エンティティの事前条件は、プロパティの値の妥当性であり、バリデーションによって保証します。
プロパティのバリデーションの実現方法の一つは、セッターを使う方法(自己カプセル化)です。
public void setFirstName(String firstName) {
if(firstName == null) {
throw new IllegalArgumentException(“The_firstName_may_not_be_set_to_null”);
}
if(firstName.isEmpty()) {
throw new IllegalArgumentException(“Must_provide_a_firstName”);
}
this.firstName = firstName;
}
プロパティのバリデーションの実現方法のもう一つは、JavaのBean Validationを使う方法です。
次のようにプロパティにアノテーションを設定し、
@NotNull
@NotEmpty
private String firstName;
後述するリポジトリでバリデーターを使って検証します。
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
また、プロパティのうちデータドメインが明確なもを上記値オブジェクトとして実装し、それをエンティティに埋め込む(@Embedded)ことで確実に、ドメインを保証することができます。
@Embedded
private Address address;
なお、値が必須であるプロパティは、コンストラクタで初期化するようにします。
※セッターは自己カプセル化されています。
public ApplicationTask(ApplicationTaskId applicationTaskId, CompanyId companyId, String applicationTaskName) {
this.setApplicationTaskId(applicationTaskId);
this.setCompanyId(companyId);
this.setApplicationTaskName(applicationTaskName);
}
あと、列挙型の値を文字列としてデータベースに保存したい場合は、次のように実装します。
@Enumerated(EnumType.STRING)
private AssociatedType associatedType;
エンティティの事前条件は、JUnitで検証するようにします。
エンティティの事後条件
出荷日は受注日より後でなければならないなど関連制約を含めたビジネスロジックは、コマンドを実行するエンティティ側が保証すべき事後条件になります。
エンティティの事後条件はセッターで実装し、JUnitで、その検証を行います。
エンティティをモデル化するときは、次の図のように、UMLのクラスのステレオタイプを使います。
関連の実装
次にドメインモデルを構成する要素(エンティティや値オブジェクト)同士の関連についてどう実装するか見ていきましょう。
値オブジェクトに対する関連
まず、値オブジェクトに対する関連から考えましょう。
上図のようにエンティティが一つの値オブジェクトに関連する場合、上述したように@Embeddedを使ってエンティティに組み込みます。
public class Customer{
@Embedded
private FullName fullName;
}
また、上図のようにエンティティが複数のの値オブジェクトに関連する場合、@ElementCollectionを使ってエンティティに組み込みます。
public class Customer{
@ElementCollection
private List<Email> emails;
}
@ElementCollectionを使う場合、永続化するときに、集約元と集約対象の関係を表すテーブルが必要になります。
エンティティに対する関連
次にエンティティに対する関連について見ていきましょう。
まず、下図のように受注が複数の受注明細を持つ場合の関連について考えてみましょう。
UMLでは、全体と部分の関係があり、全体側と部分側のオブジェクトのライフサイクルが同じ場合、コンポジション(黒塗りのひし形)という表記で表します。
この場合、受注と受注明細のライフサイクルは同じなので、受注と受注明細は同じタイミングで生成、更新され消滅します。
なので、永続化の観点で考えると、同じトランザクション境界を持つ集合になります。
ドメイン駆動設計では、この集合を集約と呼んでいます。
同じトランザクション境界を持つコンポジションの場合、JPAの@OneToManyなどのリレーションを使って実装することができます。
public class SalesOrder{
@OneToMany
private List<SalesOrder> salesOrderDetails;
}
車とタイヤも、ライフサイクルが同じコンポジションの関係です。
下図のcarIdは車の製造番号(シリアル番号)、tireIdはタイアの製造番号になります。
public class Car{
@OneToMany
private List<Tire> tires;
}
それでは、車型(車の種類を表す概念)と、タイヤ型の関係はどうでしょうか。
下図のcarTypeIdは車の種類を一意に識別する番号、tireTypeIdはタイアの種類を一意に識別する番号になります。
車とタイアの場合、それぞれの個体を表すので車に対するタイヤの多重度が4になっていますが、車型とタイア型の場合、それぞれの種類を表すので車型に対するタイヤ型の多重度が多になっています。
UMLでは、全体と部分の関係だが必ずしもライフサイクルが同じではない場合、集約(白塗りのひし形)という表記で表します。
集約の場合、タイヤ型はタイヤ型のライフサイクルを持ち、タイア型はタイヤ型のライフサイクルがあるので、同じトランザクション境界を持つ必要はありません。
なので、JPAのリレーションを使ってムリヤリ同じトランザクション境界にしてしまうと、パフォーマンスが落ちたりトランザクション障害を起こす可能性がでます。
そこで、ここでは、JPAのリレーションではなく値オブジェクトと同じように、相手のエンティティの代理オブジェクトを組み込むことでエンティティの関連を実装する方法を紹介します。
次の図のように、タイヤ型ではなく、識別子オブジェクトと名前など必要な属性だけを持つタイヤ型の代理となる値オブジェクトを関連させます。
public class CarType{
@ElementCollection
private List<AssociatedTireType> tireTypes;
}
こうしておくと、例えば、車型に対応するタイヤ型の一覧を表示するときは、代理オブジェクトが持つ名前を表示しておき、詳細情報が必要になるタイミングで、後述するリポジトリから実際のタイヤ型オブジェクトを取得して表示することができます。
これは、プロキシパターンというデザインパターンを応用した方法です。
上の例は、双方向の関連なのでタイヤ型も車型のフィールドを持たせる必要がありますが、その場合、次のように相手の識別子オブジェクト、あるいは、代理オブジェクトを持たせることで対応できます。
public class TireType{
@Embedded
private CarTypeId carTypeId;
}
public class TireType{
@Embedded
private AssociatedCarType carType;
}
代理オブジェクトを使うと、次の図のようにエンティティが多対多で関連している場合でも、互いの代理オブジェクトを持たせることで対応することができます。
ファクトリの実装
オブジェクトの生成過程をカプセル化する役割がファクトリです。
下図のように受注が複数の受注明細を持つ場合について考えてみましょう。
受注を生成するためには必ず受注明細が必要です。
これを保証するために、次のようなファクトリクラスを用意します。
受注を生成するために必要な識別子、金額、個数を与えるとファクトリが受注、および、受注明細を生成します。
その際、受注が勝手に生成できなようにプロテクトしておきます。
protected SalesOrder(){}
次に、上図を見ると受注明細を生成するためには必ず受注が必要であることがわかります。
これを保証するために受注に受注明細を生成させるようにします。
SalesOrderのcreateSalesOrderDetail
は、デザインパターンの一つであるファクトリメソッドです。
コンポジションや集約など全体と部分の関係がある場合、全体側に部分を生成するファクトリメソッドを定義するようにします。
リポジトリの実装
DDDの集約が生存期間中、それを一時的にデータベース上に保管・問い合わせするための役割がリポジトリです。
リポジトリは、JpaRepositoryを使います。
ただし、下図のように、RDBからMongoDBなどのNoSQLに変更したときも設定を変えれば対応できるようサービスからJpaRepositoryを呼び出すようにします。
なお、リポジトリのサービスはSpring MVCの@Serviceを適用して実現します。
リポジトリを使うのは後述するアプリケーションサービスです。
アプリケーションサービスでリポジトリを呼び出すときは、次のように@Qualifierで具体的なリポジトリを指定して呼び出します。
@Qualifier(“memberJpaRepository”)
@Autowired
private MemberRepository memberRepository;
これは、依存性逆転の原則(DIP)を応用した方法です。
なお、エンティティの識別子オブジェクトは、リポジトリが生成します。
public MemberId nextIdentity()
識別子には一意性の高いUUIDを使うことをお勧めします。
リポジトリは、JUnitを使って検証します。
@Serviceを使ってテストするためには@RunWith(SpringRunner.class)を使う必要があります。
@RunWith(SpringRunner.class)
@SpringBootTest
public class CoprporateRepositoryTest {
}
アプリケーションサービスの実装
は、ドメインモデルの直接のクライアントで、ユースケースのシナリオを実現するためにドメインモデルのタスクを調整します。
アプリケーションサービスはSpring MVCの@Serviceを適用して実現します。
ここでは、アプリケーションサービスの実装について次の観点で説明します。
- 事前条件
- 不変条件
- 事後条件
事前条件
アプリケーションサービスは、ヘキサゴナルアーキテクチャの要素として、ユーザーや他のアプリケーションのインターフェースとなるアダプタからDTO(Data Transfer Object)を受け取り、エンティティに変換してリポジトリに渡します。
なので、アプリケーションサービスは、DTOやパラメータを受け取ったとき、渡す側が事前条件を守っているか、その妥当性を検証(バリデーション)する必要があります。
アプリケーションサービスの事前条件は、JUnitを使って検証します。
@Serviceを使ってテストするためには@RunWith(SpringRunner.class)を使う必要があります。
@RunWith(SpringRunner.class)
@SpringBootTest
public class CorporateApplicationServiceTest {
}
不変条件
アプリケーションサービスの不変条件はトランザクション整合性を確保することです。
アプリケーションサービスのトランザクション制御は、Springの@Transactional(宣言的トランザクション)を適用して実現します。
アプリケーションサービスクラスに@Transactionalを付与することで、そのクラス内の全てのメソッドにトランザクション制御をかけることができます。
アプリケーションサービスは、エンティティ(データ)の保存や問合せなど、そのライフサイクルを管理するとき上述したリポジトリを使います。
なので、リポジトリは、エンティティを受け取ったとき、アプリケーションサービスが事前条件を守っているか、その妥当性を検証(バリデーション)する必要があります。
エンティティの状態を表すプロパティにJavaのBean Validationが適用されている場合、リポジトリでは、バリデーターを使って検証します。
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
それから、アプリケーションサービスがアクセスするリポジトリにも、@Transactionalを適用して、トランザクションを伝搬させます。
@Transactionalでロールバックさせたいときは、RuntimeExceptionを発生させる必要があります。
JPARepositoryのsaveなどデータアクセスは、DataAccessExceptionを発生させます。
DataAccessExceptionは、RuntimeExceptionの一種なので、JPARepositoryのsaveなどデータアクセスで例外が発生した場合、リポジトリにアクセスする@Transactionalサービスはロールバックされることになります。
次にアプリケーションサービスでデータ(エンティティ)のライフサイクルを管理するとき注意する必要がある点について、生成、更新、削除にわけて説明します。
データの生成
データを生成するときに注意することは次の点です。
識別子オブジェクトの生成
アプリケーションサービスは、データのライフサイクルを管理するリポジトリに識別子オブジェクトを生成させます。
識別子には一意性の高いUUIDを使うことをお勧めします。
生成日時の記録
アプリケーションサービスは、エンティティがいつ生成されたか日時を記録します。
生成日時が記録されると、それによってデータを検索することができます。
すると、データの種類によっては時代の変化によって削除してよいという判断ができるようになりデータの管理コストの節約に結びつきます。
また、サインインしたユーザーのIDも含めて誰が何のデータをいつ記録したかまで記録することもできます。
このようなデータを管理するためのデータをメタデータといいます。
データの更新
データを更新するときに注意することは次の点です。
更新日時の記録
上述した生成日時の記録と同様、メタデータとして更新日時を記録します。
楽観的同時実行制御
これは、トランザクションの独立性(Isolation)を確保するための制御です。
データを更新するとき、読み込んだときの更新日時と、更新しようとするタイミングの更新日時が異なる場合、他の人がすでに更新したとみなし更新できないようにします。
楽観的というのは、データを読み込んだタイミングで他の人が更新できないようにロックするのではなく、先に更新したほうが正と考えるからです。
なお、Spring Frameworkのバージョン5以降には、@Versionアノテーションが提供されています。
これは、エンティティクラスに追加することができ、楽観的同時実行制御を実現するために、バージョン番号を管理するアノテーションです。
@Version
private Long version;
このバージョン番号は、エンティティが更新されるたびに自動的にインクリメントされます。
楽観的同時実行制御は次のように実装することができます。
@Transactional
public Product updateProduct(Product product) {
Product existingProduct = productRepository.findById(product.getId())
.orElseThrow(() -> new EntityNotFoundException(“Product not found”));
if (!existingProduct.getVersion().equals(product.getVersion())) {
throw new OptimisticLockingFailureException(“Product has been updated by another user”);
}
existingProduct.setName(product.getName());
existingProduct.setPrice(product.getPrice());
return productRepository.save(existingProduct);
}
データの更新制約
これは、エンティティ間に集約やコンポジションの関係がある場合、全体側に対する部分側が存在する場合、全体側を更新できないようにするという制約です。
全体側の状態を参照して、部分側が生成される場合、データの更新制約を設ける必要があります。
データの削除
データの削除のとき注意すべきはデータの存在制約です。
例えば、受注と受注明細のコンポジションの場合、受注明細には受注が必要なので、受注を削除するときは、すべての受注明細も一緒に削除する必要があります。
テーブルの外部キーを定義して参照整合性を確保し、かつ、カスケード削除の制約を定義すると、@OneToManyなどJPAのリレーションによって受注を削除すると自動的に受注明細も削除してくれます。
次の例のようにコンポジションではなく集約の場合どうでしょうか。
この場合、全体側と部分側の生存期間は同じであるという制約はありません。
とはいうものの、集約の場合もコンポジションと同様、部分側には全体側が必須なので、部分側があると全体側を削除できないという制御を設けます。
なお、集約の場合、代理オブジェクトによって互いの関係を定義しているので、部分側を削除した場合、全体側の代理オブジェクトも削除する必要があります。
アプリケーションサービスの不変条件は、JUnitを使って検証します。
@Serviceを使ってテストするためには@RunWith(SpringRunner.class)を使う必要があります。
@RunWith(SpringRunner.class)
@SpringBootTest
public class CorporateApplicationServiceTest {
}
事後条件
コマンドを実行するアプリケーションサービスが保証すべき事後条件はビジネスロジックの実現です。
ここでは、ビジネスロジックに関連する次の点について説明します。
エラーハンドリング
ここでは、RESTによってフロントエンドアプリケーションと連携している場合、サーバー側で起きたエラーを、フロント側に伝えるための方法を紹介します。
まず、次のようなメッセージDTOを定義します。
@Data
public class MessageDto {
private String result;
private String errorCode;
private String errorMessage;
private String returnValue;
private String loginAddress;
}
そして、POST、PUT、DELETEの場合、アプリケーションサービスのビジネスロジックを実行しているとき、次のように処理します。
正常処理
MessageDto message = new MessageDto();
try{
//ビジネスロジック
//生成した識別子をフロントに返したい場合
message.setReturnValue(dataId.getDataCategoryId());
message.setResult(“OK”);
例外処理
}catch(Exception ex){
message.setResult(“NG”);
//フロントエンドアプリ側のメッセージコードを返すことで、それに対応するメッセージが表示される。
message.setErrorMessage(ex.getMessage());
}
ロギング
Log4j2を使っていつ誰が何の処理をしてどうだったかを記録することができます。
正常処理
Logger logger = LogManager.getLogger();
try{
//ビジネスロジック
//いつ誰が何の処理をしたか記録することができます。
logger.info(“ユーザーID:処理の名前”);
例外処理
}catch(Exception ex){
//いつ誰が何の処理したとき、どのようなエラーが発生したか記録することができます。
logger.error(“ユーザーID:処理の名前”, ex);
}
アプリケーションサービスの事後条件は、JUnitを使って検証します。
@Serviceを使ってテストするためには@RunWith(SpringRunner.class)を使う必要があります。
@RunWith(SpringRunner.class)
@SpringBootTest
public class CorporateApplicationServiceTest {
}
システム間連携の実装
ヘキサゴナルアーキテクチャでは、アダプタを介してユーザーや他のアプリケーションとシステム間連携します。
システム間連携には同期通信と非同期通信があります。
REST
相手と同期通信するときよく使われるのがHTTPのREST通信です。
REST通信は、Spring MVCの@RestControllerを使って実装します。
また、相手のREST APIを呼び出す場合は、RestTemplateを使います。
非同期メッセージング
相手と非同期通信したい場合、Spring JMSを使って実装します。
Sender
相手にメッセージを送信する場合。
次の例のように
private JmsTemplate jmsTemplate;
を使います。
public void sendCorporate(String corporateNumber){
jmsTemplate.convertAndSend(“Corporate.Queue”, corporateNumber);
}
Receiver
相手からのメッセージを受信する場合。
次の例のように、@JmsListenerを使います。
@JmsListener(destination = “Corporate.Queue”, containerFactory = “CompFactory”)
public void receiveCorporation(String corporateNumber){
applicationService.saveCorporation(corporateNumber);
}
パッケージ構成
ここでは、アプリケーションのパッケージ構成の例を示します。
ドメイン名.アプリケーション名以下を次のように分けます。
- application.service
アプリケーションサービスを格納する。
さらに各ドメインでパッケージを分ける。
インターフェースと実装を分けるときは.implに実装を格納する。 - domain.service
ドメインサービスを格納する。
さらに各ドメインでパッケージを分ける。
インターフェースと実装を分けるときは.implに実装を格納する。 - domain.model
ドメインモデルを格納する。
さらに各ドメインでパッケージを分ける。
ドメインごとのエンティティ、値オブジェクト、ファクトリを格納する。
インターフェースと実装を分けるときは.implに実装を格納する。 - adapter
さらに以下のように分ける。- db
さらにrdb、nosqlで分ける。
リポジトリを格納する。 - messaging
EventやSender、Receiverを格納する。 - rest
RectControllerやRestTemplateを格納する。
- db
DDD実装の流れ
DDDの実装は次のような流れで行います。
一つ一つ見ていきましょう。
データアーキテクチャの設計(Data Architecture)
データアーキテクチャは、エンタープライズアーキテクチャ(EA)を構成する一要素で、企業のデータの基本的な構造や振舞を表すものです。
データアーキテクチャに関しては、以下を参照してください。
【実践!DX】データアーキテクチャの設計方法
テーブルの実装(DDL)
データアーキテクチャの物理データモデルを参照して、DDL(Data Definition Language)でテーブルを定義し、それをデータベース上に実装します。
エンティティの実装(Entity)
DDLを参照して、エンティティを以下の手順で実装します。
- 値オブジェクトの実装
識別子オブジェクトなどエンティティに関連する値オブジェクトを実装します。 - 値オブジェクトの単体テスト
JUnitを使って、実装した値オブジェクトの単体テストをします。 - エンティティの実装
値オブジェクトを含むエンティティをSpring Dataの@Entityとして実装します。 - ファクトリの実装
エンティティは、ファクトリによって生成するようにするため、エンティティに対するファクトリを実装します。
リポジトリの実装(Repository)
次に以下の手順でエンティティに対するリポジトリを実装します。
- JpaRepositoryの実装
Spring DataのJpaRepositoryインターフェースを実装します。 - リポジトリインターフェースの実装
エンティティのリポジトリのインターフェースを実装します。 - リポジトリインターフェースの実現
リポジトリインターフェースを実現するリポジトリサービスを実装します。 - エンティティの単体テスト
エンティティの単体テストをします。 - リポジトリの単体テスト
リポジトリに実装されたJavaValidationの単体テストをします。
ビジネスアーキテクチャの設計(Business Architecture)
ビジネスアーキテクチャは、ビジネスの設計思想と、それを実現する仕組を表したものです。
ビジネスアーキテクチャに関しては、以下を参照してください。
【実践!DX】ビジネスアーキテクチャの設計方法
システムユースケースの定義(System Use Case)
ビジネスアーキテクチャの一つ、ビジネスプロセスのアクションフローを構成するアクションで、システムの機能によって実現するものをシステムのユースケースとして定義します。
システムユースケースについては、、以下を参照してください。
【UPで学ぶ】システム開発プロセス・要件定義
UI/APIの設計(UI/API)
次に、システムユースケースを詳細化し、システムのユーザーに対するインターフェース(ユーザーインターフェース)を設計します。
ここでいうユーザーは、システムが価値を提供する相手であり、システムと相互作用する人またはもの(対象のシステムの外部にある別のシステムなど)を表します。
UI/APIの設計については、以下を参照してください。
【UPで学ぶ】システム開発プロセス・外部設計
DTOの実装(DTO)
次にUI/APIに基づいて、ユーザーとやりとりするデータの構造をDTOとして実装します。
アプリケーションサービスの実装(Application Service)
次に、DTOを受け取って、ドメインモデルを制御するアプリケーションサービスを、Spring MVCの@Serviceとして実装します。
アダプタの実装(Adapter)
最後に、UI/APIとアプリケーションサービスのアダプタ(REST、非同期メッセージング)を実装します。
以上、ここでは、DDDの結果をJavaで実装する方法について解説しました。