Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

自己紹介 • 名前: 神速 • 会社: メドピア株式会社 • GitHub: @sinsoku (画像右上) • Twitter: @sinsoku_listy (画像右下) • Rails歴: 約7年 2

Slide 3

Slide 3 text

話すこと 1. 「コードの読みやすさ」について 2. 設計するときの流れ 3. 実装で気にしてること 日常の開発で考えていること、思考過程を紹介します。 3

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

Railsアプリの読みやすさ • Railsの哲学 • 同じことを繰り返さない(DRY:Don't Repeat Yourself) • 設定より規約(CoC:Convention over Configuration) • 「The Rails Way」「Rails流」 よく聞く単語たち。 5

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

現場で使える Ruby on Rails 5速習実践ガイド Railsはフレームワークなので、その上に 自分たちのコードを書くことで独自のア プリケーションを実現します。 自分たちのコードを書く際には、 Rails らしく考え、Railsらしいコードを書く という基本的な姿勢が大切になってきま す。 7

Slide 8

Slide 8 text

The Rails Doctrine1 1 https://postd.cc/rails-doctrine/ 8

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

The Rails Way なコード • ! 公式ドキュメントに記載あるコード • https://api.rubyonrails.org/ • https://railsguides.jp/ • " rails new と rails g で生成されるコード これらを読んで The Rails Way の全体像を推測してます。 11

Slide 12

Slide 12 text

RailsガイドとAPIドキュメントの抜粋 12

Slide 13

Slide 13 text

リソースベースのルーティング(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

Slide 14

Slide 14 text

Representational State Transfer (REST) 重要なのは以下の2つ。 • HTTPメソッドで 操作 を表現する • GET, POST, PATCH/PUT2, DELETE • URIで リソース を表現する 2 PATCH/PUTの違いは省略。気になる人はググってほしい。 14

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

ActiveRecord の命名ルール3 3 https://api.rubyonrails.org/classes/ActiveSupport/Testing/TimeHelpers.html#method-i-freeze_time 16

Slide 17

Slide 17 text

ActiveRecord のスキーマのルール 17

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

RailsガイドのActionView 21

Slide 22

Slide 22 text

メールの送信 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

Slide 23

Slide 23 text

rails new で生成されるコードの紹介 23

Slide 24

Slide 24 text

bin/setup リポジトリを git clone したあと、1コマンドで開発環境を整え られる。 $ bin/setup # "bundle install" ͱ "yarn install" ͰґଘύοέʔδͷΠϯετʔϧ # σʔλϕʔεͷ࡞੒, ...ͳͲ $ bin/rails server # αʔό͕ىಈ͠ɺ http://localhost:3000 Ͱը໘Λ֬ೝͰ͖Δ 24

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

開発環境のキャッシュの設定 キャッシュ設定を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

Slide 27

Slide 27 text

Scaffold コントローラーの紹介 27

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

Rails Wayとgem 31

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

Railsに機能が増えて gemが不要になるケース 33

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

ここまでのまとめ • 公式ドキュメントからThe Rails Wayを推測する • RESTなルーティング • シンプルなコントローラー • rails newの初期設定を尊重する • ! を入れすぎない 36

Slide 37

Slide 37 text

仕様 -> ? -> 読みやすいコード 37

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

! 要件定義 • 実装する機能の目的を聞き出す • HowをWhyに変える • その目的は コードを書かないで解決できないか • コードを書かずに解決するのが一番良い • コードを書かなければバグは起きない • エンジニアの人件費は高い " 41

Slide 42

Slide 42 text

! 「12月のDAUを増やしたい」 ! 「記事数を増やしたい」 42

Slide 43

Slide 43 text

! 要件定義 全体の仕様を最小の機能に分割し、依存関係を考える。 • 1 カレンダーを登録できる • 2 カレンダーに参加登録できる • 2 カレンダーに「いいね」できる • 2 新しいカレンダーを管理者にメール通知する 43

Slide 44

Slide 44 text

! 画面設計 • 画面イメージをしっかり描く • 表示する項目を洗い出す • 異常系を考えておく • エラーメッセージの表示位置 44

Slide 45

Slide 45 text

45

Slide 46

Slide 46 text

46

Slide 47

Slide 47 text

47

Slide 48

Slide 48 text

48

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

テーブル設計 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

Slide 55

Slide 55 text

実装イメージが頭の中でだいたい固まる 55

Slide 56

Slide 56 text

コントローラーの設計 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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

RESTなルーティングだとコント ローラーはシンプルになる 58

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

良い本はいくつもある 61

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

雑談: 複数テーブルを扱うモデルはどう書くか? • サービスクラス • フォームオブジェクト • ドメイン層? 64

Slide 65

Slide 65 text

最初はこれで済ませたりする 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

Slide 66

Slide 66 text

別クラスを作ったときの行数 > メソッド行数 常に短いコードを目指す 66

Slide 67

Slide 67 text

機能が増えたら別クラスにする # 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

Slide 68

Slide 68 text

app下にディレクトリを増やさない • ビジネスロジックを良い感じにする Interactor • View層のロジックをまとめる Draper, ActiveDecorator • 権限管理 CanCanCan, Pundit, Banken Models, Views, Controllers の3つでも複雑になるの に、この辺りの ! を使いこなせるとあまり思えない 68

Slide 69

Slide 69 text

話を戻す 69

Slide 70

Slide 70 text

基本的に RuboCop ! の標準ルールで書く • RuboCop の指摘は警告(エラーではない) • Layout/LineLength 80〜: 変数抽出を検討する • Metrics/AbcSize 15〜: リファクタを検討する • 他メンバーを説得できる理由があれば無効にする • ! 開発メンバーが日本人のみなので Style/AsciiComments を無効 70

Slide 71

Slide 71 text

メソッドの影響範囲を分かりやすくする • privateメソッドを活用する • Concernsを安易に使わない • モジュールをあまり使わない • クラス内クラスを使う 71

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

クラス内クラスに閉じ込める # 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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

おわり ご静聴ありがとうございました 76