SPA開発における各層の実装例

[これ](http://qiita.com/amoO_O/items/d54d69648df2b5236541)の続き

ちなみに SPA は Single Page Application の略らしい。Smart Phone Application かと思ってた。

# 命名規則
View, Controller, Service, Model, API の各レイヤで作るサービスの名前のルール。

- {layer}.{category}.{name}
    - layer
        - view, ctrl, srv, model, api のどれか
    - category
        - 省略可能
        - 必要に応じて「.」を増やし、階層化する
        - lowerCamelCase
    - name
        - サービス名
        - View, Service, Model は、一度生成されたものを使いまわすので、小文字始まり
        - Controller, API は、都度生成されるものなので 大文字始まり

また、ディレクトリは、サービス名の 「.」を「/」に変えるだけで行けるようにしたい。

以下は各層のサービスの例

# View
```coffeescript:views/loading.coffee
"use strict"
angular.module("app").factory "view.loading", [
  "$q", "$ionicLoading",
  ($q, $ionicLoading) ->
    return (stmt) ->
      $ionicLoading.show { template: "Loading..." }
      deferred = $q.defer()
      done = (x) ->
        deferred.resolve(x)
      done stmt done
      return deferred.promise.finally $ionicLoading.hide
]
```

- $ionicLoading で、画面を暗くして操作を出来なくし、その間に処理を行う、という操作を実現出来る。
- もともと各ページに書いてあったのを、view のサービスとして外出ししたもの。

# Controller
```coffeescript:controllers/product/gift/getForm.coffee
"use strict"
angular.module("app").controller "ctrl.product.gift.GetForm", [
  "$state", "$stateParams", "view.loading", "view.toaster", "srv.login", "srv.product", "srv.account", "srv.gift", "srv.message"
  ($state, $stateParams, loading, toaster, loginSrv, productSrv, accountSrv, giftSrv, messageSrv) ->
    loading (done) ->
      loginSrv.isLoggingIn().then (isLoggedIn) ->
        if not isLoggedIn
          return $state.go "user-login-form", {backToState: $state.current.name, stateParams: angular.toJson $stateParams }

        pProduct = productSrv.getProduct $stateParams.productId
        pFriendList = accountSrv.getFriendList()

        return $q.all [pProduct, pFriendList]
        .then ([product, friendList]) ->
          $scope.product = product
          $scope.friendList = friendList
          $scope.continue = (friendId) ->
            giftSrv.validate product.id, friendId
            .then ->
              $state.go "product-gift-confirm", {productId: product.id, friendId: friendId}
            .catch (errorCode)
              toaster.add messageSrv.message errorCode
]
```

- これで 23 行
- ページ遷移のための簡単な if 分岐を除き、基本的には model からもらったデータを view（$scope）に渡しているだけ。
- promise を駆使しているので、「どの時点でどのデータを取得できているか」は見失わないようにしたい。

# Service

```coffeescript:services/gift.coffee
angular.module("app").factory "srv.gift", [
  "model.inventory", "model.account", "model.gift"
  (inventoryModel, accountModel, giftModel) ->
    giftSrv =
      validate: (productId, friendId) ->
        pHasProduct = inventoryModel.hasProduct productId
        pHasFriend = accountModel.hasFriend friendId
        return $q.all [pHasProduct, pHasFriend]
        .then ->
          giftModel.validate productId, friendId

      giftTo: (productId, friendId) ->
        @validate productId, friendId
        .then ->
          giftModel.giftTo productId, friendId
    return giftSrv
]
```

- Model を組み合わせて、ビジネスロジックを実現している。
- 各種エラーは、Model が生成しているので Service では何もしていない
    - 今回はたまたまそうだというだけで、Service が Error を生成してはいけないわけではない

# Model
```coffeescript:models/gift.coffee
angular.module("app").factory "model.gift", [
  "model.message", "api.gift.Validate", "api.gift.Post"
  (messageModel, ValidateGiftAPI, PostGiftAPI) ->
    giftModel =
      validate: (productId, friendId) ->
        api = new ValidateGiftAPI()
        return api.request productId, friendId
        .then (response) ->
          if not response.data.hasError
            return true
          switch response.data.error_code
            when "noThankYou" then $q.reject messageModel.warn.REFUSED_YOUR_GIFT
            when "haveAlreadyGot" then $q.reject messageModel.warn.FRIEND_HAVE_ALREADY_GOT

      giftTo: (productId, friendId) ->
        api = new PostGiftAPI()
        return api.request productId, friendId
]
```

- ほぼ API を呼び出すだけ。
- API が返すエラーを promise を reject する形で上の層へ伝えている。
- API の通信エラーや 401エラー などは、各種 Model では記述せず、$httpProvider.intercepts に登録するべきだ。

# API
```coffeescript:apis/gift/validate.coffee
anguler.module("app").factory "api.gift.Validate", [
  "$http", "api.Base"
  ($http, BaseAPI) ->
    class Validate extends BaseAPI
      path = "/gift/validate"

      useSsl: -> true

      getPath: -> super path

      getParams: (productId, friendId) ->
        return super
          productId: productId
          friendId: friendId

      request: (productId, friendId) ->
        return $http.get @getPath(),
          params: @getParams productId, friendId

    return Validate
]
```

- 一番単純。$http を使って API を叩くだけ。
- 自動生成する仕組みを作ってもいいかもしれない。
- BaseAPI に、URL の共通部分や、API の version, デフォルトで渡す Params などが置いてある。

# その他
キャッシュ周り、セキュリティ周りはまだ勉強中。
かなり実践に近いコードを書いてみました。何かの参考になれば幸いです。