有頂天Ruby

ビールを飲みながらRubyについて書きます。

オブジェクト指向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のアプリケーションでもこのテクニックをもっと試してみたい。 それはあなたによりよいオブジェクト設計について考えること、より特定の目的を持ったオブジェクトを持つこと、値オブジェクト上にロジックを持つことを制限(モデルとプレゼンターのように)することを強制するだろう。

あなたがこの記事について考えていることを、ぜひ教えて下さい! :)