VSCodeを使ったJavaScriptのモジュール抽出


背景

ブラウザ向けJavaScriptの開発でも、BrowserifyWebpackといったモジュールバンドルツールを作うことで、ファイル単位でモジュールを分割した開発が可能になりました。

すでにあるソースコードを、モジュールにわける操作は、機械的に実施できますが、手間です。
VSCodeのリファクタリング機能を使うことで、機械的な手間を軽減することができます。

方法

注意:今回はすでにファイル内にprivateなstatic関数がある状況を対象とします。
privateなstatic関数を抽出する方法は対象としません。

対象のサンプル

ファイル
以下のように、exportする関数とは別に、privateなstatic関数を含むモジュールがあります。
このprivateなstatic関数をモジュールとして抽出します。

index.js
export default function(annotationData, modeAccordingToButton) {
  return function(selectionModel) {
    const modifications = selectionModel.all().map((e) => annotationData.getModificationOf(e).map((m) => m.pred))

    updateModificationButton(modeAccordingToButton, 'Negation', modifications)
    updateModificationButton(modeAccordingToButton, 'Speculation', modifications)
  }
}

function updateModificationButton(modeAccordingToButton, specified, modificationsOfSelectedElement) {
  // All modification has specified modification if exits.
  modeAccordingToButton[specified.toLowerCase()]
    .value(doesAllModificaionHasSpecified(specified, modificationsOfSelectedElement))
}

function doesAllModificaionHasSpecified(specified, modificationsOfSelectedElement) {
  if (modificationsOfSelectedElement.length < 0) {
    return false
  }

  return modificationsOfSelectedElement.length === modificationsOfSelectedElement.filter((m) => m.includes(specified)).length
}

updateModificationButton関数と、doesAllModificaionHasSpecified関数をモジュールとして抽出していきます。

モジュール抽出

updateModificationButton関数

VSCodeでは、privateなstatic関数 をフォーカスすると、ヒントアイコンが表示されます。
ヒントアイコンをクリックすると新しいファイルへ移動しますと提案が表示されます。

これをクリックすると選択したprivateなstatic関数が別ファイルupdateModificationButton.jsに分かれます。

updateModificationButton.js
import { doesAllModificaionHasSpecified } from "./index";
export function updateModificationButton(modeAccordingToButton, specified, modificationsOfSelectedElement) {
  // All modification has specified modification if exits.
  modeAccordingToButton[specified.toLowerCase()]
    .value(doesAllModificaionHasSpecified(specified, modificationsOfSelectedElement));
}

選択したupdateModificationButton関数は、新しく作成されたupdateModificationButton.jsに移動されています。

index.js
import { updateModificationButton } from "./updateModificationButton";

export default function(annotationData, modeAccordingToButton) {
  return function(selectionModel) {
    const modifications = selectionModel.all().map((e) => annotationData.getModificationOf(e).map((m) => m.pred))

    updateModificationButton(modeAccordingToButton, 'Negation', modifications)
    updateModificationButton(modeAccordingToButton, 'Speculation', modifications)
  }
}

export function doesAllModificaionHasSpecified(specified, modificationsOfSelectedElement) {
  if (modificationsOfSelectedElement.length < 0) {
    return false
  }

  return modificationsOfSelectedElement.length === modificationsOfSelectedElement.filter((m) => m.includes(specified)).length
}

代わりに、index.jsにはimport { updateModificationButton } from "./updateModificationButton";の一文が追加されます。

相互import問題

気になる点はdoesAllModificaionHasSpecified関数の前にexportが追加されている点です。
updateModificationButton関数内でdoesAllModificaionHasSpecified関数を使っているため、新しく作ったupdateModificationButton.jsからdoesAllModificaionHasSpecifiedを参照する必要があるためです。

updateModificationButton.jsを確認するとimport { doesAllModificaionHasSpecified } from "./index";文があるのがわかります。
updateModificationButton.jsindex.jsで、お互いにimportしあっていて気持ち悪いです。

doesAllModificaionHasSpecified

気にせずdoesAllModificaionHasSpecified新しいファイルへ移動します

するとdoesAllModificaionHasSpecified関数はdoesAllModificaionHasSpecified.jsファイルにわかれます。
updateModificationButton.jsは次のように修正されます。

import { doesAllModificaionHasSpecified } from "./doesAllModificaionHasSpecified";
export function updateModificationButton(modeAccordingToButton, specified, modificationsOfSelectedElement) {
  // All modification has specified modification if exits.
  modeAccordingToButton[specified.toLowerCase()]
    .value(doesAllModificaionHasSpecified(specified, modificationsOfSelectedElement));
}

updateModificationButton.jsdoesAllModificaionHasSpecified.jsを参照し、index.jsへの参照はなくなりました。
一的に相互import問題は発生しますが、すべてのprivateなstatic関数をモジュールとして抽出すれば、自然に解消されます。

その他

一つの関数をexportするときは名前付きエクスポートより、デフォルトエクスポートの方が望ましいと考えています。
次のように修正します。

index.js
import updateModificationButton from "./updateModificationButton";

export default function(annotationData, modeAccordingToButton) {
  return function(selectionModel) {
    const modifications = selectionModel.all().map((e) => annotationData.getModificationOf(e).map((m) => m.pred))

    updateModificationButton(modeAccordingToButton, 'Negation', modifications)
    updateModificationButton(modeAccordingToButton, 'Speculation', modifications)
  }
}
updateModificationButton.js
import doesAllModificaionHasSpecified from "./doesAllModificaionHasSpecified";

export default function(modeAccordingToButton, specified, modificationsOfSelectedElement) {
  // All modification has specified modification if exits.
  modeAccordingToButton[specified.toLowerCase()]
    .value(doesAllModificaionHasSpecified(specified, modificationsOfSelectedElement));
}
doesAllModificaionHasSpecified.js
export default function(specified, modificationsOfSelectedElement) {
  if (modificationsOfSelectedElement.length < 0) {
    return false;
  }
  return modificationsOfSelectedElement.length === modificationsOfSelectedElement.filter((m) => m.includes(specified)).length;
}

まとめ

VSCodeのリファクタリング機能を使うことで、JavaScriptのファイル分割の手間が軽減できました。
これによってBrowserifyやWebpackをつかった、モジュール分割の恩恵を受けやすくなります。
小さな独立したファイルにわけることで、以下のような恩恵が受けられます。

  • ソースコードの影響範囲が明確になり、修正しやすくなる
  • 複数人で修正作業をした時に、衝突が起きる範囲が狭くなる

参考

JavaScript(ES2015) の export 方法は「名前付きエクスポート」 と 「デフォルトエクスポート」 の 2 種類あり、import する際の名前の付け方が異なる - dev_dubのブログ