Railsで railsconfig/config と ダックタイピング を使って簡易的にDIコンテナ化する

[Shinosaka.rb Advent Calendar 2016](http://qiita.com/advent-calendar/2016/shinosakarb) の 5日目の記事です。

## <i class="fa fa-cubes"></i> はじめに

「外部のリソースサーバなどからデータを取得する」などのユースケースを考えたときに、本番環境であればS3から取得する、ステージング環境であれば特定のサーバからFTPする、開発・テスト環境であればローカルのデータやモックを使いたいなどデプロイ先の環境に応じて切替えたいということがあると思います。

実現方法はいくつかあると思いますが、 今年のshinosaka.rb の AdventCalendar でも同様の記事があるのでとても参考になると思います！！

* 1日目 [featureスペックでクライアント（JavaScript）側から呼び出すRequestをモックする - Qiita](http://qiita.com/chimame/items/55f8369b539c28215ad9)
* 4日目 [Railsで外部リクエストをスタブする - Qiita](http://qiita.com/murajun1978/items/f430370a2194b292dfde)

### よくある実装パターン

このような仕様を実装する場合のよくあるパターンとしては `Rails.env.production?` の分岐により切替えることができます。例えば以下のような感じです。

```ruby:if分岐で分けてしまうパターン
class TodosController < ApplicationController
  def show 
    If Rails.env.production?
      # Procuction環境の処理
    else
      # Production以外の処理
    end
  end
  # ...省略
end
```

このパターンでもアプリケーションが単純なうちはいいのですが、規模が大きくなりこのようなif分岐がいたるところで出てくると運用保守という点でとてもやりにくくなることが想像できると思います。(stagingという env がでてきたらどうするかなど）

そんなとき **ダックタイピング** と **DI** によって環境ごとに切替えられると便利そうだということで、 [railsconfig/config](https://github.com/railsconfig/config) を使って簡易的なDIコンテナとして実現してみたので紹介します。

## <i class="fa fa-question-circle"></i> 用語について

### DIとは

DIとDIコンテナについては以下の記事がとてもわかり易かったので、そちらを参照してみてください。

* [DI・DIコンテナ、ちゃんと理解出来てる・・？ - Qiita](http://qiita.com/ritukiii/items/de30b2d944109521298f)
* [やはりあなた方のDependency Injectionはまちがっている。 — A Day in Serenity (Reloaded) — PHP, FuelPHP, Linux or something](http://blog.a-way-out.net/blog/2015/08/31/your-dependency-injection-is-wrong-as-I-expected/)


### [railsconfig/config](https://github.com/railsconfig/config) とは

使っている方も多いと思いますが、グローバルな定数を設定できるツールです。
アクセストークンなどを定義しておいてコード上からは `Settings.access_token` などでどこからでも参照して使えるといったものです。

## <i class="fa fa-cube"></i> 参考実装の構成
サンプルコードはここにおいています。
[yusabana-sandbox/dirails-sample](https://github.com/yusabana-sandbox/dirails-sample)

```
APP_ROOT
├── app/
│   ├── clients/
│   │   └── ad/
│   │       ├── development_contents.rb <= DIする依存クラス（本番以外の環境用)
│   │       └── production_contents.rb  <= DIする依存クラス(本番用)
│   ├── controllers/
│   │   ├── todos_controller.rb
│   │   └── ...
│   └── ...
│
├── config/
│   ├── settings.yml
│   ├── settings.local.yml
│   ├── settings/                  <= Rails.envによって切替えたい内容を記述する
│   │   ├── development.yml
│   │   ├── production.yml
│   │   └── test.yml
│   ├── application.rb
│   └── ...
└── ...
```


## <i class="fa fa-cube"></i> DIで切り替えるクラスを定義

まずは例としてAd(広告)のコンテンツ取得クラスがあるとします。それを各環境向けのクラスとして定義しておきます。コンテンツの取得ロジックなどは環境ごとに違うという想定です。

```ruby:本番環境向けのコンテンツ取得クラス
class Ad::ProductionContents
  def extract
    # ここに本番環境独自のコンテンツ取得のコードを書く
    'production contents'
  end
end
```

```ruby:本番環境以外のコンテンツ取得クラス
class Ad::DevelopmentContents
  def extract
    # ここに本番環境以外のコンテンツ取得コードを書く。
    # または以下のようにダミー文字列を返してスタブしてもいい
    'development contents'
  end
end
```

上記のように定義された各環境向けの `コンテンツ取得クラス` を [railsconfig/config](https://github.com/railsconfig/config) で動的に差し替えられるようにします。

## <i class="fa fa-cube"></i> [railsconfig/config](https://github.com/railsconfig/config) で参照するClientsクラスを切替える

### railsconfig の設定ファイルをyamlでかく環境ごとに定義

各環境ごとの設定を定義して、クラス名を書くことで依存クラスを指定します。

```yaml:config/settings/production.yml
# このファイルは Rails.env が production のときにのみ読み込まれる
clients_class: 'Ad::ProductionContents'
```

```yaml:config/settings/development.yml
# このファイルは Rails.env が development のときにのみ読み込まれる
clients_class: 'Ad::DevelopmentContents'
```

### 実際に利用するコード

例えばコントローラなどで、依存を記述した railsconfigの `Settings` からクラスを取得して実装するコードを書きます。
以下のようにControllerのクラスでは特に `Rails.env` の環境のことは意識しないで取得できます。


```ruby
class TodosController < ApplicationController
  def show 
    @ad_contents = Settings.clients_class.constantize.new.extract
  end
  # ...省略
end
```

* `Settings.clients_class.constantize.new` の部分
  * `production` のときは production向けの `Ad::ProductionContents` がインスタンス化され、それ以外のときは `Ad::DevelopmentContents` がインスタンス化されるようになります。
* 上記の通りインスタンス化して、共通のインターフェースの `extract` メソッドで各環境に応じたコンテンツが取得できます。


## <i class="fa fa-check"></i> まとめ

`Ad::ProductionContents` と `Ad::DevelopmentContents` は共通インターフェースとなる `extract` メソッドを持たせてダックタイピングをしつつ、 Settings(railsconfig) によってダックタイピングの依存オブジェクトを切替えて利用することが出来るようになります。

* メリット
    * ダックタイピングにより新たな **env** を作ったとしても特に呼び出し側(ここでは `TodosController` )は変更せずとも、 `XXXContents` クラスを追加するだけで機能追加できます。
    * コード内でのif分岐がなくなります。( `if Rails.env.prodution?` ) など
    * 依存オブジェクトをSettings(簡易DIコンテナ)で管理しているので依存オブジェクトをきりかえるなどが柔軟にできます。
* デメリット
    * 各環境のクラスがふえてきて実装コードがファイルレベルで分散してまうことで複雑度は増します。  
      (が、、慣れてしまえばRailsの規約同様、チームで開発しても同じような実装になり運用することはやりやすくなると思います。)

最後に、、サンプル実装この内容に関しては以下のコミットで確認ができます。

* [yusabana-sandbox/dirails-sample@dbfd145](https://github.com/yusabana-sandbox/dirails-sample/commit/dbfd1454570693fd1d98fd4509345de904cef777)



[Shinosaka.rb Advent Calendar 2016](http://qiita.com/advent-calendar/2016/shinosakarb) の 5日目の記事でした。明日は、、、「空き」です。（誰か...）

以上です。