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

# 背景

ブラウザ向けJavaScriptの開発でも、[Browserify](http://browserify.org/)や[Webpack](https://webpack.js.org/)といったモジュールバンドルツールを作うことで、ファイル単位でモジュールを分割した開発が可能になりました。

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


# 方法

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

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

```js: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関数* をフォーカスすると、ヒントアイコンが表示されます。
ヒントアイコンをクリックすると`新しいファイルへ移動します`と提案が表示されます。

![スクリーンショット 2019-07-16 12.02.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15964/1475264e-9d69-5f65-fc03-3bb863ed300c.png)

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

```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`に移動されています。

```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.js`と`index.js`で、お互いにimportしあっていて気持ち悪いです。


### doesAllModificaionHasSpecified

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

![スクリーンショット 2019-07-16 12.11.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15964/beb9dfd8-44ba-7f6e-8008-44ba2724cf62.png)

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

```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.js`は`doesAllModificaionHasSpecified.js`を参照し、`index.js`への参照はなくなりました。
一的に相互import問題は発生しますが、すべての*privateなstatic関数*をモジュールとして抽出すれば、自然に解消されます。

## その他

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

```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)
  }
}
```

```js: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));
}
```

```js: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のブログ](https://dev-dub.hatenablog.com/entry/2019/02/17/172455)