36
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ワイ「ハスケル子ちゃん、ライブラリのラップって本当に意味あるの?」(やってみた)

Last updated at Posted at 2021-01-25

きっかけ

@Yametaro さんのワイ「なに!?ライブラリをラップするやと!?」という記事がきっかけです。
この中で、ハスケル子ちゃんが

ハスケル子「ラッパー関数を作って、それを使っていた場合は」
ハスケル子「1箇所だけの修正で済みます」

という発言をしています。
確かにその通りだと思いますが、その場面にあたったことがないので腑に落ちませんでした。
なので実践してみよう!というのがきっかけです。

どうやるか

問題はどうやるのかですが、HTTP通信を行うライブラリをラップして、使用するライブラリを切り替えるような実装にしました。

フレームワークはVueを使用し、jQueryからaxiosへと入れ替えていきます。

下準備

要件定義

現実的なプロジェクトの方がイメージをしやすいので、本を管理できるサイトという想定で進めていきます。
管理といっても本の名前を投稿するのみです。
また、エラーハンドリングについては今回は無視することとします。

API準備

json-serverを使用してローカルにモックAPIを作成します。

db.json
{
  "books": [
    {
      "id": 1,
      "title": "json-server"
    },
    {
      "id": 2,
      "title": "タイトル2"
    }
  ]
}

次のコマンドで起動しておきます。

コマンド
npx json-server --watch db.json

  \{^_^}/ hi!

  Loading db.json
  Done

  Resources
  http://localhost:3000/books

  Home
  http://localhost:3000

  Type s + enter at any time to create a snapshot of the database
  Watching...

リクエスト 説明
GET /books 本一覧の作成
POST /books 本の登録

本の登録に関しては/book/1の方がいいのですが、今回はREST APIの設計ではないので目をつぶることにします。

http://licalhost:3000/books にアクセスしてみて、jsonデータが取れたら準備OKです。

Vueの準備

vue-cliを使用してVueプロジェクトを作成します。
恥ずかしながらtypescriptを使用できないのでjavascriptで記述しています。
また、jQueryとaxiosもインストールしておきます。

npm i jquery
npm i axios

設計

イメージ

各モジュールをどのような構造にするのかイメージを膨らませます。

Untitled Diagram.png

各VueコンポーネントからAPIモジュールを呼び出します。このAPIモジュールは書籍APIのSDKの役割を担ってもらいます。
そして、APIモジュールはHTTPモジュールを呼び出します。
このHTTPモジュールが今回の課題であるライブラリをラップしているモジュールになります。
今回は実験なので、HTTPモジュールをjQuery用とaxios用にファイルを分割します。しかし、実際にはこのHTTPモジュールを編集してライブラリを切り替えるイメージです。

コンポーネント作成

Vueのコンポーネントについては今回本題ではないので解説は省略します。
完成した画像は以下の通りです。

image.png

jQueryでの実装

実装

ディレクトリ構造

/src
  /api
    /index.js
  /jquery
    /index.js

コード

jquery/index.js
import $ from "jquery"

/**
 * レスポンスデータの加工
 */
function processResponse(statusCode, body, headers) {
  return {
    statusCode,
    body,
    headers
  }
}

/**
 * ヘッダー情報をマップ型に加工
 * @param {*} raw_headers 未加工のjQueryヘッダー情報
 */
function processHeaderToMap(raw_headers) {
  var arr = raw_headers.trim().split(/[\r\n]+/);
  arr.reduce((accumulator, currentValue) => {
    var parts = currentValue.split(': ');
    var header = parts.shift();
    var value = parts.join(': ');
    accumulator[header] = value;
    return accumulator
  }, {})
}

/**
 * Http通信用クラス
 */
export default class {
  /**
   * Http通信を行うためのインスタンスを作成
   * APIが複数ある場合にも対応
   * @param {string} baseUrl ベースURL
   * @param {*} option オプションパラメーター
   * @param {string} option.dataType Content-typeにあたるデータタイプ
   * @param {string} option.headers デフォルトヘッダー情報
   */
  constructor(baseUrl, { dataType = "json", headers = {} }) {
    this.baseUrl = baseUrl
    this.dataType = dataType
    this.headers = headers
  }

  /**
   * getメソッドの呼び出し
   * @param {string} url URLパス
   * @param {object} query クエリデータ
   */
  get(url, query) {
    const baseUrl = this.baseUrl
    const dataType = this.dataType
    const headers = this.headers
    return new Promise(function (resolve, reject) {
      $.ajax({
        url: `${baseUrl}${url}`,
        type: 'get',
        data: query,
        dataType,
        headers,
      }).done(function (data, textStatus, jqXHR) {
        const headers = processHeaderToMap(jqXHR.getAllResponseHeaders());
        resolve(processResponse(textStatus, data, headers))
      }).fail(function (jqXHR, textStatus, errorThrown) {
        reject(errorThrown)
      })
    })
  }

  /**
   * postメソッド呼び出し
   * queryがないのはREST原則に則っているため
   * @param {string} url URLパス
   * @param {object} data リクエストボディ
   */
  post(url, data) {
    const baseUrl = this.baseUrl
    const dataType = this.dataType
    const headers = this.headers
    if (dataType == 'json') {
      data = JSON.stringify(data)
    }

    return new Promise(function (resolve, reject) {
      $.ajax({
        url: `${baseUrl}${url}`,
        type: 'post',
        data: data,
        dataType,
        headers,
      }).done(function (data, textStatus, jqXHR) {
        const headers = processHeaderToMap(jqXHR.getAllResponseHeaders());
        resolve(processResponse(textStatus, data, headers))
      }).fail(function (jqXHR, textStatus, errorThrown) {
        reject(errorThrown)
      })
    })
  }
}
src\api\index.js
import Http from "../jquery"

const headers = {
    "Content-Type": "application/json"
}
const instance = new Http("http://localhost:3000", { headers })

/**
 * 本一覧の取得
 */
export const getBooks = async function() {
    const res = await instance.get("/books")
    return res.body
}

/**
 * 本の登録
 */
export const postBook = async function(data) {
    const res = await instance.post("/books", data)
    return res.body
}

解説

使い方としては、自作jQueryモジュールをAPIが呼び出し、インスタンスを作成します。
インスタンス作成時にヘッダー情報などを登録して後程使う構成にしました。
理由としては、呼び出すAPIが一つとは限らないからです。
API(エンドポイント)ごとにインスタンスを作成することで汎用性を持たせています。

通信するときには作成したインスタンスから各メソッドを呼び出します。
レスポンスについてはprocessResponseメソッドを作成し、独自のレスポンスオブジェクトとして値を返すようにしました。

動作確認

2021-01-24_21h37_37.png

データもしっかり取得できています。登録側も問題ないです。

axiosでの実装

実装

本題です。jQueryからaxiosへモジュールを変えます。

src\axios\index.js
import axios from "axios"

/**
 * レスポンスデータの加工
 */
function processResponse(response) {
  return {
    "statusCode": response.statusCode,
    "body": response.data,
    "headers": response.headers
  }
}

/**
 * Http通信用クラス
 */
export default class Api {
  /**
   * Http通信を行うためのインスタンスを作成
   * APIが複数ある場合にも対応
   * @param {string} baseUrl ベースURL
   * @param {*} option オプションパラメーター
   * @param {string} option.dataType Content-typeにあたるデータタイプ
   * @param {string} option.headers デフォルトヘッダー情報
   */
  constructor(baseUrl) {
    this.baseUrl = baseUrl
    this.instance = axios.create({
      baseURL: baseUrl
    })
  }

  /**
   * getメソッドの呼び出し
   * @param {string} url URLパス
   * @param {object} query クエリデータ
   */
  async get(url, params) {
    const config = {
      params
    }
    const response = await this.instance.get(url, config)
    return processResponse(response)
  }

  /**
   * postメソッド呼び出し
   * queryがないのはREST原則に則っているため
   * @param {string} url URLパス
   * @param {object} data リクエストボディ
   */
  async post(url, data) {
    const response = await this.instance.post(url, data)
    return processResponse(response)
  }
}
src\api\index.js
import Http from "../axios"
// import Http from "../jquery"

const headers = {
    "Content-Type": "application/json"
}
const instance = new Http("http://localhost:3000", { headers })

/**
 * 本一覧の取得
 */
export const getBooks = async function() {
    const res = await instance.get("/books")
    return res.body
}

/**
 * 本の登録
 */
export const postBook = async function(data) {
    const res = await instance.post("/books", data)
    return res.body
}

解説

jQueryに比べてaxiosの方がコードがすっきりしました。
しかし、特記すべき点はそこではなく、src\api\index.jsのコードがほぼ変わっていないことです。
変更点としてはモジュールの呼び出し部分のみです。
独自のHTTPクラス、レスポンスを作成したことによりモジュールを変更しても呼び出し元のsrc\api\index.jsには影響を及ぼしていません。

動作確認

image.pngimage.png

この画像を見せても何が変わったんだ?といった感じですが、ちゃんと動いています。(信じてください)

まとめ・感想

このように、ライブラリをラップすると1か所(1ファイル)のみの修正でモジュールを変更することができました!
実際の現場で提案してもなかなか受け入れられることが少ないですが、実証できたおかげでより信ぴょう性が増したと思います。
また、ライブラリを置き換える可能性ってあるの?ということに関してですが、現状でjQueryがほかのライブラリに置き換わっているのを見ると、現在使用しているライブラリが将来的には置き換わる可能性は十分あるのではないのでしょうか。
そのときに、この設計でよかった!といえるような設計をしていきます。

謝辞

今回、この題名にて記事にする際、やめ太郎さんが嫌な思いをしないのか確認させていただきました。
突然のメッセージにも関わらず、記事の内容にも目を通していただきありがとうございます。
この場を借りて御礼申し上げます。

36
28
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
36
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?