Railsアプリの設計

 Railsアプリの設計

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

Ecad9d801d79f6c6e5df93094690685e?s=128

Takumi Shotoku

December 13, 2019
Tweet

Transcript

  1. Railsアプリの設計 銀座Rails#16 2019/12/13(Fri) 神速(@sinsoku) 1

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

    (画像右上) • Twitter: @sinsoku_listy (画像右下) • Rails歴: 約7年 2
  3. 話すこと 1. 「コードの読みやすさ」について 2. 設計するときの流れ 3. 実装で気にしてること 日常の開発で考えていること、思考過程を紹介します。 3

  4. 1. 「コードの読みやすさ」について 4

  5. Railsアプリの読みやすさ • Railsの哲学 • 同じことを繰り返さない(DRY:Don't Repeat Yourself) • 設定より規約(CoC:Convention over

    Configuration) • 「The Rails Way」「Rails流」 よく聞く単語たち。 5
  6. Railsガイド Railsは、最善の開発方法というものを1 つに定めるという、ある意味大胆な判断 に基いて設計されています。 Railsは、何かをなすうえで最善の方法と いうものが1つだけあると仮定し、それに 沿った開発を全面的に支援します。 言い換えれば、ここで仮定されている理 想の開発手法に沿わない別の開発手法は 行いにくくなるようにしています。

    この「The Rails Way」、「Rails流」とで もいうべき手法を...(略 6
  7. 現場で使える Ruby on Rails 5速習実践ガイド Railsはフレームワークなので、その上に 自分たちのコードを書くことで独自のア プリケーションを実現します。 自分たちのコードを書く際には、 Rails

    らしく考え、Railsらしいコードを書く という基本的な姿勢が大切になってきま す。 7
  8. The Rails Doctrine1 1 https://postd.cc/rails-doctrine/ 8

  9. 考え方はなんとなく分かった 9

  10. 具体的なコードはどう書くのか 10

  11. The Rails Way なコード • ! 公式ドキュメントに記載あるコード • https://api.rubyonrails.org/ •

    https://railsguides.jp/ • " rails new と rails g で生成されるコード これらを読んで The Rails Way の全体像を推測してます。 11
  12. RailsガイドとAPIドキュメントの抜粋 12

  13. リソースベースのルーティング(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
  14. Representational State Transfer (REST) 重要なのは以下の2つ。 • HTTPメソッドで 操作 を表現する •

    GET, POST, PATCH/PUT2, DELETE • URIで リソース を表現する 2 PATCH/PUTの違いは省略。気になる人はググってほしい。 14
  15. 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
  16. ActiveRecord の命名ルール3 3 https://api.rubyonrails.org/classes/ActiveSupport/Testing/TimeHelpers.html#method-i-freeze_time 16

  17. ActiveRecord のスキーマのルール 17

  18. Timestamp のカラム名について time型は xxx_at で、date型は xxx_on が使われている。 18

  19. validate で指定するメソッド名 must_be_xxx という命名になっている。 19

  20. RailsガイドのActionView4 4 https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-insert_all 20

  21. RailsガイドのActionView 21

  22. メールの送信 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
  23. rails new で生成されるコードの紹介 23

  24. bin/setup リポジトリを git clone したあと、1コマンドで開発環境を整え られる。 $ bin/setup # "bundle

    install" ͱ "yarn install" ͰґଘύοέʔδͷΠϯετʔϧ # σʔλϕʔεͷ࡞੒, ...ͳͲ $ bin/rails server # αʔό͕ىಈ͠ɺ http://localhost:3000 Ͱը໘Λ֬ೝͰ͖Δ 24
  25. 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
  26. 開発環境のキャッシュの設定 キャッシュ設定を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
  27. Scaffold コントローラーの紹介 27

  28. 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
  29. 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
  30. 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
  31. Rails Wayとgem 31

  32. 依存gemは少ない方が良い • gemのコンセプトが Rails Way に合っていない可能性 • 依存gemに比例して学習コストは増える • 新しいメンバーを増やすときに辛い

    • gemをアップグレードし続けるコスト 32
  33. Railsに機能が増えて gemが不要になるケース 33

  34. 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
  35. 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
  36. ここまでのまとめ • 公式ドキュメントからThe Rails Wayを推測する • RESTなルーティング • シンプルなコントローラー •

    rails newの初期設定を尊重する • ! を入れすぎない 36
  37. 仕様 -> ? -> 読みやすいコード 37

  38. 2. 設計するときの流れ 38

  39. 設計するときの流れ 1. 要件定義 2. 画面設計 3. リソースとDB設計 4. クラス設計 5.

    実装 39
  40. 具体例 ! 「◦iitaのようなアドベントカレ ンダー機能を作りたい」 40

  41. ! 要件定義 • 実装する機能の目的を聞き出す • HowをWhyに変える • その目的は コードを書かないで解決できないか •

    コードを書かずに解決するのが一番良い • コードを書かなければバグは起きない • エンジニアの人件費は高い " 41
  42. ! 「12月のDAUを増やしたい」 ! 「記事数を増やしたい」 42

  43. ! 要件定義 全体の仕様を最小の機能に分割し、依存関係を考える。 • 1 カレンダーを登録できる • 2 カレンダーに参加登録できる •

    2 カレンダーに「いいね」できる • 2 新しいカレンダーを管理者にメール通知する 43
  44. ! 画面設計 • 画面イメージをしっかり描く • 表示する項目を洗い出す • 異常系を考えておく • エラーメッセージの表示位置

    44
  45. 45

  46. 46

  47. 47

  48. 48

  49. ! 「思っていたのと違う」 と言われないように、実装前に見せる 49

  50. 顧客に確認 ! 「こんな感じの画面で認識あってます?」 ! 「OK」 ※口頭よりSlackなどの文書で残した方が良い 50

  51. ! リソース設計 • RESTなルーティングを考える • 必要なテーブルを考える • テーブルはできるだけ正規化する • DBの制約は厳しくする

    51
  52. 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
  53. 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
  54. テーブル設計 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
  55. 実装イメージが頭の中でだいたい固まる 55

  56. コントローラーの設計 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
  57. 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
  58. RESTなルーティングだとコント ローラーはシンプルになる 58

  59. 脳内 -> ! -> Rubyコード 59

  60. 3. 実装で気にしてること 60

  61. 良い本はいくつもある 61

  62. 基本の考え • コードは短く、読みやすく書く • 基本的に RuboCop ! の標準ルールで書く • メソッドの影響範囲を分かりやすくする

    • コードを消しやすくする 62
  63. コードは短く、読みやすく書く • 短いコードは正義 • クラスやモジュールを作り過ぎない • 最初は単純なメソッドで済ませる • メタプロで短くするのは控える •

    パフォーマンスは後で考える " 63
  64. 雑談: 複数テーブルを扱うモデルはどう書くか? • サービスクラス • フォームオブジェクト • ドメイン層? 64

  65. 最初はこれで済ませたりする 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
  66. 別クラスを作ったときの行数 > メソッド行数 常に短いコードを目指す 66

  67. 機能が増えたら別クラスにする # app/models/blog/blog_with_tag.rb class Blog class BlogWithTag include ActiveModel::Model attr_accessor

    :blog, :tag_names def save ActiveRecord::Base.transaction do # ...ུ end end end end 67
  68. app下にディレクトリを増やさない • ビジネスロジックを良い感じにする Interactor • View層のロジックをまとめる Draper, ActiveDecorator • 権限管理

    CanCanCan, Pundit, Banken Models, Views, Controllers の3つでも複雑になるの に、この辺りの ! を使いこなせるとあまり思えない 68
  69. 話を戻す 69

  70. 基本的に RuboCop ! の標準ルールで書く • RuboCop の指摘は警告(エラーではない) • Layout/LineLength 80〜:

    変数抽出を検討する • Metrics/AbcSize 15〜: リファクタを検討する • 他メンバーを説得できる理由があれば無効にする • ! 開発メンバーが日本人のみなので Style/AsciiComments を無効 70
  71. メソッドの影響範囲を分かりやすくする • privateメソッドを活用する • Concernsを安易に使わない • モジュールをあまり使わない • クラス内クラスを使う 71

  72. Concernsの影響範囲はとても広い app/models/concerns/authentication.rb はどこでincludeできる か? • モデルだけで使える • コントローラーでも使える • !

    実はどこからでも使える 72
  73. 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
  74. クラス内クラスに閉じ込める # app/models/user/ranking.rb class User class Ranking def initialize(user) @user

    = user end end def rank # ུ end private def calculate # ུ end end 74
  75. 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
  76. おわり ご静聴ありがとうございました 76