TypeScript を使って Alexa Custom Skills を作ろう (5) 実装 - 発話

# 前回

[前回](https://qiita.com/daisukeArk/items/53a45115d1530cb8782f)の投稿では、ハンドラにあるインテントの外部モジュール化を行いました。
いよいよ、実装編は最後になります。残りは発話についてです。

# はじめに

Alexa スキルは日本以外の他の国でも提供できるように、スキル毎に複数の言語が選択できるようになっています。
`alexa-skills-kit-sdk-for-nodejs`にはローカライズする機能が`i18next`で[実装](https://github.com/alexa/alexa-skills-kit-sdk-for-nodejs/blob/master/lib/alexa.js#L240-L259)されています。

将来的に多言語対応することを見据えて、本投稿でもこの機能を利用する前提で進めていきます。（サンプルスキルでは既に日本語だけ利用しています。）

さて、今回の投稿では、発話を外部モジュール化していきます。

# さいごのじっそうだ

`index.ts`と`first-handler.ts`で実装されている発話部分を外部モジュール化します。

```typescript:index.ts（抜粋）
const languageStrings = {
  'ja-JP': {
    'translation': {
      // ~~~省略~~~
    }
  }
};
```

```typescript:first-handler.ts（抜粋）
'FirstIntent': function (this: Alexa.Handler<any>) {
  var sayNumber = this.event.request.intent.slots.number.value;

  if (!sayNumber || !newsContents.hasOwnProperty(sayNumber)) {
    this.emitWithState('Unhandled');
  }

  getNewsAsync(sayNumber)
    .then((content) => {
      this.emit(':ask', this.t('ASK_ANSWER_NUMBER', content), ｔhis.t('ASK_ANSWER_NUMBER_REPROMPT'));
    })
    .catch((error) => {
      this.emitWithState("Unhandled");
    });
},
```

## まずは、発話ディレクトリをつくろう

```bash:移動
$ cd ~/custom-skill-sample-to-convert/skill/lambda/custom/src
```

```bash:utterancesディレクトリ作成
$ mkdir -p utterances
```

## i18nextに食わせるローカライズデータを外出しする

`index.ts`にそのまま置き去りにしていたローカライズデータ(`languageStrings`変数）を外出ししてあげましょう。

```bash:ローカライズファイル作成（i18nextに食わせるもの）
$ touch ./utterances/language-strings.ts
```

```typescript:utterances/language-strings.ts
export const languageStrings = {
  'ja-JP': {
    'translation': {
      'TELL_WELCOME_MESSAGE': 'タイろうへようこそ。',
      'ASK_HELP_MESSAGE': '声を聞かせて、と言ってください。',
      'ASK_START_MODE': 'ファーストモードに変更します。数字を言ってください。',
      'ASK_START_MODE_REPROMPT': '数字を言ってください。',
      'ASK_UNHANDLED_MESSAGE': 'すみません、よく聞きとれませんでした。もう一回言ってください。',
      'TELL_GOOD_BYE': 'さようなら',
      'TELL_UNHANDLED_MESSAGE': 'すみません、よく聞きとれませんでした。',
      'ASK_ANSWER_NUMBER': '%s。他に聞きたい数字はありますか？',
      'ASK_ANSWER_NUMBER_REPROMPT': '数字を言ってください。',
      'TELL_HELLO_WORLD': 'こんにちは、世界',
      'ASK_ANYTHING_ELSE': '他になにかありますか？',
    }
  }
};
```

これはもう、カット＆ペーストしてエクスポートするだけなので単純ですね。
インポートも忘れずに。

```typescript:index.ts
import { languageStrings } from './utterances/language-strings';
```

## 発話クラスの作成

発話クラスでは、`FirstIntent`の以下の部分で必要な`最初の発話`と`追加の発話`を返す機能を実装します。

```typescript:first-handler.ts（抜粋）
getNewsAsync(sayNumber)
  .then((content) => {
    this.emit(':ask', this.t('ASK_ANSWER_NUMBER', content), this.t('ASK_ANSWER_NUMBER_REPROMPT'));
  })
  .catch((error) => {
    this.emitWithState("Unhandled");
  });
```

### 準備

まずは、ファイルを準備しましょう。
発話クラスもインテント同様、コンテキストに依存する為基底クラスを作成します。

```bash:発話基底クラスの作成
$ touch ./utterances/utterance-base.ts
```

```typescript:発話基底クラス
import * as Alexa from 'alexa-sdk';

/**
 * 発話基底クラス
 */
export abstract class UtteranceBase {
  /**
   * プロパティ - Alexaハンドラコンテキスト
   */
  protected alexaContext: Alexa.Handler<any>;

  /**
   * コンストラクタ
   * @param context Alexaハンドラコンテキスト
   */
  constructor(context: Alexa.Handler<any>) {
    this.alexaContext = context;
  }
}
```

では、これを基に発話クラスを実装していきましょう。
まずは、形だけ作ります。

```bash:発話クラスの作成
$ touch ./utterances/first-utterance.ts
```

```typescript:utterances/first-utterance.ts
import * as Alexa from 'alexa-sdk';
import { UtteranceBase } from './utterance-base';

/**
 * ファースト発話クラス
 */
export class FirstUtterance extends UtteranceBase {
  /**
   * コンストラクタ
   * @param context Alexaハンドラコンテキスト
   */
  constructor(context: Alexa.Handler<any>) {
    super(context);
  }

  /**
   * 発話内容取得
   */
  public respond() {

  }
}
```

### 発話クラスの`respond`メソッドを実装する

準備が終わったので、`respond`メソッドを実装していきます。
せっかく、TypeScriptを使うのでインタフェースも使っていきましょう。

`respond`メソッドで`最初の発話`と`追加の発話`を返す為には、ユーザから発話された数字が必要になります。
ここでは、２つのインタフェースが必要になります。

* 発話内容を得る為の条件（`ユーザが発話した数字`）
* 発話内容（`最初の発話`と`追加の発話`）

これらをインタフェースで定義します。

```bash:conditionsディレクトリ作成
$ mkdir -p conditions
```

```bash:modelsディレクトリ作成
$ mkdir -p models
```

```bash:条件インタフェースの作成
$ touch ./conditions/first-utterance-condition.ts
```

```bash:発話内容インタフェースの作成
$ touch ./models/first-utterance-result.ts
```

```typescript:models/first-utterance-condition.ts
/**
 * 発話条件 インタフェース
 */
export interface IFirstUtteranceCondition {
  /**
   * 数字
   */
  sayNumber: string,
}
```

```typescript:models/first-utterance-result.ts
/**
 * 発話結果 インタフェース
 */
export interface IFirstUtteranceResult {
  /**
   * 初回の発話
   */
  speech: string;

  /**
   * 追加の発話
   */
  repromptSpeech: string;
}
```

これを加えると、FirstUtteranceクラスはこんな感じになります。

```typescript:utterances/first-utterance.ts
import * as Alexa from 'alexa-sdk';
import { IFirstUtteranceCondition } from '../conditions/first-utterance-condition';
import { IFirstUtteranceResult } from '../models/first-utterance-result';
import { UtteranceBase } from './utterance-base';

/**
 * ファースト発話クラス
 */
export class FirstUtterance extends UtteranceBase {
  /**
   * コンストラクタ
   * @param context Alexaハンドラコンテキスト
   */
  constructor(context: Alexa.Handler<any>) {
    super(context);
  }

  /**
   * 発話内容取得
   * @param condition 条件
   */
  public respond(condition: IFirstUtteranceCondition): IFirstUtteranceResult {
    const result: IFirstUtteranceResult = {
      speech: '',
      repromptSpeech: ''
    };

    return result;
  }
}
```

あとは、`result`変数に値をセットするだけですね。

### `getNewsAsync`メソッドの外部モジュール化

これもTypeScriptを使うので、async, awaitを使った実装に変えていきましょう。
`News`となってるが、ニュースを返すものではないですが。。。

```bash:ニュースリポジトリの作成
$ touch ./models/news-repository.ts
```

```typescript:models/news-repository.ts
/**
 * ニュースリポジトリクラス
 */
export class NewsRepository {
  /**
   * コンストラクタ
   */
  constructor() {
    // 処理なし
  }

  /**
   * 内容取得
   * @param sayNumber 数字
   * @returns 内容
   */
  public getAsync(sayNumber: string): Promise<string> {
    // データ（便宜上このクラスに定義しているが、本来はデータベースからエンティティを返す想定）
    let newsContents: {[key: string]: string } = {
      '1': '1番です',
      '2': '2番です',
      '3': '3番です',
    };

    return new Promise((resolve, reject) => {
      if (newsContents[sayNumber]) {
        resolve(newsContents[sayNumber]);
      } else {
        reject();
      }
    });
  }
}
```

大きく処理は変わっていませんが、内容が取得できなかった場合は`reject`を呼び出しています。
では、これを発話クラスに盛り込みましょう。

### 発話クラスでasync, awaitを使う

```typescript:utterances/first-utterance.ts
import * as Alexa from 'alexa-sdk';
import { IFirstUtteranceCondition } from '../conditions/first-utterance-condition';
import { IFirstUtteranceResult } from '../models/first-utterance-result';
import { NewsRepository } from '../models/news-repository';
import { UtteranceBase } from './utterance-base';

/**
 * ファースト発話クラス
 */
export class FirstUtterance extends UtteranceBase {
  /**
   * リポジトリ
   */
  private repository: NewsRepository;

  /**
   * コンストラクタ
   * @param context Alexaハンドラコンテキスト
   */
  constructor(context: Alexa.Handler<any>) {
    super(context);

    // リポジトリ作成
    this.repository = new NewsRepository();
  }

  /**
   * 発話内容取得
   * @param condition 条件
   * @returns 結果
   */
  public async respond(condition: IFirstUtteranceCondition): Promise<IFirstUtteranceResult> {
    const result: IFirstUtteranceResult = {
      speech: '',
      repromptSpeech: ''
    };

    // データ取得（非同期）
    const speech = await this.repository.getAsync(condition.sayNumber);

    // 結果をセット
    result.speech = this.alexaContext.t('ASK_ANSWER_NUMBER', speech);
    result.repromptSpeech = this.alexaContext.t('ASK_ANSWER_NUMBER_REPROMPT');

    return result;
  }
}
```

ここで１つ問題が出ました。。。
`this.alexaContext.t`の戻り値がvoidになってるんですよね。。。
結果をセットできない。。。
ここの戻り値は`string`であるべきだと思ってるんですが、どうなんでしょうか。

[types/alexa-sdk](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/alexa-sdk/index.d.ts#L122)

仕方がないので、インタフェースの`speech`と`repromptSpeech`の型を`string`から`any`にすることで対応しました。
発話クラスはこれで出来上がり！
あとは、ハンドラの呼出部分を変更しましょう。

### 発話クラスをハンドラから呼ぶ

まずは使う為に、インポートです。

```typescript:handler/first-handler.ts
import { IFirstUtteranceCondition } from '../conditions/first-utterance-condition';
import { IFirstUtteranceResult } from '../models/first-utterance-result';
import { FirstUtterance } from '../utterances/first-utterance';
```

次に`FirstIntent`の呼出部分です。
非同期呼出メソッドでの戻り値の型は`Promise`なので、`async`をつけてます。

発話クラスから取得した結果を`this.emit`の第２、３引数にセットして終わりです。

```typescript:handler/first-handler.ts
'FirstIntent': async function (this: Alexa.Handler<any>) {
  var sayNumber = this.event.request.intent.slots.number.value;

  // 発話取得条件設定
  const condition: IFirstUtteranceCondition = {
    sayNumber: sayNumber
  };

  try {
    // 発話内容取得
    const result = await (new FirstUtterance(this)).respond(condition);

    this.emit(':ask', result.speech, result.repromptSpeech);
  } catch (error) {
    this.emitWithState("Unhandled");
  }
},
```

# まとめ

この投稿では、発話に関して外部モジュール化を行いました。

実装編は以上になります。いかがでしたでしょうか？
[設計](https://qiita.com/daisukeArk/items/c22f7f4e4f231d05dd51)に合わせ分類して実装を行いました。

TypeScriptの使い方、私もまだまだ勉強中ですが、これから始める人の参考になれば幸いです。
細かい説明が無いので分かり辛いかもしれませんが。。。

今回の投稿のソースは以下にあります。
[発話外部モジュール化](https://github.com/daisukeArk/custom-skill-sample-to-convert/tree/dev/export-utterances)

次回は、いよいよ最後。[単体テスト](https://qiita.com/daisukeArk/items/afa12708810a6b0b0fde)です。