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

Railsアプリの設計

 Railsアプリの設計

銀座Rails#16 @リンクアンドモチベーション
https://ginza-rails.connpass.com/event/155467/

Takumi Shotoku

December 13, 2019
Tweet

More Decks by Takumi Shotoku

Other Decks in Technology

Transcript

  1. 自己紹介 • 名前: 神速 • 会社: メドピア株式会社 • GitHub: @sinsoku

    (画像右上) • Twitter: @sinsoku_listy (画像右下) • Rails歴: 約7年 2
  2. The Rails Way なコード • ! 公式ドキュメントに記載あるコード • https://api.rubyonrails.org/ •

    https://railsguides.jp/ • " rails new と rails g で生成されるコード これらを読んで The Rails Way の全体像を推測してます。 11
  3. リソースベースのルーティング(REST) Rails.application.routes.draw do resources :users end # Prefix Verb URI

    Pattern Controller#Action # users GET /users(.:format) users#index # POST /users(.:format) users#create # new_user GET /users/new(.:format) users#new # edit_user GET /users/:id/edit(.:format) users#edit # user GET /users/:id(.:format) users#show # PATCH /users/:id(.:format) users#update # PUT /users/:id(.:format) users#update # DELETE /users/:id(.:format) users#destroy 13
  4. Representational State Transfer (REST) 重要なのは以下の2つ。 • HTTPメソッドで 操作 を表現する •

    GET, POST, PATCH/PUT2, DELETE • URIで リソース を表現する 2 PATCH/PUTの違いは省略。気になる人はググってほしい。 14
  5. Rails のルーティング(RESTful) Rails.application.routes.draw do resources :users end # Prefix Verb

    URI Pattern Controller#Action # users GET /users(.:format) users#index # POST /users(.:format) users#create # new_user GET /users/new(.:format) users#new # edit_user GET /users/:id/edit(.:format) users#edit # user GET /users/:id(.:format) users#show # PATCH /users/:id(.:format) users#update # PUT /users/:id(.:format) users#update # DELETE /users/:id(.:format) users#destroy 15
  6. メールの送信 class UsersController < ApplicationController def create respond_to do |format|

    if @user.save # อଘޙʹUserMailerΛ࢖ͬͯwelcomeϝʔϧΛૹ৴ UserMailer.with(user: @user).welcome_email.deliver_later format.html { redirect_to(@user, notice: 'Ϣʔβʔ͕ਖ਼ৗʹ࡞੒͞Ε·ͨ͠ɻ') } format.json { render json: @user, status: :created, location: @user } else format.html { render action: 'new' } format.json { render json: @user.errors, status: :unprocessable_entity } end end end end 22
  7. bin/setup リポジトリを git clone したあと、1コマンドで開発環境を整え られる。 $ bin/setup # "bundle

    install" ͱ "yarn install" ͰґଘύοέʔδͷΠϯετʔϧ # σʔλϕʔεͷ࡞੒, ...ͳͲ $ bin/rails server # αʔό͕ىಈ͠ɺ http://localhost:3000 Ͱը໘Λ֬ೝͰ͖Δ 24
  8. database.yml データベースの接続設定を環境変数で指定できることが記載され ている。 # On Heroku and other platform providers,

    you may have a full connection URL # available as an environment variable. For example: # # DATABASE_URL="mysql2://myuser:mypass@localhost/somedatabase" # # You can use this database configuration with: # # production: # url: <%= ENV['DATABASE_URL'] %> # production: <<: *default 25
  9. 開発環境のキャッシュの設定 キャッシュ設定をON/OFFする仕組みが用意されている。 # Enable/disable caching. By default caching is disabled.

    # Run rails dev:cache to toggle caching. if Rails.root.join('tmp', 'caching-dev.txt').exist? config.action_controller.perform_caching = true config.action_controller.enable_fragment_cache_logging = true config.cache_store = :memory_store config.public_file_server.headers = { 'Cache-Control' => "public, max-age=#{2.days.to_i}" } else config.action_controller.perform_caching = false config.cache_store = :null_store end 26
  10. index, show GETの処理には分岐がなくて、副作用(=DBへの書き込み)も存 在しない。 before_action :set_user, only: [:show, :edit, :update,

    :destroy] def index @users = User.all end def show end private def set_user @user = User.find(params[:id]) end 28
  11. create 正常系・異常系の単純な分岐になっている。 def create @user = User.new(user_params) respond_to do |format|

    if @user.save format.html { redirect_to @user, notice: 'User was successfully created.' } format.json { render :show, status: :created, location: @user } else format.html { render :new } format.json { render json: @user.errors, status: :unprocessable_entity } end end end 29
  12. update create とほぼ同じで、正常系・異常系の単純な分岐になってい る。 before_action :set_user, only: [:show, :edit, :update,

    :destroy] def create respond_to do |format| if @user.update(user_params) format.html { redirect_to @user, notice: 'User was successfully updated.' } format.json { render :show, status: :ok, location: @user } else format.html { render :edit } format.json { render json: @user.errors, status: :unprocessable_entity } end end end 30
  13. timecop 現在時刻のスタブ ActiveSupport::Testing::TimeHelpers 3 が使える。 Time.current # => Sun, 09

    Jul 2017 15:34:49 EST -05:00 freeze_time sleep(1) Time.current # => Sun, 09 Jul 2017 15:34:49 EST -05:00 3 https://api.rubyonrails.org/classes/ActiveSupport/Testing/TimeHelpers.html#method-i-freeze_time 34
  14. activerecord-import バルクインサート Rails 6から insert_all が使える。4 Book.insert_all([ { id: 1,

    title: "Rework", author: "David" }, { id: 2, title: "Eloquent Ruby", author: "Russ" } ]) 4 https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-insert_all 35
  15. ! 要件定義 • 実装する機能の目的を聞き出す • HowをWhyに変える • その目的は コードを書かないで解決できないか •

    コードを書かずに解決するのが一番良い • コードを書かなければバグは起きない • エンジニアの人件費は高い " 41
  16. 45

  17. 46

  18. 47

  19. 48

  20. RESTではないルーティング例 resources :advent_calendars do post :join, on: :member post :leave,

    on: :member end # GET /advent_calendars # POST /advent_calendars # GET /advent_calendars/ruby # PUT/PATCH /advent_calendars/ruby # DELETE /advent_calendars/ruby # POST /advent_calendars/ruby/join?day=1 # POST /advent_calendars/ruby/leave?day=1 52
  21. RESTなルーティング例 resources :advent_calendars do resources :registrations, only: [:create, :destroy] end

    # GET /advent_calendars # POST /advent_calendars # GET /advent_calendars/ruby # PUT/PATCH /advent_calendars/ruby # DELETE /advent_calendars/ruby # POST /advent_calendars/ruby/registrations # DELETE /advent_calendars/ruby/registrations/1 53
  22. テーブル設計 NOT NULL制約, ユニーク制約、外部キー制約などに気をつける。 create_table :advent_calendars do |t| t.references :user,

    null: false, foreign_key: { on_delete: :cascade } t.string :name, null: false, limit: 20, index: { unique: true } t.string :uri, null: false, limit: 20, index: { unique: true } t.text :description t.timestamps end create_table :advent_calendar_registrations do t.references :advent_calendar, null: false, foreign_key: { on_delete: :cascade } t.integer :day, null: false t.string :comment, null: false, default: "", limit: 100 t.string :url, null: false, default: "", limit: 100 t.timestamps t.index [:advent_calendar_id, :day], unique: true end 54
  23. コントローラーの設計 class AdventCalendarsController before_action :set_calendar, only: [:show, :edit, :update, :destroy]

    def index @calendars = AdventCalendar.order(id: :desc).page(params[:page]) end def new @calendar = AdventCalendar.new end def create @calendar = AdventCalendar.new(advent_calendar_params) if @calendar.save # ਖ਼ৗܥ else # ҟৗܥ end # ҎԼུ end 56
  24. module AdventCalendars class RegistrationsController before_action :set_calendar # POST /advent_calendars/:advent_calendar_id/registrations def

    create @registration = @calendar.advent_calendar_registrations.new(advent_calendar: @calendar) if @registration.save # ਖ਼ৗܥ else # ҟৗܥ end end # DELETE /advent_calendars/:advent_calendar_id/registrations/:id def delete @registration = @calendar.advent_calendar_registrations.find(id: params[:id]) @registration.destroy # ϦμΠϨΫτॲཧ end private def set_calendar @calendar = AdventCalendar.find(params[:advent_calendar_id]) end end end 57
  25. 最初はこれで済ませたりする class Blog < ApplicationRecord has_many :tags def save_with_tags(tag_names:) ActiveRecord::Base.transaction

    do self.tags = tag_names.map { |name| Tag.new(name: name) } save! end true rescue false end end 65
  26. app下にディレクトリを増やさない • ビジネスロジックを良い感じにする Interactor • View層のロジックをまとめる Draper, ActiveDecorator • 権限管理

    CanCanCan, Pundit, Banken Models, Views, Controllers の3つでも複雑になるの に、この辺りの ! を使いこなせるとあまり思えない 68
  27. 基本的に RuboCop ! の標準ルールで書く • RuboCop の指摘は警告(エラーではない) • Layout/LineLength 80〜:

    変数抽出を検討する • Metrics/AbcSize 15〜: リファクタを検討する • 他メンバーを説得できる理由があれば無効にする • ! 開発メンバーが日本人のみなので Style/AsciiComments を無効 70
  28. Moduleはprivateメソッドを使えない # app/models/concerns/rankingable.rb module Rankingable def rank # ུ end

    private def calculate # ུ end end class User include Rankingable end User.new.private_methods.include?(:calculate) #=> true 73
  29. delegate しても良い class User delegate :rank, to: :user_ranking private def

    user_ranking @user_ranking ||= User::Ranking.new(self) end end user = User.new user.rank #=> 1 75