今回は、NuxtJSを利用して作成しているアプリケーションにRepository Factoryパターンを適用するきっかけや実際にどのように適用したのかをNuxtJSの機能などと合わせて紹介したいと思います。

必要な基礎知識

この記事を理解する上であると良い知識は、3つあります。必要に応じて補足しますが、事前に理解されていると読みやすいと思います。

  • ソフトウェアデザインパターンのRepositoryパターン
  • NuxtJSの基礎知識
  • Typescript(Javascript)の基礎知識

きっかけ

適用をしようと思ったきっかけは、3つあります。

  • Axiosライブラリで行っている処理を共通化する
  • 環境を分けて開発を行いたい。
  • 単体テスト

NuxtJSのInjectについて

Injectについては、NuxtJSの公式サイトで次のように説明されています。

Sometimes you want to make functions or values available across your app. You can inject those variables into Vue instances (client side), the context (server side) and even in the Vuex store. It is a convention to prefix those functions with a $.

この inject を使うことで、Nuxt.jsのアプリケーション(server side, client side)で、いろいろな場所から共通で利用したい関数や値を(グローバル変数にすることなく)$をつけて呼び出すことができます。 依存関連の話と深い関係があるのは、「Dependency Injection(依存性の注入)」です。Dependency Injectionについては、色々良いがありますので、そちらを参照して見てください。

Axiosライブラリで行っている処理を共通化する

Axiosライブラリには、拡張機能があります。この拡張機能を使って、エラーハンドリング、リクエスト、レスポンスなどのデバッグログを共通化できるので、この機能を取り入れることを行いました。イベントについては、下記のように定義されています。こちらの内容は、axios公式のextendで紹介されている方法です。

イベント説明
onRequestaxiosのリクエストが発生した時に発生するイベント
onResponse通信相手からのレスポンスを受信した時に発生するイベント
onRequestErrorリクエスト時エラーが発生した時のイベント
onResponseErrorレンポンスがエラーの時に発生するイベント
onErrorエラーが発生した時に発生するイベント
// nuxt.config.ts
{
  modules: [
    '@nuxtjs/axios',
  ],

  plugins: [
    '~/plugins/axios'
  ]
}

下記は、プラグインの実装方法です。このプラグインでは、エラーハンドリングを共通にすること、デバッグ目的で表示するリクエスト、レスポンスの表示などを行う例です。

import { Plugin } from '@nuxt/types'
import { AxiosError, AxiosResponse, AxiosRequestConfig } from 'axios'

const axisoIntercector: Plugin = ({ $axios, redirect }) => {
  $axios.onRequest((config: AxiosRequestConfig) => {
    if (!window.navigator.onLine) {
      console.log('現在オフライン')
    }
    // リクエスト前に呼び出されるコード
    console.log('Making request to ' + config.url)
  })

  $axios.onRequestError((error: AxiosError) => {
    console.log(error)
  })

  $axios.onResponse((response: AxiosResponse) => {
    // 成功レスポンスを受け取った時に呼び出されるコード
    console.log(response)
  })

  $axios.onError((error: AxiosError) => {
    // console.log(error)

    if (!error.response) {
      // エラーレスポンスがない、net::ERR_CONNECTION_REFUSEDなどは、ここで処理する
      redirect('/500')
    }

    if (error.response!.status === 401) {
      // 認証エラーは、ログイン画面へリダイレクトする
      redirect('/Signin')
    }
    if (error.response!.status === 500) {
      // サーバ内部エラーの場合は、ソーリページへ移動する
      redirect('/500')
    }
  })
}
export default axisoIntercector

適用前のコンポーネントのコード例

適用前のコンポーネントでは、下記のようにエラーをハンドルしている部分があるかと思いますが、エラーの扱いとしては、コンポーネント固有のもの、共通的に扱わなければならないものなど複数ありますが、401400500など共通の振る舞いも混ざっていて、読みにくいコードになっています。

try {
    // ユーザ情報を取得する
    const response = await axios.get('/user')
    console.log(response);
} catch (error) {
    // Error
    if (error.response) {
        // ステータスコードによって、エラーをハンドリングする。
        if (error.response.status == '401') {
            // ログイン画面へリダイレクト
        }
        if (error.response.status == '500') {
            // ソーリページへリダイレクト
        }
        ・・・
        // このコンポーネントで必要なエラーを変換して表示する
       this.error ="エラーメッセージ"
    } else {
        // Something happened in setting up the request and triggered an Error
        console.log('Error', error.message);
    }
    console.log(error);
}

適用後のコンポーネントのコード例

下記のように401400500など共通の振る舞いをaxiosが提供する機能を利用することで実装を整理します。axiosのインスタンスをthis.$axiosで取得する変更と共通的に扱うエラーがなくなりスッキリしています。

try {
    // ユーザ情報を取得する
    const response = await this.$axios.get('/user')
    console.log(response);
} catch (error) {
    // Error
    if (error.response) {
        ・・・
        // このコンポーネントで必要なエラーを変換して表示する
       this.error ="エラーメッセージ"
    } else {
        // Something happened in setting up the request and triggered an Error
        console.log('Error', error.message)
    }
    console.log(error);
}

onError、try-catchのcatch句でのコール順序は、onError()->catchの順です。onErrorが呼ばれてもcatch句は処理されます。

ここまでは、axiosの処理の共通化になります。$axiosについては、下記のようにContextにインジェクションされているので、”NuxtJSのコンテキストアクセス”ができる箇所からは、this.$axiosという形でアクセスできます。

// javascriptコード
export default function ({ $axios }, inject) {
  // Create a custom axios instance
  const api = $axios.create({
    headers: {
      common: {
        Accept: 'text/plain, */*'
      }
    }
  })

  // Set baseURL to something different
  api.setBaseURL('https://my_api.com')

  // Inject to context as $api
  inject('api', api)
}

ここまでaxiosの共通化についての説明は終了です。これで、injectを通してエラーハンドリングを追加したaxiosは、nuxtアプリケーション内では、this.$axiosで利用できるようになっています。nuxtアプリケーションとは関係しないようなプレーンなTypescript/Javascriptでは、thisコンテキストによるアクセスができないので、constructor等で渡す必要があります。

RepositoryFactoryパターンについて

コンポーネント、Vuexからaxiosにアクセスしている処理は、$axiosという形でアクセスすることができるようになりました。

リポジトリパターンとは

DDD(ドメイン駆動設計)等でよく理解されている方もいるかと思いますので、ここはさらっとポイントだけあげたいと思います。詳しく知りたい方は、MSが記載している記事を読んでみることをおすすめします。

リポジトリは、データ ソースへのアクセスに必要なロジックをカプセル化するクラスまたはコンポーネントです。 リポジトリは一般的なデータ アクセス機能を一元管理して保守性を向上させ、ドメイン モデル レイヤーからデータベースにアクセスするためのインフラストラクチャやテクノロジを切り離します

この記載を見ると今回のようなaxiosによる外部サービス、データベースなどからデータを取得したり、変更したりするロジックをリポジトリという形で捉えるのが良いということです。 先程のユーザ情報を取得するという例をリポジトリという形で取り出してみると下記のようになります。

import { NuxtAxiosInstance } from '@nuxtjs/axios'

/*
 * ユーザ情報
 */
export interface UserInfo {
  id?: string,
  email?: string,
  lastName?: string,
  firstName?: string,

}

export interface UserInterface {
  get: (token: string, userId: string) => Promise
}

export class UserRepository implements UserInterface {
  private readonly axios: NuxtAxiosInstance
  constructor ($axios: NuxtAxiosInstance) {
    this.axios = $axios
  }

  createResource () {
    const userUrl = process.env.USER_URL
    return userUrl
  }

  /*
   * ユーザ情報を取得する
   */
  async get (token: string, userId: string): Promise {
    const userUrl = this.createResource() + '/' + userId
    const res = await this.axios.get(userUrl!, {
      headers: {
        Authorization: token
      }
    })
    const userData = res.data
    const userInfo:UserInfo = {
      id: userData.uid,
      email: userData.email,
      lastName: userData.last_name,
      firstName: userData.first_name
    }
    return userInfo
  }
}

上記のUserRepositoryをコンポーネントやstoreから利用する場合には、下記のようになります。

// importが必要。
import { UserRepository } from 'src/userRepository'

// ユーザを取得する.
const userID = '12345'
const user = new UserRepository(this.$axios).get(userID)

この実装は、利用者がUserRepositoryの実装に依存する形になるので、テストや環境ごとに振る舞いを切り替えることが容易にしずらい状況にあります。この部分の依存を断ち切るには、NuxtJSのinjectを使って解決します。次のようなプラグインを作成します。

// plugins/repositories.ts
import { Plugin } from '@nuxt/types'
import { UserRepository } from 'src/userRepository'

export interface Repositories {
  user: UserRepository
}

const repositoriesFactory: Plugin = ({ $axios }, inject: (key: string, value: any) => void) => {
  // リポジトリ生成
  const user = new UserRepository($axios)

  const repositories: Repositories = {
    user
  }
  inject('repositories', repositories)
}
export default repositoriesFactory
// nuxt.config.ts
  /*
  ** Plugins to load before mounting the App
  */
  plugins: [
    { src: '@/plugins/axios' },
    { src: '@/plugins/repositories' }
  ],

Typescriptの型認識をさせるために以下の定義を追加します。

import { Repositories } from '@/plugins/repositories'

declare module 'vue/types/vue' {
  interface Vue {
    $accessor: typeof accessorType
    readonly $repositories: Repositories
  }
}

declare module 'vuex' {
  interface Store<S> {
    readonly $repositories: Repositories
  }
}

これで利用者側は、下記のようにインポート不要になりました。

// ユーザを取得する.
const userID = '12345'
const user = this.$repositories.user.get(userID)

これでコンポーネント側のテスト実施時には、this.$repositories.userUserInterfaceを満たすようなfakeObjectに差し替えてテストできるようになりました。

おわりに

今回は、axiosに依存するロジックを整理するということから、テスタビリティを考慮してrepostiroryFactoryパターンを適用してみました。コード量は増えますが、わかりやすいので、導入してみると良いと思います。ソフトウェアデザインパターンには、ドメイン駆動設計やクリーンアーキテクチャーなどがありますが、今回のリポジトリパターンやDIの考え方は、そこに通じるものがあります。また、別の機会にソフトウェアアーキテクチャーについて紹介したいと思います。最後まで読んでいただきありがとうございました。