カラーミーショップ ロリポップ!マネージドクラウド DI TypeScript

Node.jsのWebアプリケーションにDIと型安全性を取り入れた話

カラーミーショップ ロリポップ!マネージドクラウド DI TypeScript

こんにちは、@tascriptです。

2020年3月31日、カラーミーショップアプリストアにてカラーミーWPオプションをリリースしました。 カラーミーWPオプションは、ネットショップと同一ドメインにWordPressを配置することができるアプリケーションです。

ちなみに大まかな技術スタックは以下のとおりです

  • Nuxt.js
  • TypeScript
  • TypeORM
  • InversifyJS
  • Yarn

今日はカラーミーWPオプション開発時に実装したNode.jsのWebアプリケーションにDIと型安全性を取り入れたことで得られたメリットや知見についてお話します。

DI(Dependency Injection)の導入

カラーミーWPオプションでは、サーバーサイドの構築に際してInversifyJSを使用したDIを実施しました。

DIを導入した理由は以下のとおりです。

  • アプリケーションに体系的に責務を分割するような仕組みを取り入れたい
  • データベース操作部分をモジュール化し、ビジネスロジックとの責務を分けたい
  • モジュール化によりコードの再利用性を重視したい

今回は特に、データベース操作部分をモジュール化し、ビジネスロジックとの責務を分けたいという点に注力して開発を進めました。

例えば、ある実装のテストにおいてテーブル操作の結果をモックしたいのに、ビジネスロジックとデータベース操作が密結合になっていることでデータベースを用意してテストをしなくてはいけなかったり、データベース操作がモジュール化されていないことでデータベース操作のテストができないといったことが起きないようにしました。

今回の実装におけるアーキテクチャは

  1. Entity(テーブル定義)
  2. Module(データベース操作モジュール群)
  3. Service(ビジネスロジック)
  4. API(ルーティング)

の4つの層に分割して依存性の注入を1方向にしています。図解すると以下のようになります。

実装におけるアーキテクチャ

DI導入の結果として以下のようなメリットがありました。

  • データベース操作をビジネスロジックと分割したことで、データベース操作のテストが書きやすくなった
  • 責務単位でコードをモジュール化したことにより、モジュールごとのテストの有無が一目瞭然になった
  • ビジネスロジックのテストにデータベースの操作テストが含まれなくなった
  • API層のコードが短くなって可読性がよくなった
    • 例えばリクエストに含まれるパラメータのバリデーション以外はService層のメソッドを実行するのみでよくなった

今回ご紹介したアーキテクチャの詳細に関しては、こちらの資料も参考にしていただけますと幸いです。

TypeORMの導入

今回は型安全にWebアプリケーションを構築するためTypeScriptを使用しており、データベース操作も型安全にしたかったのでORMとしてTypeORMを採用しました。

結果として型安全なデータ操作が可能になり、子テーブルもテーブル定義通りの型が参照されるようになりました。 同時に以下のような知見を得ることもできました。

  • DataMapperパターンとDIの相性がよかった(上記資料に詳細を記載)
  • DeepPartialに注意

DeepPartialに注意については、saveメソッドなどの引数の型にDeepPartialと呼ばれる型が使用されています。DeepPartialを使用すると、ネストしたプロパティまでPartialになるような型が生成できます。

interface Pepabo {
  year: number
  month: number
  day: number
  partner: {
    name: string
    team: string
  }
}

// partnerプロパティを定義した場合、partnerの内部までPartialになります
type Presentation = DeepPartial<Pepabo>

saveメソッドでは引数として、Entity(テーブル定義)のDeepParitalが使用されるため、保存するパラメータの型を保証してくれません。 つまり、「このカラムをこのデータで保存したい」と思ってもsaveメソッドの引数で定義し忘れると意図した動作をしない場合があります。 こうなると、実行時にのみエラーが起きるという現象は静的解析を実施するTypeScriptを使用するメリットを損なってしまいます。

上記の解決方法として以下の対策を取りました。

独自の型を用意

Omitを使用することで特定のEntityのプロパティについては無視をするような型を作成しました。

type LimitEntity<T> = Omit<T, ('column1' | 'column2' | 'column3' | 'assosiation')>

さらにsaveメソッドのラッパー関数を用意することで、saveメソッドを型安全にすることができます。(書きながら思ったのですが、より厳密に型を制限するのであれば、propsの型はLimitEntity<EntityA>のみでよさそうです。)

import { getConnection } from 'typeorm'

public async save(props: LimitEntity<EntityA> | EntityA): Promise<(LimitEntity<EntityA> | EntityA) & EntityA> {
  const repositry = await getConnection().getRepository(EntityA)
  const data = await repositry.save(props)
  return data
}

以上のような対策を取り入れることで、意図しないデータ操作を無意識的に防ぐことができました。

余談ですが

今回ご紹介したカラーミーWPオプションですが、実はカラーミーショップチームとマネージドクラウドチームのコラボにより誕生したプロダクトなのです!Webアプリケーションの運用はマネージドクラウド上で実施しており、運用に関する記事も書いていたりするのでよろしければこちらもチェックしていただけますと幸いです。

まとめ

カラーミーWPオプションは比較的シンプルなアプリケーションですが、開発にはモダンな技術も取り入れつつアーキテクチャを考えたりなど、色々と試行錯誤しながら開発を進めました。結果として技術的な検証も取り入れつつ、自身のサービス上で運用するなど今後が楽しみなプロダクトになりました。今後ともカラーミーWPオプションならびにカラーミーショップ、ロリポップ!マネージドクラウドのサービスをよろしくお願いします!