現場のSails.js #2 Controllerのアーキテクチャ編

# 概要
弊社では一部の社内プロジェクトでSails.jsを利用しています。その中で得られたベストプラクティスやらバッドプラクティスをまとめていこうと思います。
第2回はControllerの作り方について。

# 動作環境
* node.js v0.12.7
* Sails.js v0.11.2
* sails-mysql v0.11.0

# アーキテクチャ概要
View(ejs) -> ViewController -> ApiController -> Service -> Modelの5層構造になっています。ViewとModelは生のSails.js、ServiceもよくあるService層なので、ここではViewControllerとApiControllerについて詳しく説明します。

## ViewControllerとApiController
Sails.jsでは、View(html)を返すかjsonを返すかという違いを、たった1行のコードで表現できます。

```js:HogeController.ts
class HogeController {

  // hogepageをレンダリングして返すaction
  find(req, res) {
    Hoge.find().then(hoges => {
      res.view('hogepage', hoges);
    });
  }
  
  // hogesのjsonを返すaction
  findOne(req, res) {
    Hoge.find().then(hoges => {
      res.send(hoges);
    });
  }
}

export default new HogeController();
```

この仕組みはサクッとコードを書くには便利なのですが、規模が大きくなってくると混乱が生じます。Viewを返すactionとjsonを返すactionが入り混じり、どのControllerにどのメソッドがあるのか、把握することが難しくなってしまいます。そのため、ViewControllerとApiControllerを明確に分離しています。

ディレクトリ構成は以下のような形です。API用HogeControllerのパスが `api/controllers/api/HogeController.ts`と`api`が続くのが若干気持ち悪いですが、この構成ならSails.jsのblueprint機能で`GET /api/hoge/find` のようなURLが自動的に紐付くので結構便利です。

```
/api
 |_/controllers
   |_HogeController.ts
   |_/api
     |_HogeController.ts
```

また、ViewControllerは直接Service層にアクセスすることはなく、ApiController経由で全てのデータを取得します。

### API Controllerのサンプル
APIControllerは直接APIとして叩くケースと、ViewControllerからアクセスされるケースがあります。ただ、この違いはViewControllerの方で吸収しているため、APIControllerでは普通に `res.ok` でsuccessのレスポンス、 `res.send` でエラーレスポンスを返しているだけです。

```ts:api/controllers/api/TagController.ts
class TagController {
  'use strict';

  constructor() { }

  find(req, res) {
    sails.log.verbose('api::TagController#find');
    sails.log.verbose('params', req.allParams());
    sails.log.verbose(`user=${(req.user ? req.user.id : 'null') }`);

    const page = +req.param('page') || 1;

    TagService.find({
      limit: 30,
      page: page
    }).then((tags) => {

      res.ok(tags);

    }).catch((error) => {
      sails.log.error("api::TagController.find", error);
      res.send(500);
    });
  }
```

### View Controllerのサンプル
ViewControllerからApiControllerにアクセスするために、expressの`res`オブジェクトと同じI/Fのオブジェクトを渡して結果を受け取っています。(ちょっと作りは雑ですが)TypeScriptでコードを書いているので、ベースクラスを作成して共通化しています。

``` ts:api/controller/BaseController.ts
export default class BaseController {
  Api(method, req) {
    return new Promise((resolve, reject) => {
      method(req, {
        ok: (result) => {
          resolve(result);
        },
        send: (error, result) => {
          if (typeof error === 'object') {
            return resolve(result);
          }

          reject(new NetworkError('API call Error', error));
        }
      });
    });
  }
```

実際のViewControllerは以下のように実装します。先ほどの`BaseController`を継承し、`ApiController`のメソッドを`Api()`メソッドを利用して呼び出しています。

```ts:api/controller/TagController.ts
import BaseController from './BaseController';
import ApiTagController from './api/TagController';

class TagController extends BaseController {

  find(req, res) {
    sails.log.verbose('view::TagController#find');
    sails.log.verbose('params', req.allParams());
    sails.log.verbose(`user=${req.user ? req.user.id : "null"}`);

    this.Api(ApiTagController.all, req).then(tags => {

      res.view('tagpage', tags);

    }).catch(error => {
      // error
    });
  }
}

export default new TagController();
```

# まとめ
ViewControllerとApiControllerを明確に役割を分けることで、可読性が上がりメンテナンス性が向上します。何でも出来るフレームワークというものは破綻させることも簡単なので、適切なルールを作って運用していきましょう。