テーブルモジュールパターンからドメインモデルへ(SAP CAP設計パターンの変更)

# テーブルモジュールパターンからドメインモデルへ(SAP CAP設計パターンの変更)

SAPのサーバーサイド用のフレームワークであるCAPの設計に、テーブルモジュールが使えることを示した[^1]。CAPがフレームワークとして提供する配列（二次元表）をテーブルモジュールクラスの生成に使い、ドメインロジックを実現する。データソース、UI、ドメインロジックの３者が同一の二次元表で実現できるとき、テーブルモジュールパターンは有効。ただ、ドメインロジックが二次元表でおさまらないことも多い。それに気づいた段階で**ドメインモデルに移行**していくことになる。

CAPのフレームワークから見て、どのようなときにテーブルモジュールパターンが使え、その限界がどこにあるのかについてSAP開発コミュニティで議論があってもいいと思う。本記事では、テーブルモジュールパターンからドメインモデルへ移行するときの変化点について示す。

## テーブルモジュールクラスの分解

テーブルモジュールでは、CDSの定義により使える配列（データセット）を利用した。その部分は生かしたい。そこで、ドメインモデルだけを抽出する。この抽出の境界をどこに設定するかによって、パターンが異なる。

### CAPのどこで境界線をひくか

テーブルモジュールを分解する前に、そもそもCAPでどのような分け方ができるかを確認しておきたい。

#### 境界によるアーキテクチャ分類

CAP設計のどこかにデータソースレイヤとドメインロジックレイヤの境界線を引く。すると境界線をまたぐオブジェクトが必要になる。境界はまたぐオブジェクトで表すことができる。SQL、DTO、データセット、個別ドメインオブジェクト、集約の５パターンがある。

テーブルモジュールパターンはデータセットを境界とした。本記事では、レポジトリによりドメインモデルに移行する。他にも個別ドメインオブジェクトに境界を引くこともできる。

| 境界   | データソースパターン  | ドメインロジックパターン | CAPで実現できるか  |
|-----------------|-----------------|-----------------|--------------------------------|
| SQL | クエリ関数 |  データフローパイプライン[^5]   | 可 |
| DTO  | ローデータゲートウェイ | トランザクションスクリプト | 素朴なケースで可 |
| データセット     | テーブルデータゲートウェイ | テーブルモジュール | 二次元表に限定して可 |
| 個別ドメインオブジェクト    | データマッパ | ドメインモデル | 可 |
| 集約                  | レポジトリ   | ドメインモデル | 可 |

#### SQLを境界にする：データフローパイプライン

先にドメインモデルではない（オブジェクト指向ですらない）ものを確認しておく。

クエリ関数でSQLを生成する。CAPのCQLもSQLを生成するJavaScriptの関数である。CQLは`SELECT.from().where().columns()`のようなまるでSQLのような見た目をしている。CQLはクエリ関数といえる。

**データフローパイプライン**[^5]は、関数型プログラミングでドメインロジックを表現する方法。IOからIOの間を関数でつなぐ。関数が連続することもある。それをデータフローパイプラインという。そのパイプラインでビジネスロジックが表現される。関数の引数と返り値にTypeScriptの型を与えるが、この型をビジネスコンセプトと対応付ける。データフローパイプラインは型によって安全につながる。

このパターンでは、CAPのCQLはビジネスロジックの一部となる。CQLの`SELECT`や`INSERT`などがデータフローパイプラインの起点や終点となる。

#### DTO／データセットを境界にする：トランザクションスクリプトまたはテーブルモジュール

DTOのようにロジックを中に一切持たないオブジェクトを返すのは**ゲートウェイ**[^4]。テーブルモジュールパターンに読み替えれば、DTO（の配列）はデータセット。

CAPにはサービスクラスが継承する`cds.ApplicationService`にCRUDメソッド（`read()`メソッドなど）がある。`.read()`メソッドはオブジェクトの配列を返してくれる。当然、返された配列にビジネスロジックはついていない。DTOといえる。すなわち、`cds.ApplicationService`はゲートウェイといえる。

ODataからのAPIがキーを指定した場合にはローデータゲートウェイとして機能し、キーを指定しない場合にはテーブルデータゲートウェイとして機能する。継承したクラス内でゲートウェイ機能を使えるだけではなく、`cds.connect.to()`で接続して外部からもゲートウェイ機能を使える。

CAPでは、フレームワークの境界がDTOまたはデータセット。ここで境界線を引くと、トランザクションスクリプトかテーブルモジュールになる。

#### ドメインオブジェクトを境界とする：ドメインモデル

ドメインオブジェクトを返すためには、（TypeScriptでクラスベースの開発をする場合）クラスの定義と生成の必要がある。それを行ってくれるのが**データマッパ**。生成のバリデーションは各ドメインオブジェクトが行う。

データマッパはCAPでは提供していない。CAPはORMではない。必要なら開発者が独自実装する。だが、CAPでデータマッパの開発の機会は少ないと思う。

仮に、CDSで深い階層が生じたとしても、Deep Readをせずに各エンティティから別々にデータセットを取得し、各要素からドメインオブジェクトを生成する実装をしたとしたらデータマッパとなる。どうせ実装するならレポジトリにするだろう。

#### 集約を境界とする：ドメインモデル

複数あるドメインオブジェクトを集約としてひとまとめにして、相互の整合性を保証した状態で出し入れを行うのは**レポジトリ**。ドメインオブジェクト単独では整合性の保証ができないので、集約ルートで整合性を保証する。レポジトリは集約ルートに限定する役割となる。

集約のデータ範囲は自分が実装する側の場合にはDeep Readで関係するデータをまとめて取得するイメージ。S/4HANAなどの他のサービスを使う場合も集約の単位で提供されているはず。その単位でドメインオブジェクトを作る。その内部もドメインオブジェクトで構成される。

集約で境界線を引きレポジトリを作るパターンもドメインモデルパターンの一種。


### 分解のイメージ

本記事では、テーブルモジュールからドメインモデルに変更する。具体に入る前にイメージをつけたい。

#### OとRの分離と集約の構成を行うイメージ

変更前はテーブルモジュールだった。ドメインモデルの機能を抽出することで、ドメインオブジェクトを切り離す。個別ドメインオブジェクトを生成するのではなく、集約を生成する。その結果レポジトリとなる。

```plantuml
hide circle
rectangle テーブルモジュール{
class a as "ドメインオブジェクト抽出" #palegreen ##[dashed]green
class データセット<<DATASET>>
class テーブルモジュール
a <-- テーブルモジュール #green;line.dashed;text:green
テーブルモジュール o-> データセット : 保持
}
rectangle ドメインモデル{
class 集約ルート
class 値オブジェクトA
class 値オブジェクトB
class データセット <<DATASET>>
class レポジトリ
値オブジェクトA <-o 集約ルート
集約ルート o-> 値オブジェクトB
集約ルート <-- レポジトリ
レポジトリ o-> データセット : 保持
```

### 分解の実際
#### サービスクラス：レポジトリの生成

もとはテーブルモジュールインスタンスを生成。生成されたインスタンスのビジネスロジックを呼び出していた。データセットを抽出するクエリはサービスクラスから`deepRead.call()`を呼び出していた。

```typescript: 変更前
export class RevenueCalculationServiceTM extends cds.ApplicationService {
  this.on("calculateRecognitions", this.entities.Contracts, 
    /* ... */
    const contracts = new Contracts( await deepRead.call(this, /* ... */));
    contracts.calculateRecognitions(/* ... */));
    /* ... */
```

変更後はドメインモデルの生成の前に、まずレポジトリを生成。レポジトリクラスは`ContractRepository`。CAPの`cds.ApplicationService`がデータアクセスの機能を提供することは変わらないので、データアクセス機能をレポジトリにインジェクションするため、`this`（サービスクラス）を引数として渡す。

生成されたレポジトリインスタンスの`.read()`メソッドによりドメインモデルのインスタンスを生成する。

```typescript: 変更後
export class RevenueCalculationServiceDM extends cds.ApplicationService {
  this.on("calculateRecognitions", this.entities.Contracts,
    /* ... */
    const repository = new ContractRepository(this);
    const aContract = await repository.read(/* ... */);
    aContract.calculateRecognitions();
    /* ... */
```

#### 集約の生成

元はデータセットを外部の関数をつかって取得していた。そのデータセットはテーブルモジュールクラスのコンストラクタ引数としていたのは上記でみた。

```typescript:変更前
async function deepRead( this: RevenueCalculationServiceTM, contractID: number): Promise<Contract[]> {
  return await SELECT.from(this.entities.Contracts, /* ... */
```

変更後は、レポジトリ`ContractRepository`の`read()`関数にクエリが移動した。変更前と同様にクエリを実行する。その結果得られたデータセット`dataset`から、集約ルートのクラスのインスタンスを生成して返す。

集約ルートは`Contract`。クラス`Contract`を定義しておき、レポジトリで生成する。集約ルートなので、関連するオブジェクトも合わせて生成する。すべてが正常に生成されないと`Contract`の生成ができない。

ソフトウェアライセンス契約のキー`ID`、契約日`whenSigned`はプリミティブ型のままとした。

契約金額`amount`は`number`ではなく、クラス`Money`を使う。クラス`Money`について本ドメインモデルのコアではないが、PoEAAではかなりの分量を用いて詳述されている。ファクトリ`Money.dollars()`を使い、コンストラクタは使用していない。

ソフトウェアプロダクト`product`は、クラス`Product`を使う。ファクトリ`Product.factoryProduct()`を使う。後述するが収益認識の計算方法もファクトリで定義される。

収益認識`revenueRecognitions`も、クラス`RevenueRecognition`が使われる。

```typescript: 変更後
export class ContractRepository {
  /*...*/
  public async read(contractID: number): Promise<Contract> {
    const dataset = await SELECT.from(this.srv.entities.Contracts /* ... */
    return new Contract(
      dataset[0].ID,
      dataset[0].whenSigned,
      Money.dollars(dataset[0].amount),
      Product.factoryProduct(dataset[0].product.type),
      dataset[0].revenueRecognitions.map(
        (r: any) =>
          new RevenueRecognition(new Money(r.amount, Currency.USD()), r.date),
      ),
    );
  }
```
#### ビジネスロジックの配置

テーブルモジュールでビジネスロジックは、コード行数の多い長い関数となっていた。これがどうなったかを見る。

#### ビジネスロジックメソッドのキー引数はなくなる

テーブルモジュールの場合には、引数にキーを取り、データセットからそのキーのレコードを検索し、レコード中身もメソッドの冒頭にて取得していた。

```typescript: 変更前
export class Contracts {
  public calculateRecognitions(contractID: number): void {
    this._index = this._dataset.findIndex(
      (contract) => contract.ID == contractID,
    );
    const contractRow = this._dataset[this._index]; 
    /*...*/
  }
}
```

ドメインモデルの場合には、オブジェクト生成時にオブジェクトが知った状態になっているので、引数にキーは不要。`Product`別に収益認識の計算を行うので、収益認識の計算は`Product`にフォワード。

```typescript: 変更後
class Contract {
  /* ... */
  public calculateRecognitions(): void {
    let rr = this.product.calculateRecognitions(this);
    this._revenueRecognitions = [...rr];
  }
}
```

#### ガード節：ファクトリに移動

テーブルモジュールの場合には、ガードが必要な要素は検索されたレコードに対して行っていた。

```typescript: 変更前
    if (contractRow === undefined) return;
    if (contractRow.product === undefined) return;
    if (contractRow.amount === undefined) return;
```

ドメインモデルの場合には、各オブジェクトのファクトリで行っている。ファクトリで行われるということから、重要ではあるもののコアではない。このような知識はビジネスルールから除外される。ガード節は汎用サブドメインといえると思う。

```typescript: 変更後

class Money {
  /*..*/
  public static dollars(amount: number): Money {
    assert(amount >= 0, "Amount must be greater than or equal to 0");
    return new Money(amount, Currency.USD());
  }
  /*..*/
}

class Product {
  /*...*/
  public static factoryProduct(type: "WP" | "SS" | undefined): Product {
    return type === "WP"
      ? Product.factoryWordProcessor()
      : type === "SS"
        ? Product.factorySpreadsheet()
        : Product.factoryUndefinedProduct();
  }
  /*...*/
}
```

#### 条件分岐：アルゴリズムの分岐はストラテジーパターンに

テーブルモジュールパターンの場合には、収益認識計算処理を区別するため、条件分岐していた。条件分岐に `if` を使っていた。

```typescript: 変更前
    if (contractRow.product.type === "WP") {
      /*...*/
    }
    if (contractRow.product.type === "SS") {
      /*...*/
    }
```

ドメインモデルの場合には、ストラテジーパターンを使って収益認識計算アルゴリズムを切り替える。インタフェース`RecognitionsStrategy`を、ふたつのクラス`CompletedRecognitionsStrategy`と`ThreeWayRecognitionsStrategy`が実装している。

`Product`クラスは`RecognitionStrategy`をコンストラクタ引数として与えられている。レポジトリから`Product`を生成するファクトリを呼び出すが、`type`がワードプロセッサの場合には`CompletedRecognitionsStrategy`をコンストラクタ引数にわたし、スプレッドシートの時には`ThreeWayRecognitionsStrategy`をコンストラクタ引数に渡す。

```typescript: 変更後
interface RecognitionsStrategy {
  calculateRecognitions(contract: Contract): RevenueRecognition[];
}

class CompletedRecognitionsStrategy implements RecognitionsStrategy {
  /*...*/
}

class ThreeWayRecognitionsStrategy implements RecognitionsStrategy {
  /*...*/
}
class Product {
  /* ... */
  public static factoryWordProcessor(): Product {
    return new Product( 1, "Word Processor", "WP", new CompletedRecognitionsStrategy() );
  }
  public static factorySpreadsheet(): Product {
    return new Product( 2, "Spreadsheet",    "SS", new ThreeWayRecognitionsStrategy(30, 60) );
  }
  /* ... */
}
```

#### ありがちで煩雑な計算処理：再利用可能なクラスに配置

テーブルモジュールパターンの場合には、ありがちで煩雑な計算処理はプライベートメソッドに切り出していた。

```typescript: 変更前
export class Contracts {
  /* ... */
  public calculateRecognitions(contractID: number): void {
    /* ... */
    const alloc_amount = this.allocate_amount(amount, 3);
    /* ... */
  }
  /* ... */
  private allocate_amount(amount: number, by: number): number[] {
    const lowResult = Math.floor((amount / by) * 100) / 100;
    const highResult = lowResult + 0.01;
    const result = [];
    let remainder = (amount * 100) % by;
    for (let i = 0; i < by; i++) {
      result.push(i < remainder ? highResult : lowResult);
    }
    return result;
  }
  /* ... */
}
```

ドメインモデルの場合には、`Money`クラスに切り出している。再利用可能な場所に置くという発想。実装側は単に置いた場所を変えるだけの変更だが、呼び出す側は再利用性が高まっている。リファクタリングによって別のクラスにこのロジックを移動したとしても同じく`allocate()`が使える。わかりやすいところでは、テストでも直接`allocate()`が使えるようになった。

```typescript: 変更後
class ThreeWayRecognitionsStrategy implements RecognitionsStrategy {
  /* ... */
  calculateRecognitions(contract: Contract): RevenueRecognition[] {
  /* ... */
    const alloc_amount = contract.amountMoney.allocate(3);
  /* ... */
  }
  /* ... */
}

class Money {
  /*...*/
  public allocate(by: number): Money[] {
    const lowResult = this.devide(by);
    const highResult = lowResult.add(
      new Money(1 / this.centFactor(), this.currency()),
    );
    const result = [];
    let remainder = this.amount().amount_internal % BigInt(by);
    for (let i = BigInt(0); i < BigInt(by); i++) {
      result.push(i < remainder ? highResult : lowResult);
    }
    return result;
  }
  /*...*/
}
```

#### 構造：複数のクラスの関連によりドメイン知識が表現される

テーブルモジュールの場合には、ドメインロジックはテーブルモジュールクラスですべてであった。データセットのためにデータ型を使っていたが、それ以上のドメイン知識はソースコードの内部を見ないとわからない。とはいえ、テーブルモジュールにすべてが書かれているので、そこまで複雑で無ければ負担にはならないと思う。

```plantuml
hide circle
class Contracts {
  + calculateRecognitions(contractID: number): void
}
class Product <<Type>>{}
class Contract <<Type>>{}
class RevenueRecognition <<Type>>{}
Contracts --> Product
Contracts --> Contract
Contracts --> RevenueRecognition
```

ドメインモデルの場合には、クラス図にもドメイン知識が表現されて面白さがある。ドメイン知識そのものが複雑であることに変わりはないので、クラス図がそれなりに複雑に見えた方が正しい。クラスの命名にドメインの言葉が使われるし、デザインパターンといった設計語彙の蓄積を利用することができる点が強力。

```plantuml
hide circle
class Product
interface RecognitionsStrategy{
  +calculateRecognitions()
}
class CompletedRecognitionsStrategy
class ThreeWayRecognitionsStrategy
class RevenueRecognition
class Contract

Contract o--> RevenueRecognition
Contract o--> Product
Product o--> RecognitionsStrategy
CompletedRecognitionsStrategy .up.|> RecognitionsStrategy
ThreeWayRecognitionsStrategy .up.|> RecognitionsStrategy
```



[^1]: https://qiita.com/skusunoki/items/f59944c3ae75501e3f86
[^4]: テーブルデータゲートウェイ( _Table Data Gateway_ )とローデータゲートウェイ(_Row Data Gateway_)を合わせてゲートウェイと呼ぶ。DAOと呼ばれることもある。
[^5]: https://www.amazon.co.jp/exec/obidos/ASIN/B07B44BPFB Domain Modeling Made Functional: Tackle Software Complexity with Domain-Driven Design and F# (English Edition)