エンティティの分析と設計

ドメイン・モデルの中心となるエンティティの分析モデルと設計モデルの違いについて考察します。

分析モデルから設計モデルの構築では非機能要求(特に品質属性)への考慮が重要な拡張項目です。

しかし、構造モデル自身にも拡張すべき項目が存在します。

中でも代表的なのが、エンティティを操作するための用途別オブジェクトの導入です。

本記事では、プログラムにおけるエンティティ表現の設計方針とアーキテクチャ、およびその自動生成の可能性について深掘りします。

プログラムにおけるエンティティの表現

SimpleModelingリファレンス・プロファイル (SMRP, SimpleModeling Reference Profile)ではプログラミング言語としてScalaを採用しています。

したがってエンティティの設計はScalaプログラムとして具現化されます。

SMRPではエンティティを操作する方式として、ここではバリュー・オブジェクト (value object)を使用する方式を採用しています。 このためエンティティの設計はScalaで記述したバリュー・オブジェクトの設計が中心となります。

単純な対応の限界

以下の図は、エンティティを操作するバリュー・オブジェクトを1つだけ使用する場合の入出力アーキテクチャの例です。

単純なバリュー・オブジェクトを軸とした入出力アーキテクチャ
Figure 1. 単純なバリュー・オブジェクトを軸とした入出力アーキテクチャ

バリュー・オブジェクトを使った場合、データの更新時に「データ読み込み・上書き・書き戻し」の非効率な処理になってしまいます。

また、型安全性を犠牲にして汎用的なRecord型を使うと、プログラムの信頼性が低下します。

異なる目的の処理に同一のバリュー・オブジェクトを使うと、使用方法との不整合が起きたり、余分な考慮が必要となったりします。

データ操作アーキテクチャ

図はSMRPで採用しているデータ操作アーキテクチャです。

エンティティ入出力のアーキテクチャ
Figure 2. エンティティ入出力のアーキテクチャ

エンティティの入出力を行う際、アプリケーションは以下の選択肢を取ることができます。

  • オペレーション用のエンティティ・バリューを使用

  • I/O用のエンティティ・バリューを使用

  • レコード機能を使用

  • プラットフォームの機能を直接使用

より上位の機能を使用するほど、型安全性によりプログラミングの容易さや信頼性が向上します。

Record

SMRPでは汎用のレコード・オブジェクトを用意しています。

細かな用途ごとに専用のScalaクラスを用意するのは煩雑であり、工数もかかります。 また、似て非なるクラスの数が増えすぎると、逆にバグ (bug)の発生を誘発してしまう可能性もあります。

こういった隙間の処理のための受け皿として使用するのがレコード・オブジェクトです。

SimpleModelingLibでは、以下のレコード・オブジェクトを用意しています。

  • org.simplemodeling.record.Record

エンティティの用途別クラス

分析モデルではエンティティを1種類のクラスとして定義しますが、設計モデルでは用途別に複数のクラスを定義し、それぞれ相互変換できる構成が望まれます。

シンプルなアプリケーションでは、エンティティとScalaクラスは一対一の関係で十分ですが、アプリケーションが本格的になると、用途ごとに複数のScalaクラスが必要になります。

エンティティの種類
Figure 3. エンティティの種類

分析モデルではエンティティを1種類のクラスとしてモデル化しますが、設計モデルでは用途別に複数のクラスを用意し、それぞれの相互変換を可能にしておく必要があります。

主な用途分類

  • ロジック処理用

  • ビュー

  • エンティティ操作

    • 読み込み

    • 新規作成

    • 更新

    • 一括更新

  • 外部連携

モデル空間における配置

アプリケーションのオペレーションスペースには、ロジック処理用のバリュー・オブジェクトが配置されます(図中では Entity Value for Operation)。

ビュースペースには、外部表示用のバリュー・オブジェクト(Entity Value for View)が配置されます。

エンティティ操作用には、以下のバリュー・オブジェクトがそれぞれの目的に応じて設計されます。

  • 読み込み用:Entity Value for Read

  • 新規作成用:Entity Value for Create

  • 更新用:Entity Value for Update

  • 一括更新用:図では省略

外部連携用には、Entity Value for Exchange が用意されています。

分析モデルの例

SimpleModelingでは、分析モデル・アップ・ダウン (Analysis Model Up/Down)のコンセプトに基づき、CML (Cozy Modeling Language)で記述した分析モデルから設計モデルを作成します。

以下のソースは、分析モデル Reservation をCMLで記述した例です。

reservation.cml
Reservation
===========

# Entity

## Reservation

### Attribute

| name       | type                    | mul |
|------------+-------------------------+-----|
| id         | identifier              |   1 |
| name       | name                    |   1 |
| description| description             |   ? |
| resourceId | ResourceId              |   1 |
| interval   | Interval[ZonedDateTime] |   1 |
| reserver   | UserId                  |   1 |

分析モデルをScalaで実現

分析モデルを素直にScalaオブジェクトとして実装すると、以下のようになります。

Reservation.scala
case class Reservation(
  id: Reservation.Id,
  name: Reservation.Name,
  description: Option[Reservation.Description],
  resource: ResourceId,
  interval: Interval[ZonedDateTime],
  reserver: UserId
)

設計モデル

分析モデルから派生させた設計モデルとして、以下の用途別Scalaオブジェクトを定義してみましょう。

  • ロジック処理用

  • ビュー

  • エンティティ操作

    • 読み込み

    • 新規作成

    • 更新

    • 一括更新

  • 外部連携

ロジック処理

CQRSのコマンド(Command)では、更新処理を中心として複雑なドメイン・ロジックが実行されます。

これらの処理では、メモリ上でエンティティの操作を行います。 そのため、メモリ上での複雑なドメイン・ロジック処理に必要な情報量を持ったクラスを定義する必要があります。

Reservation.scala
case class Reservation(
  id: Reservation.Id,
  name: Reservation.Name,
  description: Option[Reservation.Description],
  resource: Resource,
  interval: Interval[ZonedDateTime],
  reserver: User
)

ロジック処理に必要な情報は、参照先オブジェクトの内容を展開してバリューとして保持する必要があります。

ただし、参照先オブジェクトの全情報を展開する必要はありません。 外部に公開可能な情報のみを展開して保持します。 ビューほどは厳密でなくても構いません。

たとえば、属性 user はデータ型 UserId ではなく、展開されたバリュー User として保持されます。

ビュー用のクラスも、参照先のオブジェクトを展開して保持する構成になります。

ビュー

クラウド・ネイティブ・アプリケーションは必然的にCQRSのアーキテクチャになります。 CQRSのアーキテクチャでは、問い合わせに対するデータとしてビューを用意します。

このビュー向けのエンティティのバリュー・オブジェクトが必要となります。

Reservation.scala
case class Reservation(
  id: Reservation.Id,
  name: Reservation.Name,
  description: Option[Reservation.Description],
  resource: Resource,
  interval: Interval[ZonedDateTime],
  reserver: User
)

参照処理に必要な情報は、参照先オブジェクトの内容を展開してバリューとして保持する必要があります。 ただし、参照先オブジェクトの全情報を展開する必要はありません。 外部に公開可能な情報のみを展開して保持します。

たとえば、属性 resource はデータ型 ResourceId ではなく、バリュー Resource として保持されます。

同様に、属性 user は UserId ではなく、展開されたバリュー User を保持します。

エンティティ操作

データベース上で永続化されるエンティティに対して操作を行うためのバリュー・オブジェクト群です。

読み込み

エンティティを単体で読み込む際に使用するScalaクラスです。

データベースとの親和性を高めるため、基本データ型を中心に構成されます。

Reservation.scala
case class Reservation(
  id: String,
  name: String,
  description: Option[String],
  resource: String,
  interval: String,
  reserver: String
)

属性の型にはScala/Javaの基本型やSQLで扱いやすい型を用いています。

interval 属性は、アプリケーション内部では ZonedDateTimeのInterval 型ですが、データベースに格納するために String 型で保持されます(XMLやJSONにエンコード)。

読み込み用のクラスでは、分析モデルの定義に従って Option 型を適切に使用します。

新規作成

エンティティを新規作成する際に使用するScalaクラスです。

必須項目は必ず指定されるようにし、それ以外は Option 型で定義されます。

Reservation.scala
case class Reservation(
  id: String,
  name: String,
  description: Option[String],
  resource: String,
  interval: String,
  reserver: String
)

更新

エンティティを更新する際に使用するScalaクラスです。

IDは必須属性として含まれ、それ以外の更新対象属性は Option 型で定義されます。

Reservation.scala
case class Reservation(
  id: String,
  name: Option[String],
  description: Option[String],
  interval: Option[String]
)

実際には、Option 型だけでは「今回の更新で無視する項目」なのか「NULL に設定して値を削除したい項目」なのかが曖昧になります。

一括更新

エンティティのIDを使わず、条件と更新値の組み合わせで一括更新したい場合に使用されるScalaクラスです。

Reservation.scala
case class Reservation(
  name: Option[String],
  description: Option[String],
  interval: Option[String]
)

基本的には Update クラスと同様の構造ですが、id 属性が含まれていません。

id 以外の条件に一致するエンティティを一括で更新する用途に用いられます。

外部連携

外部連携を行う場合には、Scalaクラスと各種データフォーマット(XML, JSON, YAMLなど)との相互変換が必要になります。

  • XML

  • JSON

  • YAML

  • HOCON

  • Properties

  • gRPC IDL

これらの変換処理を都度手書きするのは大変なため、可能な限り汎用のデータ変換ライブラリを利用します。

SimpleModelingのリファレンス・プロファイルでは、Catsファミリーの Circe を採用しています。

Reservation.scala
case class Reservation(
  id: String,
  name: String,
  description: Option[String],
  resource: String,
  interval: String,
  reserver: String
)

データベース連携と同様に、各フォーマットとの変換を容易にするために、属性の型はできるだけ基本型に抑えます。

Circe が直接サポートしていないフォーマットであっても、この形式のクラスであれば比較的容易に連携処理を実装できます。

文芸モデル駆動開発

ここまで、ドメイン・モデルにおける分析モデルと設計モデルの対応について、特にエンティティの永続化の観点から考察してきました。

型安全なエンティティ操作を実現するためには、一つのエンティティに対して用途に応じて複数のバリュー・オブジェクトを定義する必要があります。

これらのクラスを人手でコーディングするのは非常に手間がかかりますが、この問題はプログラムの自動生成によって解決できます。

SimpleModelingでは、モデル・コンパイラ「Cozy」を用いて、CMLで記述された分析モデルから、以下のような用途別の設計モデル(Scalaクラス)を自動生成します。

  • ビュー

  • ロジック処理

  • エンティティ操作

    • 読み込み

    • 新規作成

    • 更新

    • 一括更新

  • 外部連携

まとめ

本記事では、ドメイン・モデルにおけるエンティティの分析モデルと設計モデルの違いに注目し、それぞれの用途に応じたバリュー・オブジェクトの設計方針と、Scalaでの具体的な実装例を通じて、その構成と役割を明らかにしました。

また、各用途(ビュー、ロジック処理、操作、外部連携)ごとに最適化されたクラス群を用意することで、型安全性と開発効率の両立が可能になることを示しました。

さらに、モデル記述とコード生成を統合する文芸モデル駆動開発Literate Model-Driven Development (LMDD, 文芸モデル駆動開発))の枠組みの中で、Cozyによる自動生成の活用が、この設計方針を持続可能なものにする鍵であることも紹介しました。

今後は、他のドメインへの適用や生成対象の拡張、さらにUIモデルやAPI仕様との統合といった展開も期待されます。