TypeScript で DynamoDB のドメイン層を作ろう ファクトリ・ファサード実装編

# はじめに

本投稿では、[前回](https://qiita.com/daisukeArk/items/de9c92e6b650494bfb61)投稿した機能をアプリケーション層から利用しやすくするために、ファクトリ・ファサードクラスを作成します。

# ファクトリ・ファサード

これらはオブジェクト指向のデザインパターンの一種です。
ざっくりと私の理解で言うと…

## ファクトリ

インスタンスの作成を行う。

## ファサード

アプリケーション層にシンプルな入り口（メソッド）を提供する。

# 実装イメージ

サービスもリポジトリもカプセル化してしまって、アプリケーション層にはファサードで必要なサービスの機能のみを提供するようにします。

# 実装

ファサードを提供するにあたって、まずはファクトリクラスを作成していきます。
作成するファクトリクラスは以下です。

* DBコンテキスト(DynamoDB.DocumentClient)ファクトリ
* 各サービスファクトリ

## DBコンテキスト(DynamoDB.DocumentClient)ファクトリ

DynamoDBを操作する為のオブジェクトは、アプリケーション内で１つのインスタンスを提供するよう制限します。

```typescript:/src/lib/factories/db-context-factory.ts
import * as AWS from 'aws-sdk';

export class DbContextFactory {

  private static _instance: AWS.DynamoDB.DocumentClient;

  private constructor() {
  }

  public static get instance(): AWS.DynamoDB.DocumentClient {
    if (!this._instance) {
      this._instance = new AWS.DynamoDB.DocumentClient();
    }

    return this._instance;
  }
}
```

## 各サービスファクトリ

デザインパターンに沿ってファクトリを作成する場合は、インスタンス作成の抽象クラスを用意しそれらを継承してサービス毎に生成処理を具象クラスに実装していきます。
が、本投稿では１つのファクトリクラスで各サービスのインスタンス生成を提供するメソッドを実装していきます。

```typescript:src/lib/factories/service-factory.ts
import * as Repositories from '../repositories';
import * as Services from '../services';
import { DbContextFactory } from './db-context-factory';

export class ServiceFactory {
  private _customerService?: Services.CustomerService;
  private _userService?: Services.UserService;

  constructor() {
  }

  public get CustomerService(): Services.CustomerService {
    if (!this._customerService) {
      this._customerService = new Services.CustomerService(
        new Repositories.CustomerRepository(DbContextFactory.instance)
      );
    }

    return this._customerService;
  }

  public get UserService(): Services.UserService {
    if (!this._userService) {
      this._userService = new Services.UserService(
        new Repositories.UserRepository(DbContextFactory.instance)
      );
    }

    return this._userService;
  }
}
```

先程作成したDBコンテキストファクトリクラスからリポジトリの依存解決を行いサービスインスタンスを提供します。

では、ファサードの実装に入りましょう。

## 最初に考えてたコード

c#あがりなので、こんな感じでオーバロードできることを期待していたが。。。TypeScript(JavaScript)では出来ない

```typescript
  public async getAsync(condition: Conditions.CustomerCondition): Promise<Models.ICustomer | undefined> {
    return this.serviceFactory.CustomerService.getAsync(condition);
  }

  public async getAsync(condition: Conditions.UserCondition): Promise<Models.IUser | undefined> {
    return this.serviceFactory.UserService.getAsync(condition);
  }
```

TypeScriptでオーバロードする場合は、こんな感じか・・・？（**これは妄想です**）

```typescript
  public async getAsync(condition: Conditions.CustomerCondition): Promise<Models.ICustomer | undefined>;
  public async getAsync(condition: Conditions.UserCondition): Promise<Models.IUser | undefined>;
  public async getAsync(condition: ConditionBase): Promise<IEntityBase | undefined> {
    if (typeof condition === 'CustomerCondition') {
      return this.serviceFactory.CustomerService.getAsync(condition);
    } else if (typeof condition === 'UserCondition') {
      return this.serviceFactory.UserService.getAsync(condition);
    } else {
      throw new Error('対象テーブルに一致する処理が見つかりません');
    }
  }
```

```typescript
  public async getAsync<TEntity extends IEntityBase>(condition: ConditionBase): Promise<TEntity | undefined> {
    let entity: TEntity | undefined;

    if (typeof condition === 'CustomerCondition') {
      entity = await this.serviceFactory.CustomerService.getAsync(condition);
    } else if (typeof condition === 'UserCondition') {
      entity = await this.serviceFactory.UserService.getAsync(condition);
    } else {
      throw new Error('対象テーブルに一致する処理が見つかりません');
    }

    return entity;
  }
```

でも、オブジェクトはどこまでいってもオブジェクトなので出来なさそう。。。

なので入り口はこんな感じにしました。

```typescript
public async getAsync(condition: ConditionBase): Promise<IEntityBase | undefined> {}
```

これのデメリットは戻り値が基底クラスとなる為、呼び出し元でキャストが必要になることです。

## 呼び出すサービスの判定をどうするか

色々方法があるかと思いますが、今回はシンプルに操作条件クラスに抽象プロパティを用意することにしました。
各条件にテーブル名を持たせることで、振り分けを行います。

```typescript:/src/lib/conditions/condition-base.ts
import * as Enums from '../enums';

export abstract class ConditionBase {
  public abstract get tableName(): Enums.TableNames;
}
```

```typescript:/src/lib/enums/table-names.ts
export enum TableNames {
  Customers = 'customers',
  Users = 'users'
}
```

ユーザ操作条件クラス

```typescript:/src/lib/conditions/user-condition.ts
import * as Enums from '../enums';
import { ConditionBase } from './condition-base';

export class UserCondition extends ConditionBase {
  public get tableName() { return Enums.TableNames.Users; }
}
```

## ファサードを実装

条件クラスを元に`getAsync, queryAsync`メソッドをファサードクラスで提供します。

```typescript:/src/lib/facades/service-facade.ts
import * as Collections from '../collections';
import { CollectionBase } from '../collections/collection-base';
import * as Conditions from '../conditions';
import { ConditionBase } from '../conditions/condition-base';
import * as Enums from '../enums';
import { ServiceFactory } from '../factories/service-factory';
import * as Models from '../models';
import { IEntityBase } from '../models/entity-base';

/**
 * サービスファサード
 */
export class ServiceFacade {
  /**
   * インスタンス
   */
  private serviceFactory: ServiceFactory;

  /**
   * コンストラクタ
   */
  constructor(serviceFactory: ServiceFactory) {
    this.serviceFactory = serviceFactory;
  }

  /**
   * 選択
   * @param condition 条件
   * @returns エンティティ もしくは undefined
   */
  public async getAsync(condition: ConditionBase): Promise<IEntityBase | undefined> {
    let entity: IEntityBase | undefined;

    switch (condition.tableName) {
      case Enums.TableNames.Customers:
        entity = await this.serviceFactory.CustomerService.getAsync(condition);
        break;

      case Enums.TableNames.Users:
        entity = await this.serviceFactory.UserService.getAsync(condition);
        break;
      default:
        throw new Error('対象テーブルに一致する処理が見つかりません');
    }

    return entity;
  }

  /**
   * 複数選択
   * @param condition 条件
   * @returns コレクション
   */
  public async queryAsync(condition: ConditionBase): Promise<CollectionBase<IEntityBase>> {
    let collection: CollectionBase<IEntityBase>;

    switch (condition.tableName) {
      case Enums.TableNames.Customers:
        collection = await this.serviceFactory.CustomerService.queryAsync(condition);
        break;

      case Enums.TableNames.Users:
        collection = await this.serviceFactory.UserService.queryAsync(condition);
        break;
      default:
        throw new Error('対象テーブルに一致する処理が見つかりません');
    }

    return collection;
  }
}
```

# ファサードを使ってみる

ファサードを使う場合はこんな感じです。

```typescript
const facade = new ServiceFacade(new ServiceFactory());

// データ取得条件設定
const condition = new Conditions.UserCondition();
condition.getItemInput = {
  Key: {
    id: 1001,
    customerId: 1
  }
};

// 実行
const result = <Models.IUser>await facade.getAsync(condition);
```

毎回キャストしないといけないのがめんどくさい。。。

# まとめ

本投稿では、ファクトリ・ファサードクラスを作成しました。

アプリケーション層からはファサードのみを意識していれば良いので、非常に楽ですね！
ただ、キャストを毎回しなきゃいけないのがつらみなので、そこの改善を考えないといけないなぁ。。。

今回作成したソースは以下にあります。

[domain-layer-for-dynamodb-node - GitHub](https://github.com/daisukeArk/domain-layer-for-dynamodb-node)