こんにちは。エンジニアの藤井(@yutafujii)です。
社内向けのプロダクト「タレントエージェンシー支援システム(SFA/CRM)」のエンジニアをしています。
プロダクトはフロントエンドをNuxt/TypeScript・サーバーサイドをRailsで実装しているのですが、今回はフロントエンドのAPIリクエスト処理にRepositoryFactoryパターンを導入した話をさせていただきます。
RepositoryFactoryパターンとは
RepositoryFactoryとはAPIを呼び出す設計のデザインパターンとして、JorgeというVueエヴァンジェリストによって2018年に紹介されました。
(原文)Vue API calls in a smart way
https://medium.com/canariasjs/vue-api-calls-in-a-smart-way-8d521812c322
(日本語訳)【Vue.js】Web API通信のデザインパターン (個人的ベストプラクティス)
https://qiita.com/07JP27/items/0923cbe3b6435c19d761
Jorge氏のブログでは以下のような問いかけがされます。
How many times have you seen examples with an instance of axios in each component?
(各コンポーネントにaxiosインスタンスが書かれてるような実装をどれくらい見たことがありますか?)
そしてそのように実装されているコードに対してJorge氏は問題提起しています。
What happens if the endpoint changes?
(エンドポイント変更したらどうする?)
How I can handle mocks or different endpoints to test it?
(動作確認のためにエンドポイントをモックしたくなったらどうする?)
What happens if you need to reuse a call?
(再利用したくなったらどうする?)
What happens if you need to refactor some call or move it to a Vuex actions?
(Vuexに処理を移植するとかリファクタするとなったら?)
後述しますが、最後の”リファクタリングしたくなった”のがまさに私たちの陥った状況でした。
この問題に対処するために考えられたのがRepositoryFactoryパターンのようです。
これは名前の通りRepositoryパターンとFactoryパターンを組み合わせた設計ということになります。Repositoryパターンはドメイン駆動設計(Domain-Driven Design, DDD)で提唱された考え方、Factoryパターンはオブジェクト指向言語のCreational Design Patternsの一つです。
Repositoryパターンは、ドメインモデルのまとまり(Aggregateと呼ばれます)ごとにデータアクセスを1箇所に集約するRepositoryを作成し、データレイヤのロジックとドメインを疎結合にするというもの、Factoryパターンとはインスタンスの生成ロジックを一元管理するFactoryを作成し、インスタンスを必要とするクライアントから生成ロジックを分離するものです。
雑な言い方をすれば、両者を組み合わせることで以下のようなメリットを享受できるということになります。
- コードのメンテナンス性が向上(DRYに記述できる)
- 拡張性が向上(横展開するときに短時間で実装ができる)
導入背景
さて、弊社では2019年ころからモノリシックなRailsアプリケーションをフロントエンドとバックエンドに分離してきています。フロントエンドはVue/Nuxt/TypeScriptを採用していますが、詳細は下記ブログに記載しています。
tech.forstartups.com
ゼロから作ってきたばかりということもあり、APIへのリクエストは各コンポーネント中からaxiosを利用していました。
しかし、実装量が増大するにつれてAPIへのリクエストで共通の修正を行う場合に該当箇所や対象ファイルも増大し、メンテナンスが難しくなりました。そして、あるタイミングで実際にaxiosの処理をrescueしたいという話になりました。
「axiosを書いた各コンポーネント全部の箇所に修正を入れていく方法は避けた方がよいので別の方法を考えましょう」と一緒に働くメンバーの方が色々調査してくれて、RepositoryFactoryパターンを採用することにしました。
実装する
まずRepositoryパターンを導入していきます。
ドメインごとにデータへアクセスする処理をそれぞれ1枚のRepositoryに集約し、axiosによるAPIリクエストはこのファイルから(このRepositoryを通して)のみ行われるようにします。
次にFactoryパターンを導入します。
今回のFactoryパターンにおける具体的な生成物はRepositoryです。
Factoryを記述するファイルを作成し、どのRepositoryを生成するかを(呼び出し元のクライアントではなく)Factoryが決定できるようにします。
最後にaxiosを直利用していたコンポーネントを修正します。
データに対するCRUDアクションを行うときは必ずRepositoryを通すのがRepositoryパターンです。従ってaxiosを直で利用せずにRepositoryを指定するということになりますが、直接Repositoryインスタンスを生成せずにFactoryに”生成依頼”するのがFactoryパターンなので、最終的にはコンポーネントはFactoryに対してRepositoryインスタンス生成の依頼を行うよう修正します。これが下の図の .$repository(‘user’) です。
これによって得られたRepositoryは共通のInterfaceが備わっているので、あとは取得したRepositoryへのメソッド呼び出しを行うよう書き換えます。
なお、実際にはFactoryを呼び出すにあたって事前にpluginでFactoryをNuxtAppにインジェクトしておきます(repositoryという名称をつけました)。こうすることで context.root.$repository でFactoryを呼ぶことができます。
import { Inject, NuxtApp } from '@nuxt/types/app'
import {
ApiRepositoryFactory,
RepositoriesType,
} from '@/factories/api-repository-factory'
export default ({ app }: { app: NuxtApp }, inject: Inject) => {
const repositories = (name: string) => {
return ApiRepositoryFactory.get(name)(app.$axios)
}
inject('repositories', repositories)
}
declare module 'vue/types/vue' {
interface Vue {
$repositories: RepositoriesType
}
}
また、コンポーネントにおけるRepositoryを通したCRUDアクションはcomposition APIを利用してcompositionとして切り出し、コンポーネントでは当該compositionをimportして使っています。
pages/index.vue
import useUsers from '~/composables/useUsers'
export default defineComponent({
components: { },
setup(_, context: SetupContext) {
const {
get,
post,
} = useUsers(context)
composables/useUsers.ts
import { computed, reactive, toRefs } from '@vue/composition-api'
export default (context: any) => {
const state = reactive<{
user: UserType
loading: boolean
}>({
user: {},
loading: true,
})
const get = async (userHash: string) => {
const response = await context.root
.$repositories('user')
.get()
},
return {
...toRefs(state),
get,
post,
}
}
実装の概要は以上ですが、RepositoryFactoryパターンをAPIリクエストに導入した全体像を記載しておきます。
実装してみて
実装を終えて数ヶ月が経ちますが、当初の目的であったAPIクライアントに関するエラーハンドリングを1箇所に集約して管理することができるようになり(そしてそれは再利用が可能)、設計を一度理解すればコード管理が行いやすくなりました。
フロントエンドにとってはBackend For Frontend(BFF)サーバー以降のAPIサーバー・各種データソースをデータレイヤと見做すことができますが、その意味でドメインモデルとデータレイヤの中間にドメインごとに(正確にはAggregateごとに)1種のRepositoryレイヤを設けたことのメンテナンス性の高さをチーム一同実感しています。
副次的な効果として、以前の実装では設計から落ちておりスクリプトエラーとして拾っていたAPIのエラーを全てリクエスト段階で捉えて監視ツールにロギングできたことで、バグ発見までの時間を短縮できたことがありました。
Factoryパターンに厳密に従っているわけではないものの、それでもDRYに書けた部分が多く、今後もこのパターンに沿ってAPIクライアント側の機能開発は拡張していきたいと思います。
参考文献
以下のブログは実装の際に参考にさせていただきました
Vue API calls in a smart way
https://medium.com/canariasjs/vue-api-calls-in-a-smart-way-8d521812c322
【Vue.js】Web API通信のデザインパターン (個人的ベストプラクティス)
https://qiita.com/07JP27/items/0923cbe3b6435c19d761
Repositoryパターン
https://medium.com/canariasjs/vue-api-calls-in-a-smart-way-8d521812c322
Factoryパターン
https://www.oodesign.com/factory-pattern.html