Posted at

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

More than 1 year has passed since last update.

Shinosaka.rb Advent Calendar 2016 の 5日目の記事です。


はじめに

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

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


よくある実装パターン

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


if分岐で分けてしまうパターン

class TodosController < ApplicationController

def show
If Rails.env.production?
# Procuction環境の処理
else
# Production以外の処理
end
end
# ...省略
end

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

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


用語について


DIとは

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


railsconfig/config とは

使っている方も多いと思いますが、グローバルな定数を設定できるツールです。

アクセストークンなどを定義しておいてコード上からは Settings.access_token などでどこからでも参照して使えるといったものです。


参考実装の構成

サンプルコードはここにおいています。

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
│   └── ...
└── ...


DIで切り替えるクラスを定義

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


本番環境向けのコンテンツ取得クラス

class Ad::ProductionContents

def extract
# ここに本番環境独自のコンテンツ取得のコードを書く
'production contents'
end
end


本番環境以外のコンテンツ取得クラス

class Ad::DevelopmentContents

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

上記のように定義された各環境向けの コンテンツ取得クラスrailsconfig/config で動的に差し替えられるようにします。


railsconfig/config で参照するClientsクラスを切替える


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

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


config/settings/production.yml

# このファイルは Rails.env が production のときにのみ読み込まれる

clients_class: 'Ad::ProductionContents'


config/settings/development.yml

# このファイルは Rails.env が development のときにのみ読み込まれる

clients_class: 'Ad::DevelopmentContents'


実際に利用するコード

例えばコントローラなどで、依存を記述した railsconfigの Settings からクラスを取得して実装するコードを書きます。

以下のようにControllerのクラスでは特に Rails.env の環境のことは意識しないで取得できます。

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 メソッドで各環境に応じたコンテンツが取得できます。


まとめ

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


  • メリット


    • ダックタイピングにより新たな env を作ったとしても特に呼び出し側(ここでは TodosController )は変更せずとも、 XXXContents クラスを追加するだけで機能追加できます。

    • コード内でのif分岐がなくなります。( if Rails.env.prodution? ) など

    • 依存オブジェクトをSettings(簡易DIコンテナ)で管理しているので依存オブジェクトをきりかえるなどが柔軟にできます。



  • デメリット


    • 各環境のクラスがふえてきて実装コードがファイルレベルで分散してまうことで複雑度は増します。

      (が、、慣れてしまえばRailsの規約同様、チームで開発しても同じような実装になり運用することはやりやすくなると思います。)



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

Shinosaka.rb Advent Calendar 2016 の 5日目の記事でした。明日は、、、「空き」です。(誰か...)

以上です。