オブジェクト指向Rails - よりよいコントローラーを書く
Ruby 5で紹介されていたObject Oriented Rails - Writing better controllers - Pivotal Labsの訳です。(翻訳の質はうんこかもしれません、元記事読みましょう)
私は最近、Objective-Cを使ってiOS、Javaを使ってAndroid、たくさんのモバイル開発をしている。
私はRuby開発者でもあるので、それらの言語やフレームワークから学んだことを可能な限りRubyでも試し、適用してみようとするのは当然のことだろう。
今日はRailsアプリでよりよいコントローラを書くため、僕のベストを尽くそうと思う。
私はAndroidやiOS向けのアプリを書くとき、スタブやモックを使わないよう、自分自身に強制している。それは、オブジェクトの設計を向上させ、DI(依存性の注入)を使ってテストを書くことを意図している。
では、コードの例を見てみよう。
class User validates_uniqueness_of :username end class RegistrationController < ApplicationController def create user = User.where(username: params[:username]).first user ||= User.new.tap do |new_user| new_user.username = params[:username] new_user.save! end render json: user end end
ここで我々が必要としているのは、ユーザーを登録するためのユーザー名だ。 あと、もしもユーザーが既に登録していた際は、単にユーザーの情報を返すだけだ。
すっごくシンプルだね。
だけど、そのロジックはControllerに属していない。
それだと、うんこみたいなスタブやモックを使わずに、データベースに接続せずにコントローラーをテストすることは不可能だ。
any_instance
をテストで使うのは、「あなたは間違ったことをしている」ことを示す良い目印だ。
まず最初のカイゼンは次のようになるだろう
class User validates_uniqueness_of :username def self.register(username) user = User.where(username: username).first user ||= User.new.tap do |new_user| new_user.username = username new_user.save! end end end class RegistrationController < ApplicationController def create user = User.register(params[:username]) render json: user end end
これは既に元よりもはるかに優れたコードだ、君はコントローラーをテストするためにUser.register
をスタブすればいい。それでうまく行く。
もしも君がそこで足をとめ、君のアプリが大きく成長しすぎなければ、それでうまいこと出来るだろう。
だけど、もし君のアプリが成長しはじめたら、User
モデルはもっと大きく大きく育っていって、君はより多くの責務を与えていくだろう、
君はUser
モデルにもっと責務を与えていくだろう。そして、君が知っていることを超えたあたりで「僕はただしいのかな?」と自分自身に問いかけるんだ。
コードをよりよく切り分ける道、実際にスタブを取り除く最終的なコードがこれだ:
class User validate_uniqueness_of :username end class RegistrationService def register(username) user = User.where(username: username).first user ||= User.new.tap do |new_user| new_user.username = username new_user.save! end end end class RegistrationController < ApplicationController before_filter :load_registration_service def create user = @registration_service.register(params[:username]) render json: user end def load_registration_service(service = RegistrationService.new) @registration_service ||= service end end
RegistrationService
は、データベースに関連する登録に関してのみ、責務をおう。
このサービス自体はデータベースにアクセスを行わずに、HTTPサービスを使うような別のものに取り替えることが可能なことに注意して欲しい。
その上、あなたは今テストから完全にスタブを取り除くことが出来るようになった。 次のような感じだ
describe RegistrationController do before do @fake_registration_service = controller. load_registration_service(FakeRegistrationService.new) end describe "POST #create" do it "delegates to the registration service and renders the returned user" do expected_user = User.new.tap {|u| u.username = "damien"} @fake_registration_service.registered_user = expected_user post :create, username: "damien" expect(response.status).to eq(200) expect(response.body).to eq(expected_user.to_json) expect(@fake_registration_service.username_registered).to eq("damien") end end end class FakeRegistrationService attr_accessor :registered_user attr_reader :username_registered def register(username) @username_registered = username registered_user end end
哀しいことに、我々はRailsがどうやってControllerをインスタンス化するのかコントロールしていない。
だから我々はbefore_filter
の中でこんな感じのトリックを使って、サービスのデフォルト値を使うようにしなくちゃならない。
PORO(Plain Old Ruby Object、素のRuby)でこれをやるならコンストラクターを作って似たようなことをやればい。
AndroidやiOSのアプリでテストを走らせるとき、依存性の注入を使うのは非常に一般的だ。 私は個人的にはこれらのプラットフォーム上でモックを使わないことを好む。
そして、私はRuby on Railsのアプリケーションでもこのテクニックをもっと試してみたい。 それはあなたによりよいオブジェクト設計について考えること、より特定の目的を持ったオブジェクトを持つこと、値オブジェクト上にロジックを持つことを制限(モデルとプレゼンターのように)することを強制するだろう。
あなたがこの記事について考えていることを、ぜひ教えて下さい! :)