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

Trailblazerを業務で使ってみた

kbaba1001
September 20, 2015

 Trailblazerを業務で使ってみた

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

kbaba1001

September 20, 2015
Tweet

More Decks by kbaba1001

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  7. APIリクエストの妥当性
    Grape は未使用
    コントローラ、モデルでやりた
    くない
    フォームオブジェクトを作った

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  11. 雑感
    わりとこれだけでよかった
    クラスのテスト速い、書きやす

    しかし、入れ子パラメータを扱
    えない

    View Slide

  12. 入れ子
    params = {
    northeast: {latitude: 35.0, longitude: 138.0},
    southwest: {latitude: 34.0, longitude: 137.0}
    }
    これを ActiveModel で扱う…?

    View Slide

  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

    View Slide

  14. (´・ω・`)
    扱いづらい

    View Slide

  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

    View Slide

  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

    View Slide

  17. いえーい
    v(`・▽・´)v

    View Slide

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

    View Slide

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

    View Slide

  20. 次の例
    ShopService::Create

    View Slide

  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

    View Slide

  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

    View Slide

  23. (´・ω・`)
    ビジネスロジックがコントロー
    ラにある

    View Slide

  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

    View Slide

  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

    View Slide

  26. テスト
    Service Spec でビジネスロジッ
    クをテスト
    Controller Spec(または
    Request Spec) で HTTP リク
    エストのテスト

    View Slide

  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

    View Slide

  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

    View Slide

  29. Factoryを変更
    ShopService::Create をテスト
    コード中で Factory として使う

    View Slide

  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

    View Slide

  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

    View Slide

  32. メリット
    「Shopを作成したことがある」
    という状況を楽に再現できる
    FactoryGirl では自力で状況を再
    現する必要がある
    関連が多い時、他のモデルのデ
    ータも作る時、特に便利

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide


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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  44. Trailblazer
    Operationの扱いがすっきりす

    Callback や Validation モデル
    は Trailblazer の流儀に従う

    View Slide

  45. 感想
    Rails にも MVC 以外の層を作っ
    てもいい
    小さな gem を組み合わせる導
    入の柔軟さ
    テストしやすさを重視
    apotonick先生は何個gem作っ
    てるの…?
    Powered by Rabbit 2.1.6 and COZMIXNG

    View Slide