エンジニアリング事業本部の@hiroki_hayashiです。
2019年7月から始まったsmartmat cloudのフルリプレイス完了から3ヶ月程度が経過しました。
大きな新規機能も無事リリースされ、細かい不具合改善や新規機能開発もスムーズに行えるようになりました。
リリース後に、私が担当する領域でフロントエンドにおいてAPI request周りに大きな変更を施したので
その際の当初の実装の失敗と改善点をご紹介できればと思います。
改修をした動機は以下の2点です。
従来の実装だと、APIのバージョン管理については行うことはなくenvにbaseURLにバージョンが含まれているといった状態でした。
env dev
https://dev-hoge.work/api/bff/v1
上記の実装の場合、一部のAPIだけがV2に差し変わるとなった場合どうしようもありません。
このどうしようもない状態がリリースわずか2ヶ月程度で発生してしまった、というのが今回の主な動機になります....
従来の実装では、各種component及びstoreからinjectされた$axiosを呼び出すことでapi requestを行なっていました。
そのためcomponentとロジックが密結合されてしまい、unitテストが非常に書きづらいものとなっていました。
上記の問題を解決するため、repository factory パターンを採用しました。
repositoryパターンはリソースへのアクセスを分離します。
また、それぞれのロジックで引数でAPI Versionを指定する方針を取っています。
// @/api/hogeRepository.ts
import { NuxtAxiosInstance } from '@nuxtjs/axios'
type queryData = {
q: string | null
}
export class HogeRepository {
private readonly axios: NuxtAxiosInstance
constructor($axios: NuxtAxiosInstance) {
this.axios = $axios
}
createResource(apiVersion: Number) {
return `v${apiVersion}/Hoge`
}
get(data: queryData, version = 1) {
const uri = `${this.createResource(version)}/search`
return this.axios.$get(uri, {
params: { ...data }
})
}
}
@/api下にrepositoryを実装します。
クラス定義のみを行い、以下のfactoryでインスタンス化します。
上述の通り、それぞれのメソッドでAPI Versionとrequest及びqueryを受け取ります。
このような実装にはもう一つの利点があります。
仮にAPIが未実装の場合、getメソッドでstaticなjsonを返すことで、アプリケーション側の実装をそのままにAPIつなぎこみが可能です。
// plugin/repository.ts
import { HogeRepository } from '@/api/hogeRepository'
export interface Repositories {
hoge: HogeRepository
}
export default function({ $axios }, inject) {
const hoge = new HogeRepository($axios)
const repositories: Repositories = {
hoge
}
inject('repositories', repositories)
}
pluginにて実際にインスタンス化した各種repositoryをinjectします。
これでstoreやcomponent側で利用できます。
しかしこれでは、エディタによる型補完が効かないので以下のような形でvueのinterfaceを拡張してあげます。
import { Repositories } from '@/plugins/repository'
declare module 'vue/types/vue' {
interface Vue {
readonly $repositories: Repositories
}
}
declare module 'vuex' {
interface Store<S> {
readonly $repositories: Repositories
}
}
最後に実際にアプリケーション側での実装を見てみます。
<script lang="ts">
// 一部省略
methods: {
async fetchData(ctx) {
const queryData = {
q: this.q,
}
await this.$repositories.hoge
.get(queryData, 2)
.then(({ hoges }) => {
this.hoges = hoges
})
.catch((err) => {
console.error(err)
})
}
}
単体テストを書く場合、今回定義した$repositories.hogeをmockにして注入するだけでテストが可能です。
今回のようなgetをテストをする際は、staticなjsonファイルをimportすることでテストを容易に行うことができます。
単体テストの方針などはまたの機会にでも。。。
今回紹介したrepository パターンでの実装は、個人的には今の所特段大きな不便は感じておらず、
次に何か新しいプロジェクトを実装する場合は、間違いなく採用しようと思っています。