Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Trailblazerを業務で使ってみた

B49aa473d5cd7f08cdce3d56ef837f29?s=47 kbaba1001
September 20, 2015

 Trailblazerを業務で使ってみた

Trailblazer::Tokyo #1 (http://connpass.com/event/20137/) にて発表

B49aa473d5cd7f08cdce3d56ef837f29?s=128

kbaba1001

September 20, 2015
Tweet

Transcript

  1. Trailblazer を業 務で使ってみた Trailblazer を業 務で使ってみた @kbaba1001 Powered by Rabbit

    2.1.6 and COZMIXNG
  2. タイトルは釣りです Trailblazer gem は使ってない Reform のみ使用

  3. 作っているもの iOSアプリのサーバーサイド ほぼAPI、少しWebView

  4. 開発チーム Rails経験者4人 開発期間は約2か月

  5. 解決したかったこと APIリクエストの妥当性検証 モデルとリクエストパラメータ の粗結合化

  6. APIリクエストの妥当性 Grapeの例 desc 'Return a status.' params do requires :id,

    type: Integer, desc: 'Status id.' end route_param :id do get do Status.find(params[:id]) end end
  7. APIリクエストの妥当性 Grape は未使用 コントローラ、モデルでやりた くない フォームオブジェクトを作った

  8. フォームオブジェクト module ShopService class Search include ActiveModel::Model attr_accessor :latitude, :longitude

    validates :latitude, presence: true, numericality: true validates :longitude, presence: true, numericality: true def initialize(params = {}) @latitude = params[:latitude] @longitude = params[:longitude] end def collections # search は scope Shop.search(latitude: latitude, longitude: longitude) end end end
  9. コントローラ class ShopsController < ApplicationController def search service = ShopService::Search.new(params)

    if service.valid? @shops = service.collections else raise( InvalidParamatersError, service.errors.full_messages.join(', ') ) end end end
  10. テスト describe 'ShopService::Search' do specify do shop = FactoryGirl.create(:shop, latitude:

    35.0, longitude: 138.0 ) service = ShopService::Search.new( latitude: 35.0, longitude: 138.0 ) expect(service).to be_valid expect(service.collections).to include(shop) end end
  11. 雑感 わりとこれだけでよかった クラスのテスト速い、書きやす い しかし、入れ子パラメータを扱 えない

  12. 入れ子 params = { northeast: {latitude: 35.0, longitude: 138.0}, southwest:

    {latitude: 34.0, longitude: 137.0} } これを ActiveModel で扱う…?
  13. ActiveModelで入れ子 module ShopService class Search include ActiveModel::Model attr_accessor :northeast_latitude, :northeast_longitude

    attr_accessor :southwest_latitude, :southwest_longitude with_options(presence: true, numericality: true) do |v| v.validates :northeast_latitude, :northeast_longitude v.validates :southwest_latitude, :southwest_longitude end def initialize(params = {}) @northeast_latitude = params[:northeast][:latitude] @northeast_longitude = params[:northeast][:longitude] @southwest_latitude = params[:southwest][:latitude] @southwest_longitude = params[:southwest][:longitude] end end end
  14. (´・ω・`) 扱いづらい

  15. Reform module ShopService class Search < Reform::Form property :northeast do

    property :latitude property :longitude validates :latitude, presence: true, numericality: true validates :longitude, presence: true, numericality: true end property :southwest do property :latitude property :longitude validates :latitude, presence: true, numericality: true validates :longitude, presence: true, numericality: true end def collections # search は scope Shop.search(northeast: northeast, southwest: southwest) end end end
  16. Controller class ShopsController < ApplicationController def search service = ShopService::Search.new(Shop.new)

    if service.validate(params) @shops = service.collections else raise( InvalidParamatersError, service.errors.full_messages.join(', ') ) end end end
  17. いえーい v(`・▽・´)v

  18. Reform の機能 バリデーション、Mass Assignment 対策 入れ子パラメータ 配列を受け取る 複数モデルにパラメータを分配 モデルにない変数を定義

  19. Railsの機能と比較 strong_parameter / attr_accessible accepts_nested_attributes_fo r validates の if ,

    unless , on
  20. 次の例 ShopService::Create

  21. ShopService::Create module ShopService class Create < Reform::Form property :latitude property

    :longitude property :prefecture_code, virtual: true validates :latitude, presence: true, numericality: true validates :longitude, presence: true, numericality: true validates :prefecture_code, presence: true end end
  22. Controller class ShopsController < ApplicationController def create service = ShopService::Create.new(Shop.new)

    if service.validate(params) service.save do |hash| prefecture = Prefecture.find_by(code: service.prefecture_code) service.model.create( hash.except(:prefecture_code).merge(prefecture: prefecture) ) end head :no_content else raise( InvalidParamatersError, service.errors.full_messages.join(', ') ) end end end
  23. (´・ω・`) ビジネスロジックがコントロー ラにある

  24. save を上書き module ShopService class Create < Reform::Form # property

    は省略 def save super do |params| prefecture = Prefecture.find_by(code: prefecture_code) model.create( params.except(:prefecture_code).merge(prefecture: prefecture) ) yield params if block_given? end end end end
  25. Controller class ShopsController < ApplicationController def create service = ShopService::Create.new(Shop.new)

    if service.validate(params) service.save head :no_content else raise( InvalidParamatersError, service.errors.full_messages.join(', ') ) end end end
  26. テスト Service Spec でビジネスロジッ クをテスト Controller Spec(または Request Spec) で

    HTTP リク エストのテスト
  27. Service Spec describe 'ShopService::Create' do specify do shop = Shop.new

    service = ShopService::Create.new(shop) validation_result = service.validate( latitude: 35.0, longitude: 138.0, prefecture_code: 'AE01', ) expect(validation_result).to be(true) service.save shop.reload! expect(shop.latitude).to eq(35.0) expect(shop.longitude).to eq(138.0) expect(shop.prefecture.code).to eq('AE01') end end
  28. Request Spec describe 'shop api' do describe 'POST /api/v1/shop' do

    specify do post '/api/v1/shop', { latitude: 35.0, longitude: 138.0, prefecture_code: 'AE01' } expect(response.code).to be(204) expect(Shop.count).to be(1) end specify '異常系', rambulance: true do post '/api/v1/shop' expect(response.code).to be(400) expect(response.body).to include('緯度を入力してください') expect(response.body).to include('経度を入力してください') end end end
  29. Factoryを変更 ShopService::Create をテスト コード中で Factory として使う

  30. before describe 'ShopService::Search' do specify do shop = FactoryGirl.create(:shop, latitude:

    35.0, longitude: 138.0 ) service = ShopService::Search.new( latitude: 35.0, longitude: 138.0 ) expect(service).to be_valid expect(service.collections).to include(shop) end end
  31. after describe 'ShopService::Search' do specify do shop = ShopService::Create.new(Shop.new).validate( latitude:

    35.0, longitude: 138.0, prefecture_code: 'AE01', ).save service = ShopService::Search.new( latitude: 35.0, longitude: 138.0 ) expect(service).to be_valid expect(service.collections).to include(shop) end end
  32. メリット 「Shopを作成したことがある」 という状況を楽に再現できる FactoryGirl では自力で状況を再 現する必要がある 関連が多い時、他のモデルのデ ータも作る時、特に便利

  33. デメリット Read より先に Create/ Update の処理が必要 FactoryGirl のようにデフォルト 値を設定できない

  34. 小まとめ ビジネスロジックをサービス層 に封じ込めた サービス層に対してテストを書 く

  35. 小まとめ Controller Spec はレスポンス のみ見る サービス層を Factory として使 う

  36. で ここまでは実際にやったこと

  37. やってみたいこと Reform を Trailblazer (gem) にする

  38. Trailblazer gem Trailblazer gem が提供するも のは何か?

  39. Trailblazer README https://github.com/apotonick/ trailblazer 色々書いてあるけど、複数の gemを組み合わせるため Trailblazer gem 単体で提供す る機能は少ない

  40. Trailblazer gem Operation 層を提供する Operation 層を扱いやすくする メソッドを提供する

  41. Refor -> Trailblazer class Shop < ActiveRecord::Base class Create <

    Trailblazer::Operation contract do property :latitude property :longitude property :prefecture_code, virtual: true validates :latitude, presence: true, numericality: true validates :longitude, presence: true, numericality: true validates :prefecture_code, presence: true end def process(params) @model = Shop.new validate(params, @model) do |f| prefecture = Prefecture.find_by(code: f.prefecture_code) @model.create( params.except(:prefecture_code).merge(prefecture: prefecture) ).save end end end end
  42. Controller class ShopsController < ApplicationController include Trailblazer::Operation::Controller def create run

    Shop::Create do |op| head :no_content and return end raise( InvalidParamatersError, @model.errors.full_messages.join(', ') ) end end
  43. Factory # before shop = ShopService::Create.new(Shop.new).validate( latitude: 35.0, longitude: 138.0,

    prefecture_code: 'AE01' ).save # after shop = Shop::Create[ latitude: 35.0, longitude: 138.0, prefecture_code: 'AE01' ].model
  44. Trailblazer Operationの扱いがすっきりす る Callback や Validation モデル は Trailblazer の流儀に従う

  45. 感想 Rails にも MVC 以外の層を作っ てもいい 小さな gem を組み合わせる導 入の柔軟さ

    テストしやすさを重視 apotonick先生は何個gem作っ てるの…? Powered by Rabbit 2.1.6 and COZMIXNG