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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size 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 full-size slide

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

    View full-size 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 full-size 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 full-size 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 full-size slide

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

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

    View full-size slide

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

    View full-size 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 full-size slide

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

    View full-size 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 full-size 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  20. 次の例
    ShopService::Create

    View full-size 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 full-size 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 full-size slide

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

    View full-size 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 full-size 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 full-size slide

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

    View full-size 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 full-size 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 full-size slide

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

    View full-size 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 full-size 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide


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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size 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 full-size 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 full-size 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 full-size slide

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

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

    View full-size slide

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

    View full-size slide