Quantcast
Channel: Ruby on Railsの記事一覧|TechRacho by BPS株式会社
Viewing all 1381 articles
Browse latest View live

Rails tips: RSpecのテスト設計でよくあるやらかし4種(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails tips: RSpecのテスト設計でよくあるやらかし4種(翻訳)

私はテストを書くのがとっても好きなのですが、「ヤバいテストを書くぐらいならテストがない方がまし」な状況もあります。テストがなければ全部手動でテストするしかないという覚悟もできようというものですが、ヤバいテストがあるばかりに、自分自身はもちろん関係者まで「問題なし」と誤認してしまいます。

当てにしてはならないヤバいテストかどうかをどうやって突き止めればよいのでしょうか。テスト作成時にやらかしてしまいがちな過ちをいくつかご紹介しましょう。

シンプルなクラスとヤバいテストを1つずつ書いて分析してみることにします。

module Users
  class NameService
    def initialize(user)
      @user = user
    end

    def name
      if user.name.present?
        user.name
      else
        ::ExternalAPI.new(user).get_name
      end
    end

    private
    attr_reader :user
  end
end

ユーザー名がある場合はそれを返し、ない場合は何らかの外部APIを呼び出してオンラインプロファイルからユーザー名を取得するクラスです。続いて、残念きわまるテストを書きましょう。

require 'spec_helper'

describe Users::NameService do
  describe '#name' do
    it 'nameを返す' do
      user = User.create!(name: 'Mike Black')
      service = described_class.new(user)

      expect(service.name).to eq(user.name)
    end

    it 'nameを返す' do
      user = User.create!(name: nil)
      service = described_class.new(user)

      expect(service.name).to eq('Mike Jr Black')
    end
  end
end

さて、このテストのどこがマズいかおわかりでしょうか?

1. ドキュメントの説明がまったく舌足らず

--format documentationフラグを付けてspecを実行してみればわかりますが、これではテスト対象のメソッドが何をするのかさっぱりわかりません。知りたかったらクラスやテストコードを開くしかないでしょう。「nameを返す」ほほう、だから何?他の情報が何ひとつありません。テストはアプリにおける極めて重要な情報源なのですから、ペーペーの新人だろうと熟練開発者だろうと、exampleには必ずちゃんと意味の取れることを書きましょう。それでは修正してみます。

describe Users::NameService do
  describe '#name' do
    it 'ユーザーに名前が1つある場合はユーザー名を返す' do
      user = User.create!(name: 'Mike Black')
      service = described_class.new(user)

      expect(service.name).to eq(user.name)
    end

    it 'ユーザーが名前を持ってない場合はAPIから取ったユーザー名を返す' do
      user = User.create!(name: nil)
      service = described_class.new(user)

      expect(service.name).to eq('Mike Jr Black')
    end
  end
end

違いがどれほど大きいかおわかりでしょうか?これなら--format documentationを付けてspecを実行したときでもいちいちコードを見に行って内容を確認せずに済みます。

2. テストが外部とべったり癒着している

ここでテストしたいのはネームサービスなのに、サービスの外にあるExternalAPI#get_nameクラス内のコードまで呼び出しています。これは単体テストとしておかしいので、コードをもっと細かく分離し(メソッド化するのが普通)、それだけをテストしたいと思います。変更によって他の部分に一切影響を与えたくありません。たとえばクラスは問題なく動作しているがExternalAPI#get_nameの部分でエラーが起きたという状況を考えてみましょう。ここでテストが失敗すると、テストを2つも修正しなければならないでしょう(Users::NameService#nameExternalAPI#get_name)。これを避けるためにはスタブを使います。

describe Users::NameService do
  describe '#name' do
    it 'ユーザーに名前が1つある場合はユーザー名を返す' do
      user = User.create!(name: 'Mike Black')
      service = described_class.new(user)

      expect(service.name).to eq(user.name)
    end

    it 'ユーザーが名前を持ってない場合はAPIから取ったユーザー名を返す' do
      user = User.create!(name: nil)
      external_api = instance_double(ExternalAPI, get_name: 'Mike Jr Black')
      allow(ExternalAPI).to receive(:new).with(user).and_return(external_api)

      service = described_class.new(user)

      expect(service.name).to eq('Mike Jr Black')
    end
  end
end

これでテストが分離され、しかも前よりずっと高速になりました。たとえAPIクラス内でエラーが発生したとしても、実装は正しいのですからテストは失敗しません。

3. 無意味にデータベースを叩いている

このテストでデータベースを実際に叩く必要はありません。今度もスタブを使ってテストをスピードアップしましょう。

describe Users::NameService do
  describe '#name' do
    it 'ユーザーに名前が1つある場合はユーザー名を返す' do
      user = instance_double(User, name: 'Mike Black')
      service = described_class.new(user)

      expect(service.name).to eq(user.name)
    end

    it 'ユーザーが名前を持ってない場合はAPIから取ったユーザー名を返す' do
      user = instance_double(User, name: nil)
      external_api = instance_double(ExternalAPI, get_name: 'Mike Jr Black')
      allow(ExternalAPI).to receive(:new).with(user).and_return(external_api)

      service = described_class.new(user)

      expect(service.name).to eq('Mike Jr Black')
    end
  end
end

これでデータベースや外部サービスを叩かなくなりました。テストは分離され、しかもものすごく速くなりました。もちろんほかにもリファクタリングしてUserモデルインスタンスのスタブを別メソッドに切り出すことも可能は可能ですが、本記事の例では不要です(し、その方がよいのはもちろんです)。

4. privateメソッドをわざわざテストしている

これについてはコード例までは載せませんでしたが、これもよくあるやらかしです。publicメソッドは既にテストしていますし、その結果はprivateメソッドから生み出されたのですから、privateメソッドをテストする必要などありません。どうしてもprivateメソッドをテストしたいというのであれば、まずコードの方をもっと小さなクラスに分割してから、それらを個別にテストすることを検討しましょう。このような事態を避ける最良の方法は、メソッドを設計するときに「単一責任の原則」を用いることです。

お知らせ: RSpec & TDDの電子書籍を無料でダウンロード

もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。

関連記事

Railsで学ぶSOLID(1): 単一責任の原則(翻訳)


週刊Railsウォッチ(20180420)NGINX Unit正式リリース、Rubyをメモリリークさせてみる、美しいコード宇宙ほか

$
0
0

こんにちは、hachi8833です。記事を絞ろうと気張るのを諦めて単に絞ることにしました。

昨日のつっつきは、BPSの福岡拠点であるウイングドアの皆さまとZoomで画面共有しながら行いました。おかげさまでさらに盛り上がりました。ありがとうございます。

ところが何ということでしょう、録音を忘れていた私でした…😢今週のつっつき成分は少なめです。

というわけで、季節の変わり目のウォッチ、いってみましょう。

各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ

⚓Rails: 今週の改修

Rails 6になるmasterブランチから見繕いました。

先週取り上げたようなsplat外し的修正が目立ちます。86cafe7なんかもそうですね。

⚓RedisCacheStore#delete_matchedでブロックが発生してタイムアウトしていたのを修正

# commitより
 def delete_matched(matcher, options = nil) 
   instrument :delete_matched, matcher do 
     case matcher 
     when String 
       redis.with { |c| c.eval DELETE_GLOB_LUA, [], [namespace_key(matcher, options)] } 
     else 
       raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}" 
     end 
   end 
 end 

つっつきボイス: 「Lua言語が使われてる部分があったんだ…」

参考: Lua - Wikipedia

⚓javascript_include_tagヘルパーにnonce: trueオプションを追加

# actionview/lib/action_view/helpers/asset_tag_helper.rb#L
       def javascript_include_tag(*sources)
         options = sources.extract_options!.stringify_keys
         path_options = options.extract!("protocol", "extname", "host", "skip_pipeline").symbolize_keys
@@ -90,6 +95,9 @@ def javascript_include_tag(*sources)
           tag_options = {
             "src" => href
           }.merge!(options)
+          if tag_options["nonce"] == true
+            tag_options["nonce"] = content_security_policy_nonce
+          end
           content_tag("script".freeze, "", tag_options)
         }.join("\n").html_safe

つっつきボイス:nonceって前にもつっつきで扱った気が」「number used onceのことだったんかー↓」「同じようなCSP属性があったけど何だったかな」「ところで今ぐぐって見つけていただいたreference.hyper-text.orgってちょっといい感じのリファレンスサイトですね」

‘nonce-
nonce 属性は、CSP(Content Security Policy)によって、文書内に読み込まれた script 要素や、style 要素の内容を実行するかを決定するために利用される nonce(number used once / ワンタイムトークン) を指定します。
reference.hyper-text.orgより

参考: nonce 属性 | HTML5 タグリファレンス | W3 Watch Reference

参考: CSP: script-src - HTTP | MDN

⚓Content Security Policyでリクエストごとのnonceを1つだけにする

これもnonceですね。

# actionpack/lib/action_dispatch/http/content_security_policy.rb#L232
-      def build_directives(context)
+      def build_directives(context, nonce)
         @directives.map do |directive, sources|
           if sources.is_a?(Array)
             "#{directive} #{build_directive(sources, context).join(' ')}"
+            if nonce && nonce_directive?(directive)
+              "#{directive} #{build_directive(sources, context).join(' ')} 'nonce-#{nonce}'"
+            else
+              "#{directive} #{build_directive(sources, context).join(' ')}"
+            end
           elsif sources
             directive
           else
...

⚓APIドキュメントの修正3種

# activemodel/lib/active_model/validations/inclusion.rb#L21
       #   class Person < ActiveRecord::Base
-      #     validates_inclusion_of :gender, in: %w( m f )
+      #     validates_inclusion_of :role, in: %w( admin contributor )
       #     validates_inclusion_of :age, in: 0..99
       #     validates_inclusion_of :format, in: %w( jpg gif png ), message: "extension %{value} is not included in the list"
       #     validates_inclusion_of :states, in: ->(person) { STATES[person.country] }

今どきgenderが男と女だけというのは古臭い感じかもですね。
そういえばだいぶ昔は「クライアント/サーバー」と言わずに「マスター/スレーブ」って言ってたんですよね。「主人と奴隷」はさすがに200年遡る感じです。


つっつきボイス: 「へー、LGBTコンシャスというか」「そういえばこの間はてブであがってたISO 5218が、まさにこうした用途のための国際規格ですよ」「知らなかったー!」

  • 0 = not known(不明)
  • 1 = male(男性)
  • 2 = female(女性)
  • 9 = not applicable(適用不能)

ブクマにyuguiさんのコメントを見つけました:

システムで「性別」の情報を扱う前に知っておくべきこと – Qiita

「入力して何の役に立つの?」という最重要ポイントを読み落とさないでほしい。医療かマーケか戸籍処理か本人確認か、用途に応じて入力させるべきかも提示選択肢も入力欄ラベルも自ずと定まり、難しいことはない

2018/04/13 06:55

参考: システムで「性別」の情報を扱う前に知っておくべきこと - Qiita — これ必読ですね

遠い昔、子どもの頃の遠足で訪れたお寺で他の子が「観音様って男?女?」と質問すると、お寺の管理人さんが「観音は 男にあらず 女にあらず」と迷いなく答えたのをふと思い出しました。典拠を探そうと思ってググると新興宗教系サイトがわらわら出てきた…


# actionview/lib/action_view/context.rb#L10
   # Action View contexts are supplied to Action Controller to render a template.
   # The default Action View context is ActionView::Base.
   #
-  # In order to work with ActionController, a Context must just include this module.
-  # The initialization of the variables used by the context (@output_buffer, @view_flow,
-  # and @virtual_path) is responsibility of the object that includes this module
-  # (although you can call _prepare_context defined below).
+  # In order to work with Action Controller, a Context must just include this
+  # module. The initialization of the variables used by the context
+  # (@output_buffer, @view_flow, and @virtual_path) is responsibility of the
+  # object that includes this module (although you can call _prepare_context
+  # defined below).

つっつきボイス: 「クラス名としてはActionControllerだけど名称としてはAction Controllerが正式ってことなんでしょうか?」「んー、というより冒頭にAction Viewって書いてあるから単にそれと書式を合わせたってことなんじゃ?」「あなるほど、ドキュメントのローカルレベルでの統一というか」

くっついてる方が検索しやすいかなとちょっぴり思ったりしました。


# guides/source/_welcome.html.erb#L18
 <% end %>
 <p>
 The guides for earlier releases:
+<a href="http://guides.rubyonrails.org/v5.2/">Rails 5.2</a>,
 <a href="http://guides.rubyonrails.org/v5.1/">Rails 5.1</a>,
 <a href="http://guides.rubyonrails.org/v5.0/">Rails 5.0</a>,
 <a href="http://guides.rubyonrails.org/v4.2/">Rails 4.2</a>,

つまりRuby on Rails 5.2 Release Notesが正式になったということですね。Railsガイドの翻訳の優先順位上げます。ご期待ください。


railsguides.jpより

⚓サーバーIP指定時のHOST環境変数のサポート廃止

# 
-            ENV.fetch("HOST", default_host)
+
+            if ENV["HOST"] && !ENV["BINDING"]
+              ActiveSupport::Deprecation.warn(<<-MSG.squish)
+                Using the `HOST` environment to specify the IP is deprecated and will be removed in Rails 6.1.
+                Please use `BINDING` environment instead.
+              MSG
+
+              return ENV["HOST"]
+            end
+
+            ENV.fetch("BINDING", default_host)

SuSeでは$HOSTがデフォルトで設定され、$HOSTNAMEと等しくなっている。
https://www.suse.com/documentation/sled11/book_sle_admin/data/sec_adm_variables.html
従って、デフォルトではlocalhostではなくhostnameにバインドされる。これはデフォルトの振る舞いとしてはふさわしくないように思える。
この環境変数名の利用を避けるために、環境変数をHOSTからBINDINGに変更する。
同コミットより


つっつきボイス: 「お、これ大事な修正ですね: Rails 6.1からはHOSTS環境変数のサポートが削除されるのでBINDING環境変数を使うように、と」

「話逸れますが(逸れてばっかり)、昔いた会社のヨーロッパ系社員がSuSeを「ズーゼ」ってドイツ語っぽく発音してて、最初何のこと?って思っちゃいました」「へー、SuSe Linuxって日本ではなぜかそれほど知名度ないけど、ヨーロッパでは長い間ずっと主流ですね」「シンボルカラーが緑色だったのだけ覚えてます: 今もそうなのかな?」


suse.comより

⚓storageフォルダに.keepファイルを追加する条件を修正

# railties/lib/rails/generators/rails/app/templates/gitignore.tt
 <% unless skip_active_storage? -%>
 # Ignore uploaded files in development
 /storage/*
-
+<% if keeps? -%>
+!/storage/.keep
 <% end -%>
+<% end -%>
+
 <% unless options.skip_yarn? -%>
 /node_modules
 /yarn-error.log

最初にRailsを触ったときに、空のフォルダに入っている.keepって何だろう?と思ったのを思い出しました。


つっつきボイス: 「ははぁ、.gitignore/storage/.keep登録しちゃってたのか: そりゃ直さなきゃ!」「空ディレクトリにファイルが何もないとディレクトリがGitに登録されないんで、.keepが置かれてるんですね」「そうそう、前に教えてもらって知りました」

⚓Rails

⚓Action Policy: Ruby/Rails向けauthorizationフレームワーク


元記事より

authorizationの訳語でいつも悩んでます。「認証(authentication)」と対にして「認可」「承認」とされることが多いのですが、認可でも何かこうしっくりこない…リソース管理とかリソースのアクセス権の扱いなんかがauthorizationなんですよね。


つっつきボイス: 「これもEvil Martiansさんがスポンサーになってる: Evil MartiansさんのブログはTechRachoの翻訳記事にときどき使わせてもらってるんですが、こうやってちょっと先を行くようなgemのスポンサーになっているのをちょくちょく見かけるので」「action_policy gemはEvil Martiansのリポジトリとは別の場所なのか: それにしてもevil-seedとかevil-clientとか、RailsのGemfileとかで見かけたらドキーッっとしそうw」「徹底しているというか、旗色は鮮明ですね」

# 同リポジトリより
class PostPolicy < ApplicationPolicy
  # everyone can see any post
  def show?
    true
  end

  def update?
    # `user` is a performing subject,
    # `record` is a target object (post we want to update)
    user.admin? || (user.id == record.user_id)
  end
end

「で、このaction_policyはコントローラ側に寄せた設計のようだ」「以前から悩んでるところなんだけど、authorizationってPundit式にモデルに寄せるべきか、cancancan式にコントローラというかURL側に寄せるべきか、どっちだと思う?」「そうなんすよねぇ…モデル側の方がライブラリとしておさまりはいいのかなと思ったりもしますが、複雑になってくるとつらくなってくるし、果たしてモデルの責務なんだろうかという疑問も残るし」「自分は何となくだけど、コントローラ側の方が自然な気がするんだけどなー」「cancancanは機能が多すぎて逆にツライ: コントローラでauthorize!ってできればもうそれでいいのっ!w」「住処が定まらない感じですね」

新しいRailsフロントエンド開発(1)Asset PipelineからWebpackへ(翻訳)

⚓NGINX Unit正式リリース

新しいRubyアプリサーバー「NGINX Unit」を調べてみた(翻訳)


つっつきボイス: 「どうも思い違いされていることが多いみたいなんですが、NGINX UnitはNGINXとは別物なんですよねー」「どちらもNGINX社の製品なのにそうなんですね: 紛らわしい…」



nginx.comより

⚓社内Slackより: 悪例サンプルコードづくりは難しい

SOLID Principles #4 - Interface Segregation Principle  | Netguru Blog on Ruby/Ruby on Railsを翻訳したときに、サンプルコードについて社内Slackでかなり議論になりました。
私も訳していて、原著者が非常に苦しみながら書いていることを痛感しました。

# 同記事の悪例コード
class Computer 
  def turn_on; end
  def type; end
  def replace_cpu; end
  def change_user_password; end
end

class ComputerUser
  def initialize
    @computer = Computer.new
  end
end

class Programmer < ComputerUser
  def write_code
    @computer.turn_on
    @computer.type
  end
end

class Administrator < ComputerUser
  def update_user
    @computer.turn_on
    @computer.change_user_password
  end
end

class Technician < ComputerUser
  def fix_computer
    @computer.replace_cpu
    @computer.turn_on
  end
end

記事として練り直しを考えているところです。Slackでの指摘をまとめると以下のような感じでした。

  • サンプルコードの修正前と修正後の差が大きすぎて、ISPにフォーカスしにくい
  • そもそも修正前のコードがいろいろひどすぎて、そっちに気を取られてしまう
  • 可能なら修正したいところだけど、 ユースケースが見えない

推測ですが、教科書にありがちな哺乳類だの何だのという浮世離れしたクラスではなく、何とかして現実的なクラスにしようとしたように思えます。

ISPを説明するのに向いたサンプルコードがあれば著者もぐっと楽になりそうです。手頃な素材がどこかにないものでしょうか。

⚓RailsでのExplicit Contract

# 同記事より
class StudentsController < ApplicationController

  # ...

  def pull
    pulled_student_list = third_party_contract.fetch_students['students']

    pulled_student_list.each do |pulled_student|
      student = Student.new
      student.name = pulled_student['name']
      student.age = pulled_student['age']
      student.save!
    end
  end

  private

  def third_party_contract
    Rails.configuration.x.third_party_contract
  end
end

つっつきボイス: 「third party contract? どうやら法律用語から来てるっぽい?わからんけど」「httpartyっていうHTTPクライアントgem使ってますね: ★5,000近い」

# 同リポジトリより
# Use the class methods to get down to business quickly
response = HTTParty.get('http://api.stackexchange.com/2.2/questions?site=stackoverflow')

puts response.body, response.code, response.message, response.headers.inspect

# Or wrap things up in your own class
class StackExchange
  include HTTParty
  base_uri 'api.stackexchange.com'

  def initialize(service, page)
    @options = { query: { site: service, page: page } }
  end

  def questions
    self.class.get("/2.2/questions", @options)
  end

  def users
    self.class.get("/2.2/users", @options)
  end
end

stack_exchange = StackExchange.new("stackoverflow", 1)
puts stack_exchange.questions
puts stack_exchange.users

「httpartyで思い出したけど、herっていうgemもなかなかよくできてますヨ(前にも話したけど)」

# 同リポジトリより
User.all
# GET "https://api.example.com/users" and return an array of User objects

User.find(1)
# GET "https://api.example.com/users/1" and return a User object

@user = User.create(fullname: "Tobias Fünke")
# POST "https://api.example.com/users" with `fullname=Tobias+Fünke` and return the saved User object

@user = User.new(fullname: "Tobias Fünke")
@user.occupation = "actor"
@user.save
# POST "https://api.example.com/users" with `fullname=Tobias+Fünke&occupation=actor` and return the saved User object

@user = User.find(1)
@user.fullname = "Lindsay Fünke"
@user.save
# PUT "https://api.example.com/users/1" with `fullname=Lindsay+Fünke` and return the updated User object

⚓ChromebookでRailsが動く?

参考: ChromebookのFile Systemへの書込み制限を解除する方法 ― Write-Protect-Screwの外し方 C300MA編 | Chrome速報

Chromebrew(crewコマンド)というのを入れると様々なシステムを簡単にインストールできる。EmacsもNodeも簡単にインストールできた。Sinatraも使えたのでWebアプリ開発もできてしまう。頑張ればRailsも動きそうである。(crewだとコケたが個別インストールすれば大丈夫そう)


つっつきボイス: 「ポイントは、Chromebookのフタを外してプロテクトスイッチを解除することみたいです」「DockerがChromebookで動けばいいんじゃね?そしたらもう何でも動くよ♥

⚓その他記事など


つっつきボイス:AssetSyncはAWSのS3と同期するgemだけど、Azureにも対応したのか」

参考: Microsoft Azure Cloud Computing Platform & Services

⚓Ruby trunkより

⚓IRBのパーサーをRipperにした


同パッチより

⚓===match?より遅い

# 同issueより
# ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin17]

require 'benchmark/ips'

def run_benchmark(title, **benchmarks)
  puts '', title, '=' * title.size, ''

  # Validation
  benchmarks.each do |benchmark_name, benchmark_fn|
    puts "#{benchmark_name} result: #{benchmark_fn.call()}"
  end

  puts

  Benchmark.ips do |bm|
    benchmarks.each do |benchmark_name, benchmark_fn|
      bm.report(benchmark_name, &benchmark_fn)
    end

    bm.compare!
  end
end

regex = /foo/
# => /foo/
string = 'foobarbaz'
# => "foobarbaz"

run_benchmark('=== vs match? - 2.5.1',
  '===':    -> { regex === string },
  'match?': -> { regex.match? string }
)

=== vs match? - 2.5.1
=====================

=== result: true
match? result: true

Warming up --------------------------------------
                 ===   173.435k i/100ms
              match?   233.124k i/100ms
Calculating -------------------------------------
                 ===      3.174M (± 1.6%) i/s -     15.956M in   5.029027s
              match?      5.626M (± 2.5%) i/s -     28.208M in   5.016991s

Comparison:
              match?:  5626170.1 i/s
                 ===:  3173659.6 i/s - 1.77x  slower

つっつきボイス: 「まず===match?って同一なんだっけ違うんだっけ?」「ほむほむ、match?は位置を指定できるのと、例の特殊変数を更新しないところが違う、と↓」「特殊変数を更新してたらその分遅くなりそうですね」「$~とか使うとRubyがPerlチックになるんで好きじゃないなー」「私も」

  • Regexp#===: 文字列 string との正規表現マッチを行います。 マッチした場合は真を返します。string が文字列でもシンボルでもない場合には false を返します。このメソッドは主にcase文での比較に用いられます。
  • Regexp#match?: 指定された文字列 str に対して 位置 pos から自身が表す正規表現によるマッチングを行います。 マッチした場合 true を返し、マッチしない場合には false を返します。また、$~ などパターンマッチに関する組み込み変数の値は変更されません
    Rubyリファレンスマニュアルより

[Ruby] Kernelの特殊変数をできるだけ$記号なしで書いてみる

⚓Windowsで削除の制御文字が期待どおりに動かない

ruby -e 'puts "Hello world!\e[D\e[D\e[K\nWhat is up?"'
ruby -e 'puts "Hello world!\e[D\e[D\e[0K\nWhat is up?"'
ruby -e 'puts "Hello world!\e[D\e[D\e[1K\nWhat is up?"'

# 期待
Hello worl
Whats up?
Hello worl
Whats up?
           !
Whats up?

# 実際
          d!
What is up?
          d!
What is up?
          d!
What is up?

つっつきボイス: 「普段Web開発しているとほとんどやらないけど、Windowsの生の環境でRubyを使ってる人って、実は結構いるんじゃないかしら: 社内業務改善の手作りツールとかで」「それ私マジでやってました: Excelがバージョンアップするたびに手作りVBAが動かなくなるのが嫌になって、ExcelをRubyで制御する拡張をインストールして動かしたのがRubyとの最初の出会い」「WindowsユーザーのサポートはRubyのありがたい点」

どうやったかもう思い出せませんが、require 'win32ole'的にやったのかもしれません。確かRubyのバージョンは1.9だったはず。

参考: Rubyist Magazine - VBA より便利で手軽 Excel 操作スクリプト言語「Ruby」へのお誘い (前編)

⚓rangeの終わりを「無指定」にできるようになる

今日見つけたツイートです。

ary[1..]   # と同等[1..-1]

その後#14700で早速バグ上がってました。

⚓Ruby

⚓Rubyでメモリーリークを引き起こす方法(Ruby Weeklyより)

# 同記事より
a = []
b = {}

loop {
  sleep(1)

  10_000.times { a << "abc" }

  puts GC.stat(b)[:heap_live_slots]
}

つっつきボイス: 「おー、漏れてる漏れてる」「sleep(1)って必要なんだろうか?」「単に見やすくしてるんでしょうかね」「C拡張がリークするのは不思議でも何でもないw」「heap_live_slotsか…情報どこかにあるか?」「つ↓」

参考: Understanding Ruby GC through GC.stat

⚓型あり言語からRubyに来た人の心理学

2004年なのですごく古いですが、どうやら有名みたいです。

# 同書き込みより
# こうなりがち
   def date=(val)
     class="keyword">case val
     when Date
       @date = val
     when Time
       @date = Date.new(val.year, val.month, val.day)
     when String
       if val =~ /(\d{4})\s*[-\/\\]\s*(\d{1,2})\s*[-\/\\]\s*(\d{1,2})/
         @date = Date.new($1.to_i,$2.to_i,$3.to_i)
       else
         raise ArgumentError, "Unable to parse #{val} as date"
       end
     when Array
       if val.length == 3
         @date = Date.new(val[0], val[1], val[2])
       end
     else
       raise ArgumentError, "Unable to parse #{val} as date"
     end
   end

そして筆者の考えるダックタイピングの真髄はこうだそうです。

# year、month、dayメソッドに応答するオブジェクトを取る
attr_accessor :date

つっつきボイス: 「クラスチェックしたくてcaseで分岐しちゃうやつねー」「でもActiveSupportとかでこういう分岐、普通に見かけますよw」「どっひゃー」「if val行の正規表現はつらそう…」

参考: 静的型と OO というものははじめから… - Oh, you `re no (fun _ → more) — ここから辿りました

⚓「ゴッドクラス」というアンチパターン

Gobyのst0012さんに教わって知りました。

巷では、OOPでは大量のメソッドを保有する万能クラス (ゴッドクラス) を作ってはいけないらしい。また、1メソッドあたり3行程度であるべきで、長いメソッドを書くのはオブジェクト指向的ではないらしい。そういうコードを書くやつはオブジェクト指向がわかっていないのだそうだ。
Qiita記事より


つっつきボイス: 「↑1メソッドあたり3行!来たなー」「引数縦に並べるとそれだけで3行終わっちゃいます」「自分も5行まで!なんて言ったりするけど基本ジョークジョークw」「20行までなら許して欲しい」「50行は多いかな?」「Rubocopのデフォルトって何行まで許されるんでしたっけ?」「コメント除いて10行みたいです」

【保存版】Rubyスタイルガイド(日本語・解説付き)総もくじ

⚓🌟Code Galaxies Visualization🌟


同サイトより

Ruby、もちろんあります。美しすぎる。Go Conference 2018で知りました。


つっつきボイス: 「HomebrewやDebianパッケージまであるし」



同サイトより

宇宙船操作マニュアルとか気分出ますね(Hキーで表示できます)。
今週の🌟を贈呈します。おめでとうございます。

⚓rdl: Rubyで型チェック

# 同リポジトリより
file.rb:
  require 'rdl'
  extend RDL::Annotate

  type '(Integer) -> Integer', typecheck: :now
  def id(x)
    "forty-two"
  end

つっつきボイス:->こういう型宣言する関数型言語、あった気がする」「Ruby 3.0だとコメントに記述する形(アノテーション)で型チェックが入りそうなんでしたっけ」「dry-validationみたいなライブラリレベルの型チェックでいい気がする…」

# dry-rb.orgより
schema = Dry::Validation.Form do
  required(:name).filled
  required(:age).filled(:int?, gt?: 18)
end

schema.("name" => "Jane", "age" => "30").to_h
# => {name: "Jane", age: 30}

schema.("name" => "Jane", "age" => "17").messages
# => {:age=>["must be greater than 18"]}

Rails: dry-rbでForm Objectを作る(翻訳)

⚓その他記事など


つっつきボイス:redoって確かYARVがらみで見かけた気が」

参考: YARV: Yet another RubyVM / Instruction Table

⚓SQL

⚓PostgreSQLで大規模データを処理する(Postgres Weeklyより)


つっつきボイス: 「まだ中身読んでなかった」「ふむふむ、parallelismにパーティショニングにVACUUM FREEZEにFDW…、普通にぽすぐれででかいデータを扱う記事ですね」「(ぽすぐれで計算処理やらせるのかとおもた…)」

⚓PostgreSQL 11に入る「Covering Index」

-- 同記事より
=# CREATE TABLE new_example (a int, b int, c int);
CREATE TABLE
=# INSERT INTO new_example
     SELECT 3 * val, 3 * val + 1, 3 * val + 2
     FROM generate_series(0, 1000000) as val;
INSERT 0 1000001
=# CREATE UNIQUE INDEX new_unique_idx ON new_example(a, b)
     INCLUDE (c);
CREATE INDEX
=# VACUUM ANALYZE;
VACUUM
=#  EXPLAIN ANALYZE SELECT a, b, c FROM new_example WHERE a < 10000;
                       QUERY PLAN
-----------------------------------------------------
 Index Only Scan using new_unique_idx on new_example
     (cost=0.42..116.06 rows=3408 width=12)
     (actual time=0.085..2.348 rows=3334 loops=1)
   Index Cond: (a < 10000)
   Heap Fetches: 0
 Planning Time: 1.851 ms
 Execution Time: 2.840 ms
(5 rows)

つっつきボイス: 「え?Covering Indexってぽすぐれになかった?: MySQLにはずうっと前からあるのに」「ほんとだ、ググるとMySQLばっかり出てくる」

参考: インデックスのみのスキャン: テーブルアクセスを避ける

インデックスのはたらきによって、テーブルアクセスしなくても良かった場合のことを、 カバリングインデックスと言います。これは、インデックスの機能であるかのような響きがあるので、誤解されやすい用語です。インデックスのみのスキャン、という言い方の 方が、実行計画の操作であることを明白に示しています。
上記ブログより

つっつきでMERGE JOINの話にもなったのですが展開思い出せず…

⚓EdgeDBって何じゃろう

Rich Harrisさんって有名な人みたい。

⚓JavaScript

⚓async/awaitヘルからの脱出(JavaScript Weeklyより)

// 同記事より
async function selectPizza() {
  const pizzaData = await getPizzaData()    // async call
  const chosenPizza = choosePizza()    // sync call
  await addPizzaToCart(chosenPizza)    // async call
}

async function selectDrink() {
  const drinkData = await getDrinkData()    // async call
  const chosenDrink = chooseDrink()    // sync call
  await addDrinkToCart(chosenDrink)    // async call
}

(async () => {
  const pizzaPromise = selectPizza()
  const drinkPromise = selectDrink()
  await pizzaPromise
  await drinkPromise
  orderItems()    // async call
})()

// Although I prefer it this way 

(async () => {
  Promise.all([selectPizza(), selectDrink()]).then(orderItems)   // async call
})()

⚓TypeScriptの記法を理解する(JavaScript Weeklyより)

interface Array<T> {
  concat(...items: Array<T[] | T>): T[];
  reduce<U>(
    callback: (state: U, element: T, index: number, array: T[]) => U,
    firstState?: U): U;
  ···
}

つっつきボイス: 「TypeScriptはいいと思う: ただねー、環境の準備がめんどいw」「誰か準備してくれるならTypeScriptにしたい」「そういえばこの中にひとり、TypeScript勢いませんでしたっけ?」「ワイ?| ゜Θ゜)<そうでもないよ」

参考: TypeScript - JavaScript that scales.


typescriptlang.orgより

⚓これで全部終わらそうかという勢いのオンラインドキュメント: Front-End Developer Handbook 2018(JavaScript Weeklyより)


frontendmasters.comより

JS/CSS/HTML…と、新しいフロントエンド絡みのドキュメントを親の仇のように集約しています。


つっつきボイス: 「先週のrubyreferences.github.ioといい、このフォーマットのドキュメントテンプレ流行ってるのかなと思って」「というよりみんなGitHub Pages使ってるからじゃないすか?」「あそっか」「ともあれ、どこまできちんと書かれているかはちゃんと読むまでわからないですけどねw」


frontendmasters.comより

「お、とってもいい地図見つけた↑」「サイトの作りというか、フロントエンドはこれをこういう順に学べという道案内ですね」「同じページの『↓これは学ぶなリスト』にCoffeeScriptが…😢」「SASSも!?」

A short word of advice on learning. Learn the actual underlying technologies, before learning abstractions. Don’t learn jQuery, learn the DOM. Don’t learn SASS, learn CSS. Don’t learn HAML, learn HTML. Don’t learn CoffeeScript, learn JavaScript. Don’t learn Handlebars, learn JavaScript ES6 templates. Don’t just use Bootstrap, learn UI patterns.

「SASSじゃなくて素のCSSにしろと言われてもなー」「PostCSSあたりはどうだろう?」


github.com/postcssより

こちらにも星あげたい気持ちしてきました。

⚓CSS/HTML/フロントエンド

⚓Unicodeコンソーシアムの絵文字募集フォーム


unicode.orgより

Turing Complete FMの文字コードの回でも、絵文字の寿司にバリアントがいっぱいあるのにピザがそうじゃないのはどうよとか楽しい話があった気がします。


つっつきボイス: 「次は2020年に採択されるそうです」「文字だけのリストだとわかりにくいなー」「TechRachoのシンボルマークしれっと出したら案外通ったりして?」「(爆)」

Rubyの内部文字コードはUTF-8ではない…だと…?!

⚓出会えてうれしいCSSベスト3

Rubyのメソッドなんかでもこういうのやってみようかな。


つっつきボイス: 「flexboxが人気」「calcも多いですね」

⚓Service Workerとは


同記事より

⚓Go言語

⚓grapi: gRPC APIサーバー&ジェネレーター


つっつきボイス: 「Go Conference 2018でgRPCの登場率高かったです」「gRPCのようなミドルウェア的なものはGoが得意そうっすね」

⚓Go言語がWebAssemblyをサポートへ

Gobyちゃんがブラウザでするっと動く日が来るかなと思って。ちなみにこの記事の情報源は基本的に#18892でした。

⚓Goで学ぶTCP/IPスタック


つっつきボイス: 「そうそう、Goに限らずTCP/IPスタックのコードは一度は読んでおくと勉強になりますヨ」

// 同リポジトリより
// GetTCPProbe returns the TCPProbeFunc if installed with AddTCPProbe, nil
// otherwise.
func (s *Stack) GetTCPProbe() TCPProbeFunc {
    s.mu.Lock()
    p := s.tcpProbeFunc
    s.mu.Unlock()
    return p
}

// RemoveTCPProbe removes an installed TCP probe.
//
// NOTE: This only ensures that endpoints created after this call do not
// have a probe attached. Endpoints already created will continue to invoke
// TCP probe.
func (s *Stack) RemoveTCPProbe() {
    s.mu.Lock()
    s.tcpProbeFunc = nil
    s.mu.Unlock()
}

⚓Go格言集とGo Conference 2018

先週紛れ込んだGo Conference 2018で知りました。元記事には詳しい説明へのリンクがあります。雑に訳してみました。


  • メモリ共有で通信するのも、通信でメモリ共有するのもまかりならぬ
  • コンカレンシーは並列(parallelism)にあらず
  • チャンネルは協調動作(オーケストレーション)のためのもの、ミューテックスはシリアライズのためのもの
  • インターフェイスが肥大化するほど抽象化の度合いが落ちる
  • ゼロ値を有用なものにせよ
  • interface{}それ自身は何の情報ももたらさない
  • gofmtのスタイルを好きなやつなど誰もいない、だからこそgofmtは好かれる
  • 依存をわずかでもこしらえるくらいなら、コピーしてしまえ
  • syscallは常にビルドタグで保護しなければならぬ
  • cgoは常にビルドタグで保護しなければならぬ
  • cgoはGoにあらず
  • 安全でないパッケージを使ったが最後、何の保証もありはしない
  • 巧みなコードを書くより、明確なコードを書くがよい
  • リフレクションが明確になることなどありえぬ
  • エラーとは値のことであり、それ以外の何ものでもない
  • エラーをチェックするだけでは足りぬ、丁重に処理せよ
  • アーキテクチャを設計し、コンポーネントに命名し、詳細をドキュメントに書け
  • ドキュメンテーションはユーザーのためのものである
  • うろたえるな

こういうのって言語の思想を手っ取り早く知るのに便利ですね。

つっつきボイス: 「Goの格言、( ・∀・)イイ!!こういうの大事だし、”Concurrency is not parallelism.”とかごもっとも」

「ところでこのページのGopherくんの絵、このユルさがもうたまらないw」「この雑なライトセーバーみたいなやつ↓、測ってないけど45度としか思えないw」


go-proverbs.github.ioより


Go Conference 2018をすごーく手短にまとめると、以下が印象に残っています。多すぎてここには到底盛り込めないので。

  • Goのmap(Rubyで言うハッシュ)は本質的にスレッドセーフにならないし、しない
  • Go界隈ではマイクロサービスの普及がだいぶ進んでいる(少なくともRails Developers Meetupでの挙手数と比べれば)
  • 一方モノリスのよさも見直されつつあったりするらしい: GCPといえども下手にサービスを割るとオーバーヘッドにつながるとか
  • Goは公式資料がエクストリームに充実しているので、野良ブログより先にこちらを最初にあさるべし
  • GoでORMするならActiveRecordのことは忘れること — 「ActiveRecordの完成度はやっぱり凄い」という率直な感想も

参考: Go Conference 2018 Spring - 資料一覧 - connpass

開発チームを苦しめるマイクロサービス(翻訳)

⚓その他

⚓同人誌「現代Webフロントエンドデザインパターン」


つっつきボイス: 「BPSのアプリチームには技術書典の常連がいるらしいという噂」「どかっと積み上げてくれるのかな( ^ω^)ワクワク」

⚓大物の独自路線: Microsoftの独自LinuxカーネルとAmazonの独自ブラウザ


つっつきボイス: 「あーこれねw」「Amazonの独自ブラウザの名前がInternetって…」「誰に挑戦してるんだろうか」

参考: Amazonが容量わずか2MBの軽量版インターネットブラウザ、その名も「Internet」をリリース - GIGAZINE

⚓GCP BigQueryの東京リージョン開始


つっつきボイス: 「お、今まで東京リージョンなかったのか」

クラウド業界でAWSのサービス名の命名センスが他にも広がってるような気が個人的にしています。

⚓その他のその他



⚓番外

⚓英国BBCのGitHubリポジトリ


つっつきボイス: 「仕事中にたまたま見つけてくれた人がいたので」「本物なんですね」「BBCはこういうのをけっこうちゃんとやってますね」

↓動画は直接BBCとは関係ありませんが。

⚓手計算だったし

⚓そこまで精度上がったか

ビルの窓ガラスの振動を外からレーザー光線で測定して音声を盗聴するという技術を思い出しました。


つっつきボイス: 「おお、この手の技術は実は結構前からあるんですよ」「PLCって?」「まさに電力線搬送通信(Power Line Communication)で、屋内の電源コードを使ってデータ通信する技術」「へー!」「ちなみにPLCやるときはデータをPLC経由では屋外に出せないんですよ」「出せないっていうのは、法律的に?」「そう: 厳しく制限されている」「そっかー、日本は電波法厳しいし」

参考: 電力線搬送通信 - Wikipedia
参考: 電波法 - Wikipedia

⚓複合原子層作製システム


iis.u-tokyo.ac.jpより

参考: 東大、シート状の原子層を自在に積み重ねるロボットシステムを開発 | マイナビニュース

⚓日本沿岸で全世界数百年分のレアメタル鉱床が見つかる


つっつきボイス: 「どうやって掘り出すかが問題ですからねー」「何しろ海の底なんで」


今週は以上です。

バックナンバー(2018年度)

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやRSSなど)です。

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

Postgres Weekly

postgres_weekly_banner

Frontend Focus

frontendfocus_banner_captured

JavaScript Weekly

javascriptweekly_logo_captured

Rails tips: コントローラでrescueする方法(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails tips: コントローラでrescueする方法(翻訳)

Railsのコントローラでエラーをrescueしなければならないことがあります。クエリをかけたがデータベースにレコードがない場合にraiseされるActiveRecord::RecordNotFound例外からrescueする必要が生じることがほとんどでしょう。

サンプルのコントローラを作りましょう。

class UsersController < ApplicationController
  def show
    @user = User.find(params[:id])
  rescue ActiveRecord::RecordNotFound => e
    redirect_to :root, alert: 'User not found'
  end

  def edit
    @user = User.find(params[:id])
  rescue ActiveRecord::RecordNotFound => e
    redirect_to :root, alert: 'User not found'
  end
end

2つのメソッドがあり、どちらも同じ方法で例外を扱っています。このコードを使いまわせるように、ActiveSupportモジュールのrescue_fromを使う方法があります。

class UsersController < ApplicationController
  rescue_from ActiveRecord::RecordNotFound do |exception|
    redirect_to :root, alert: 'User not found'
  end

  def show
    @user = User.find(params[:id])
  end

  def edit
    @user = User.find(params[:id])
  end
end

before_actionフィルタを使ってshowメソッドやeditメソッドもリファクタリングできますが、それはまたの機会に。さて、これでコントローラのコードが読みやすくなり、同じコードを2回書く必要がなくなりました。いいですね!ブロックの代わりにメソッド名を渡すこともできます。

class UsersController < ApplicationController
  rescue_from ActiveRecord::RecordNotFound, with: :redirect_to_homepage

  def show
    @user = User.find(params[:id])
  end

  def edit
    @user = User.find(params[:id])
  end

  protected

  def redirect_to_homepage
    redirect_to :root, alert: 'User not found'
  end
end

concernsを使えば、1つのメソッドを複数のコントローラで使いまわすこともできるようになります。これは、受け取った例外からrescueするロジックに依存します。rescue_methodは、エラー時にしょぼいデフォルトのエラーページではなくもっとましなページを表示したい場合にとても便利です。

お知らせ: RSpec & TDDの電子書籍を無料でダウンロード

もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。

関連記事

リファクタリングRuby: サブクラスをレジストリに置き換える(翻訳)

Service Objectがアンチパターンである理由とよりよい代替手段(翻訳)

Rails tips: ストーリーのあるRSpecテストを書く(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails tips: ストーリーのあるRSpecテストを書く(翻訳)

これまでにRSpecのspyスタブについて学んできましたので、今回はもっと意味のあるspecを作ってみましょう。テストというと、「どう動いているか」をテストすると考えているかもしれません。しかしそのテストを集めてドキュメントをビルドするときのことについて考えていますか?別に余分なことをする必要はありませんが、しなければならないのは「ストーリーが見えるテストを書く」ことだけです。その方法についてご説明いたします。

ストーリーの骨格

よいストーリーにはしっかりした構造があり、だからこそ簡単に追うことができます。ストーリーを追うためにあちこち彷徨わなければならないとしたら、流れを見失ってしまうかもしれません。あなたがテストを読むときにまさにこれがしばしば起きているのです。

設計のまずいテストをちょっと見てみましょう。

require 'spec_helper'

describe NameService do
  let(:name_service) { described_class.new(user) }
  let(:user) { instance_double(User, full_name: 'Mike Willson') }
  let(:admin_policy) { instance_double(AdminPolicy, admin?: false) }

  before do
    allow(AdminPolicy).to receive(:new).with(user).and_return(admin_policy)
  end

  describe '#full_name' do
    subject { name_service.full_name }

    it 'ユーザーのフルネームを返す' do
      expect(subject).to eq(user.full_name)
    end

    context 'ユーザーがadminの場合' do
      before { allow(admin_policy).to receive(:admin?).and_return(true) }

      it 'フルネームの前に`ADMIN`を付けて返す' do
        expect(subject).to eq("ADMIN #{user.full_name}")
      end
    end
  end
end

ここからどんなストーリーが見えてきますか?下手な台本ですね。最初にPolicy Objectを宣言していますが、それがどう使われているのかがわかりません。こんな台本の導入部など読んでられないでしょうから、itタグに進みましょう。

itはたった1行の短いストーリーです。しかしクラスのコードを読んでおかなければ、しばらくの間目を凝らさないと何のメソッドをテストしているのかわかりません(クラスのコードは非常に短いのですが、ここではあえて掲載しませんでした)。

それではもっとましなストーリーを書いてみましょう。

require 'spec_helper'

describe NameService do
  describe '#full_name' do
    it 'ユーザーのフルネームを返す' do
      user = instance_double(User, full_name: 'Mike Willson')
      admin_policy = instance_double(AdminPolicy, admin?: false)
      allow(AdminPolicy).to receive(:new).with(user).and_return(admin_policy)

      name_service = NameService.new(user)
      full_name = user.full_name

      expect(admin_policy).to have_received(:admin?).once
      expect(full_name).to eq(user.full_name)
    end

    it 'ユーザーがadminの場合フルネームの前に`ADMIN`を付けて返す' do
      user = instance_double(User, full_name: 'Mike Willson')
      admin_policy = instance_double(AdminPolicy, admin?: true)
      allow(AdminPolicy).to receive(:new).with(user).and_return(admin_policy)

      name_service = NameService.new(user)
      full_name = user.full_name

      expect(admin_policy).to have_received(:admin?).once
      expect(full_name).to eq("ADMIN #{user.full_name}")
    end
  end
end

今度はずっと読みやすくてよいストーリーだと思いませんか?導入部を除くと2つの章があり(共通のコードを別メソッドに切り出してDRYにすることもできます)。あっちこっち飛び回らずに上から下まですっと読み下すことができ、しかもそれぞれのストーリーがちゃんと独立しています。

よいストーリーを書くには、よい作文スタイルを守らなければなりません。上の例の場合、それぞれのストーリーがセットアップ、操作、検証の3部構成になっています。

1. セットアップ

このサービスで使うクラスをスタブ化し、外部コードを呼び出したりしないようオブジェクトを準備します。テスト対象のクラスで使われるクラスやオブジェクトがどんな値を返すかもわかります。セットアップでは、スタブ化とコードの準備を行います。

2. 操作

ここは最もシンプルな部分です。クラスのインスタンスを作成して、テストしたいコードを呼ぶだけです。

3. 検証

最後の部分では、すべてうまく動いたかどうかをテストしなければなりません。正しい結果が返ったか、正しいクラスが呼び出されたか。

ただしここで忘れてはならないことがあります。上のような戦略は、テストを読みやすくメンテ可能にするためにletbeforeブロックを大量に使うことを強いられる大規模なテストには合いません。

お知らせ: RSpec & TDDの電子書籍を無料でダウンロード

もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。

関連記事

Railsのテスティングピラミッド(翻訳)

テストを不安定にする5つの残念な書き方(翻訳)

Rails tips: RSpecのスタブとモックの違い(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails tips: RSpecのスタブとモックの違い(翻訳)

本記事を読む前にスタブやモックの使い方も調べておきましょう。

構文の違い

唯一の違いは先頭のメソッドです。スタブではallowが先行し、モックではexpectが先行するでしょう。

利用法の違い

スタブは、他のメソッドが実行されないようにして欲しい値を返させるものです。モックは、指定のメソッドが実行されたかどうかを、引数や頻度を指定してチェックするものです。

コード例

例から学ぶのが一番ですので、ひとつコード例をご覧ください。クラスとそれに対応するテストを作ります。

class Message
  def initialize(user: user)
    @user = user
  end

  def send
    user.update_message_sent_at
    name = user.name(format: 'message')
    Emailer.send_message(name)
  end

  private
  attr_reader :user
end

3つのメソッドはテストで呼びたくありませんので、これらをスタブでふさぎ、正しく実行されたかどうかをモックでチェックします。

require 'spec_helper'

describe Message do
  describe '#send' do
    it 'sends message' do
      user = instance_double(User)
      name = 'Tim'
      allow(user).to receive(:name).with(format: 'message').and_return(name)
      allow(user).to receive(:update_message_sent_at)
      alloW(Emailer).to receive(:send_message).with(name)

      message = Message.new(user: user)
      message.send

      expect(user).to have_received(:update_message_sent_at).once
      expect(user).to have_received(:name).with(format: 'message').once
      expect(Emailer).to have_received(:send_message).with(name).once
    end
  end
end

これでおしまいです。スタブは元のコードを実行しないようにし、モックは指定のメソッドが実際に呼ばれたかどうかをチェックします。このようなテストの構造は「スパイ」と呼ばれます。スパイについてはこちらをご覧ください。

お知らせ: RSpec & TDDの電子書籍を無料でダウンロード

もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。

関連記事

ソフトウェアテストでstubを使うコストを考える(翻訳)

[Rails] RSpecのモックとスタブの使い方

Rails tips: 遅いクエリのログをDB設定変更なしで取るコツ(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails tips: 遅いクエリのログをDB設定変更なしで取るコツ(翻訳)

アプリの種類を問わず、遅いクエリのログを読むのはパフォーマンス向上のためのリファクタリングで弱い部分を発見するよい方法のひとつです。

以前の私は、遅いクエリのログというとデータベースエンジンで生成されたログのことが念頭にありました。しかし、データベースの遅いクエリのログを取る独自のメカニズムを作れることをご存知でしょうか?RailsのActiveSupport::Notificationsモジュールを使うと、指定のイベントをサブスクライブできます。イベントのひとつに sql.active_recordがあり、これはデータベース操作のたびにトリガされます。

次のような感じで使えます。

ActiveSupport::Notifications.subscribe('sql.active_record') do |name, start, finish, id, payload|
  # interesting stuff
end

以下の興味深い変数があります。

  • payload: クエリの詳細を含むハッシュで、payload[:sql]でクエリを取得できます
  • start: クエリ開始時のTimeオブジェクトです
  • finish: クエリ終了時のTimeオブジェクトです

次の方法で、クエリに要した時間を簡単に取得できます。

ActiveSupport::Notifications.subscribe('sql.active_record') do |name, start, finish, id, payload|
  duration = finish.to_f - start.to_f
end

3秒後に遅いクエリが開始するのであれば、次のようにしてクエリのログを取れます。

ActiveSupport::Notifications.subscribe('sql.active_record') do |name, start, finish, id, payload|
  duration = finish.to_f - start.to_f

  if duration >= 3.0
    SomeLogger.log("slow query detected: #{payload[:sql]}, duration: #{duration}")
  end
end

Railsアプリ用のイニシャライザを作成する

遅いクエリロガーの基本的な機能ができたので、実際にrailsプロジェクトに放り込んでサーバーで動かしてみましょう。SlowQueryLoggerクラスを作成し、config/initializer/slow_query_logger.rbに保存します。

class SlowQueryLogger
  MAX_DURATION = 3.0

  def self.initialize!
    ActiveSupport::Notifications.subscribe('sql.active_record') do |name, start, finish, id, payload|
      duration = finish.to_f - start.to_f

      if duration >= MAX_DURATION
        SomeLogger.log("slow query detected: #{payload[:sql]}, duration: #{duration}")
      end
    end
  end
end

SlowQueryLogger.initialize!

これで、遅いクエリのログが出力されます。このクラスはどのプロジェクトでも使える手頃な作りで、何となればgem化してクエリパフォーマンスを改善したい箇所の特定に役立てることもできます。警告をファイルに出力する代わりに、Rollbarなどの監視システムに送ることもできます。

お知らせ: RSpec & TDDの電子書籍を無料でダウンロード

もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。

関連記事

[Rails5] Active Supportの概要をつかむ

[Ruby] module_functionでモジュールの特異メソッドを簡潔に書く

Rails tips: アプリでOmniAuthによるLinkedIn認証を準備する(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails tips: アプリでOmniAuthによるLinkedIn認証を準備する(翻訳)

LinkedInの機能でアプリにサインインする機能を追加してみたいと思いませんか?適切なgemをインストールする前に、LinkedIn上に新しい「マイアプリ」を作成しておくべきです。この手順全体はとてもシンプルで、応募フォームに記入して、credentialを自分のアプリの設定ファイルにコピーすればおしまいです。

新しいアプリを作成するには、自分のアカウントでサインインする必要があります。既にサインインしている場合は、マイアプリを開いてください。

必要なフィールドに記入します。メールアドレス、電話番号、Webサイトのアドレスは検証されませんのでご心配なく。自分のアプリがまだ動いていなくても、まだ存在しないWebアドレスを入力できます。

フォームを送信すると、アプリの詳細が表示されます。Authentication KeysセクションにClient IDClient secretという2つのキーがあります。自分のアプリにLinkedInでサインインできるようにするためにこれらのキーが必要になります。手順はこれで完了です。

重要: LinkedInは、認証が完了したユーザー接続のフェッチを許可しなくなりました。API経由でさらに多くの機能を利用するには、LinkedInパートナーになる必要があります。詳しくはPartner Programs | LinkedIn Developer Networkをご覧ください。

お知らせ: RSpec & TDDの電子書籍を無料でダウンロード

もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。

関連記事

[Devise How-To] OmniAuth: 概要(翻訳)

週刊Railsウォッチ(20180427)Rails開発者アンケート2017結果発表、「次に学びたい言語」、各種カンファレンス目白押しほか

$
0
0

こんにちは、hachi8833です。最近はTechRachoが私の外部記憶装置と化しています。

ゴールデンウィーク特集号というつもりでもなかったのですが、またしても駆け込みでガンガン増やしてしまいました。この後もう少し足すかもしれません(懲りてない)。

ゴールデンウィーク前のウォッチ、さわやか気分でいってみましょう。次回のウォッチは5/11(金)の予定です。GW中日の5/1と5/2には何かポエム系記事書くかもしれません。

今回のつっつきも、BPSの福岡拠点であるウイングドアの皆さまと画面共有しながら行いました。いつもありがとうございます。今回はなぜかZoomのご機嫌がよろしくなかったので、appear.inに切り替えてしのぎました。


つっつきボイス: 「損切が大事」

各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ

⚓Rails: 今週の改修

公式情報はRailsConf 2018 Editionと銘打った特集を組んでいます。

#32612とか#32610とか#32582などは先週取り上げてしまったので、今週は少し趣を変えて、後述のRails 5.2リリースノートからこれまで取り上げていなかったものもピックアップしてみたいと思います。

⚓存在しないタイムゾーンを指定した場合のActiveSupport::Timezone.all例外を修正

# activesupport/lib/active_support/values/time_zone.rb#L280
         def zones_map
           @zones_map ||= MAPPING.each_with_object({}) do |(name, _), zones|
-            zones[name] = self[name]
+            timezone = self[name]
+            zones[name] = timezone if timezone
           end
         end

つっつきボイス: 「タイムゾーンかー」「たしか使っちゃだめなクラスありましたよね: DateTimeだったか」「以前もロシアかどっかでタイムゾーンの扱いが面倒とかいう話が出たような」「そうそう、以前のウォッチ↓でも扱いました: タイムゾーンのくせに飛び地になってたりとか」「同じ名前なのにタイムゾーンのオフセットが確定しないことがありそげ」

週刊Railsウォッチ(20171117)Rails開発3年分のコツ集大成、PostgreSQL 10.1でセキュリティ問題修正ほか

Rubyスタイルガイドを読む: 数値、文字列、日時(日付・時刻・時間)

「タイムゾーンが存在しない場合というか、存在しないタイムゾーンを指定した場合のエラーのようですね」「存在しないタイムゾーンを叩くとか、タイムゾーンを引数にとって動的挿入したとかぐらいでRailsでエラーがraiseされたらそれはヤダなー」「raiseするぐらいならしれっとデフォルトのタイムゾーン使って続行するぐらいの方がましかも」

⚓デフォルトオプションがなければ空の[]を渡すようにした

# actionview/lib/action_view/helpers/translation_helper.rb#L60
       def translate(key, options = {})
         options = options.dup
         has_default = options.has_key?(:default)
-        remaining_defaults = Array(options.delete(:default)).compact
+        if has_default
+          remaining_defaults = Array(options.delete(:default)).compact
+        else
+          remaining_defaults = []
+        end

元の英語がよれよれで、ソース見ないと意味わからなかった…(追伸: その後ちょい修正されたっぽいです)


つっつきボイス: 「これはソース見るほうが早いです」「テストコードないし…」「テストないと何を期待してるのかイマイチわからんなー」「まあそんな大きな影響があるとは思えない感じ」

⚓ロケールを指定したときの非可算名詞の複数形対応を修正

ここからはRails 5.2リリースノートより拾いました。

# activesupport/lib/active_support/inflector/methods.rb#L388
       # Applies inflection rules for +singularize+ and +pluralize+.
       #
-      #  apply_inflections('post', inflections.plurals)    # => "posts"
-      #  apply_inflections('posts', inflections.singulars) # => "post"
-      def apply_inflections(word, rules)
+      # If passed an optional +locale+ parameter, the uncountables will be
+      # found for that locale.
+      #
+      #  apply_inflections('post', inflections.plurals, :en)    # => "posts"
+      #  apply_inflections('posts', inflections.singulars, :en) # => "post"
+      def apply_inflections(word, rules, locale = :en)
         result = word.to_s.dup

-        if word.empty? || inflections.uncountables.uncountable?(result)
+        if word.empty? || inflections(locale).uncountables.uncountable?(result)
           result
         else
           rules.each { |(rule, replacement)| break if result.sub!(rule, replacement) }
# activesupport/test/inflector_test.rb#L437
+    assert_equal("agua", "agua".pluralize(:es))
+    assert_equal("aguas", "agua".pluralize)
+
     ActiveSupport::Inflector.inflections(:es) { |inflect| inflect.clear }

     assert ActiveSupport::Inflector.inflections(:es).plurals.empty?
     assert ActiveSupport::Inflector.inflections(:es).singulars.empty?
+    assert ActiveSupport::Inflector.inflections(:es).uncountables.empty?
     assert !ActiveSupport::Inflector.inflections.plurals.empty?
     assert !ActiveSupport::Inflector.inflections.singulars.empty?
+    assert !ActiveSupport::Inflector.inflections.uncountables.empty?
   end

スペイン語なんかのagua(水)はラテン語由来だったはずで、英語のaquaの語源ですね。以下はボサノバの定番曲「おいしい水」から。「アグァジュデベー」って聞こえます。

参考: おいしい水 - Wikipedia


つっつきボイス:pluralize、一発で発音できんわ: Railsで初めて覚えた単語」「これきっと造語だよな…と思ったら一応手元の串刺し検索辞書に載ってた: 英辞郎だけでしたがw」「”複数形にする”」「singulalizeがあるならきっとその逆があるってことぐらいは推測つくけど」「他で使い所なさそー: あ、でも英語圏の小学校とかでは普通に使うのかな」「それは間違いないはずです: 英語の単数複数ってすっごく重要なので」

「で、uncountable(非可算名詞)というあの英語的にくっそ面倒なやつに、さらにロケールごとに対応したということですね: furnitureとかequipmentとかinformationみたいなやつ」「ははー、同じ単語でも活用形とか単複同形とかがスペイン語とかドイツ語なんかの言語によって違ったりすることがあるってことか!」「Railsでこんなところまでサポートするのはもう果てしがないというか、途中で嫌になったりしないかな」「沼化してる」

「もうオレはdatasなんて見てもそんなにびっくりしないけどなっ」「自分が見た限りですが、実はそういう不規則活用って最近の英語圏でもどんどんグダグダになりつつありますね: mailって本来は集合名詞(sheepとかfishみたいに)なんですが、最近は中国人英語とかインド人英語が怒涛のように流入しているせいか、平気でmailsとか使う事例が山ほどあって、もうmailsありでいいんじゃね?って思ったり」

「確か社内GitLabのアクティビティログか何かだったんですが、metadataで、複数形がmetadatasに活用されてたという話、見ましたヨ」「(爆)」「それ英語的に判断難しいなー」「複合語だし」「metadatum、聞いたことも見たこともねーし」

  • datum / data
  • index / indices? indexes?
  • mail / mail? mails?
  • metadata? metadatum? / metadatas???

「そういえばinformationってs付けていいのかどうかいつも迷う」「基本、s付けないんですけどね」「indexが単数で、indicesが複数形」「お、それはデータベース方面でお馴染み: show indicesとかやりますね」

後で手元の串刺し辞書(Logophileというアプリで検索できるようにしてある)の英英辞典で確認すると、こうありました。

informationは非可算名詞としてしか使えない。”an information”も”informations”もアウト。特定の情報を指したい場合に、”a piece of information”や”an item of information”などと書くことならできる。

英語の数詞(a sheet of papersなど)はこれがまた面倒で、日本語の「一匹、一羽、一頭、一着、一反」の面倒くささに匹敵します。タンスは「一竿」って数えるんですって。

参考: 【常識?】知っておくとカッコいい!色々な物の数え方、単位【雑学】 - NAVER まとめ

⚓ActiveRecord::Calculationsに引数とブロックを同時に渡すことを非推奨化

kamipoさんです。

Relationselectcountsumは、ブロックを受け取れるEnumerableメソッドでもある。既に、ブロックを受け取るselect4fc3366以降引数を取らなくなったので、それに合わせた。

# activerecord/lib/active_record/relation/calculations.rb#L39
     def count(column_name = nil)
-      return super() if block_given?
+      if block_given?
+        unless column_name.nil?
+          ActiveSupport::Deprecation.warn \
+            "When `count' is called with a block, it ignores other arguments. " \
+            "This behavior is now deprecated and will result in an ArgumentError in Rails 5.3."
+        end
+
+        return super()
+      end
+
       calculate(:count, column_name)
     end

つっつきボイス: 「うん、これは正しいと思う: 引数とブロックのどっちも取れたりすると挙動予測するの難しくなるし」

⚓limitが絡むときのfirstlastの挙動を統一

# 同PRより
Topic.limit(1).first(2).size #=> 2
Topic.limit(1).last(2).size  #=> 1
# activerecord/lib/active_record/relation/finder_methods.rb#L532
         else
           relation = ordered_relation

-          if limit_value.nil? || index < limit_value
+          if limit_value
+            limit = [limit_value - index, limit].min
+          end
+
+          if limit > 0
             relation = relation.offset(offset_index + index) unless index.zero?
             relation.limit(limit).to_a
           else

つっつきボイス: 「ほほー、first()last()はRubyのメソッドで、引数で最初から何番目とか最後から何番目というのを指定できるやつ: secondなんかは確かRailsのメソッドだけど」

追伸: ActiveRecordに関しては、別定義みたいです。

参考: ActiveRecord::FinderMethods

「とはいうもののサンプルでlimit(1)してからfirst(2)する意味がわからない: 修正の方はminを取るようにして挙動を一貫させたということだけど、こんな書き方誰もしないっしょ」「まず誰も踏まなさそうですね」

⚓非推奨の:if/:unless文字列フィルタを削除

# activesupport/test/callbacks_test.rb#L1189
-      assert_deprecated { klass.before_save :tweedle, if: "true" }
-      assert_deprecated { klass.after_save :tweedle, unless: "false" }
-      assert_deprecated { klass.skip_callback :save, :before, :tweedle, if: "true" }
-      assert_deprecated { klass.skip_callback :save, :after, :tweedle, unless: "false" }
+      assert_raises(ArgumentError) { klass.before_save :tweedle, if: ["true"] }
+      assert_raises(ArgumentError) { klass.before_save :tweedle, if: "true" }
+      assert_raises(ArgumentError) { klass.after_save :tweedle, unless: "false" }
+      assert_raises(ArgumentError) { klass.skip_callback :save, :before, :tweedle, if: "true" }
+      assert_raises(ArgumentError) { klass.skip_callback :save, :after, :tweedle, unless: "false" }

つっつきボイス: 「文字列フィルタって何だったっけと思って」「そうそう、前はコールバックのif:とかunless:オプションに文字列でRubyのコードを何でも渡せちゃったんですよね」「何それ怖い」「それが非推奨になり、今回ついに消えたってことか: そういえばそういうの見たなどっかで」

「今後はどうするのかな?lambdaで渡す?」「本家PRをみると、どちらかというとメソッドを定義して、Symbolで指定しろって感じに見えますね(どっちでもいいけど)」「それにしても、こういうところに生のRubyコード渡せちゃう実装って怖すぎるよね」「ユーザー入力をそこに直接ツッコんだりしたら…((((;゚Д゚))))ガクガクブルブル」「しかしですね、昔のRailsの本ではこの書き方しちゃってたんですよ」「えー!」「まあ流石にnewとか書いてあったらおかしいってすぐわかるけど、あの頃メソッド呼び出しなんかはよく普通にこうやって書いて渡されたりしてたんですよね」「…」

⚓Rails

⚓Rails開発者アンケート2017の結果発表


rails-hosting.comより

コンテナ使いが半分超えたそうです。morimorihogeさんも勉強会で言ってましたが、Dockerはキャズムをとっくに過ぎて定番となりましたね。
自分の首を絞めるのはわかっていても、次回から「インフラ&コンテナ」枠も作らないといかんなという気がしてきました。


つっつきボイス: 「今話題のアレです: 前にウォッチで紹介した後に私も回答入れました」「ほほうアレね」

  • If not all apps are kept updated, why not?

「TIMEとBUDGETって実質同じ」「ところでこういうアンケートってどういう人たちがアンケートに回答してるか次第だよねホント」「永遠の課題ですね」「たとえば3.0 or earlierとか、実態がこのとおりなのかどうかは甚だ疑問: 意識低い人はそもそもこういうアンケートに回答しないだろうし」

「お、GitLabけっこう使われてるな」「コンテナ率58%、この母集団ではなっ」

「(好きなgemイラッとするgemで)まあ妥当でしょうね: 誰もが嫌うactiveadminとか」「(爆)」「なおオレはDevise嫌いじゃない派」「…Devise、コントローラさえもっとちゃんとしてれば」「あ、ホントそう!あれはマジでキモい、つらい」「というと?」「使ってカスタマイズすればわかります: 自分はカスタマイズが必要になったら、基本的にDeviseのコントローラは継承しないという戦略を取りますね」「そうそう」「Deviseのコントローラを継承したうえであれこれカスタマイズしようとすると、まずうまくいかない」「たとえばRegistrationのフェーズを自分たちでカスタマイズしたいなら、DeviseのRegstrationコントローラを見ながら自分でRegistrationコントローラを書くww: それが一番安全」「(爆)」

「ログインで段階的に会員登録するとかよくあるじゃないですか: たとえば最初はメールアドレスだけ入力させて取りあえず使えるようにしてあげる、そして他の機能を増やしたかったら個人情報をもっと入力してください、ってやるとか、こういうのをDeviseでやろうとするとかなり大変」「(゚д゚)(。。)(゚д゚)(。。) ウンウン」「デフォルトのRegistrationコントローラだとその辺がうまくいかないんで、そういうときにコントローラ自作したっ」

Deviseのこの立ち位置、タレントで言うと誰でしょう。

「RSpecも両方に入ってるのもまあわかる」「イラッとするgemの方にActiveRecordが入っているのは、だいたいarelのせい」「(爆)」

参考: rails/activerecord/lib/arel

このarelのリポジトリを探していて、まったく同じ名前のarelというRailsリポジトリの代数ライブラリが上位に出てきました。紛らわしいです…

「ところでこの『好きなgemイラッとするgem』のビジュアル表示、なかなかキレイですよね: こういうアート作品があった気がするんですけど」「ビックカメラ的とも言える」「たしかに」

  • For greenfield Rails projects, what are you using to manage Javascript libraries?

「おー、今日のBPS社内のチームミーティングで盛り上がったアセットマネジメントじゃないすか」「半分近くWebpack使ってるとか、母集団偏りすぎじゃね?」「まだ半分以上はasset pipeline使ってるということでもありますね」

  • Which Application Exception tracking tools do you use in production?

NewRelicがまた上昇? 9割ぐらいの人が無料枠とかじゃなくて?」「NewRelicいいんだけど高いんだよなー: 前にも話したけど」「自分はairbrakeクローンのerrbitを使ったりしてるかな: たまに落ちてますけど」「あと某案件で使ったrollbarはなかなかよかった: うろ覚えだけど、たしか無料枠でも5000イベントぐらいは食えた」「有料だと結構取られますが、rollbarはかなりいいヤツ」


newrelic.comより



airbrake.ioより


github.com/airbrake/airbrakeより

  • リポジトリ: errbit/errbit — オープンソースのairbrake API互換クローン


rollbar.comより


  • Which Rails Servers are you using in production?

「これはもうPumaでいいんじゃね?」「(笑)」「もうPuma以外を使う理由があまりない」「Pumaが普通に動いてればもうUnicornに戻りたくないし: 何がつらいって、Unicornでgraceful restartとやらが肝心なときにちゃんと動いたことがオレの中ではなかったこと」「そうだったんすか」「僅差だけどついにPassengerに抜かれたし」

  • Which databases do you prefer to use in production?

「やー、もうぽすぐれで決まりっしょ」「もうぽすぐれ!ぽすぐれ!」「Othersって何でしょね?」「そりゃあMicrosoft SQLとかOracleとか」「そういえば某案件ではついにOracle 12が登場するし」「新しいやつっすよね: マルチテナント対応とか何とかって」

「まあ母集団については適度に差し引いて考えるとして、とにかくいろいろ興味深かった」

⚓RailsガイドのRails 5.2リリースノート更新

お待たせいたしました。


つっつきボイス: 「お、これはhachi8833さんがやったやつ」「はいです: ちょっぴり翻訳しそびれた箇所がありましたがyasukawaさんが修正してくれました🙇」「たぶんそのあたりで自分がエネルギー切れ起こしたっぽいですw」

「今回訳していて、2年近くRailsウォッチとつっつき回やってて本当によかったと思いました: ActiveSupportDate#prev_occurringDate#next_occurring#26600)とか、この回のつっつき↓で以前扱ってなかったらきっと正確に訳せなかったと思います」

週刊Railsウォッチ(20170609)ついにtherubyracerからmini_racerへ、注意しないとハマるgem、5.1でのVue.jsとTurbolinksの共存ほか

「いやーほんと、こうやって顔を突き合わせながら毎週コミットを追いかけてああだこうだと言ってきたからこそ、更新情報がこれだけ膨大になっても『あー、あの時のアレねハイハイ』ってスイスイ読めるようになりましたね」「毎週追うのをやってなかったら、このボリュームを前にして『うげぇ、誰か人柱で何か踏むまで待つか』なんてスルーしちゃったり」「自分もね、Railsウォッチのつっつきを毎週やるようになってから、Railsの新しいバージョンを使うことへの抵抗感下がりましたもん」「確かにー」

⚓GitLab Web IDEがCEに登場


about.gitlab.comより

写真がいいですねー。社内GitLabサーバーにもただちに導入決定しました。

⚓Railsのメモリ喰いを鎮圧する(Hacklinesより)


同記事より


つっつきボイス: 「Railsのメモリ喰い、そんなのUnicorn使うのやめればだいたい直る」「(爆)」「いやマジで」
「他にも、Passengerの有料版にすればそれだけで相当メモリ量下がりますし」「とにかくRailsはRails自体のライブラリのフットプリントが大きすぎるんで、ワーカー1個立てるとそれだけで100MBとか食っちゃうから」「なるほどー」「そういうところがスレッドになればshared memoryになって軽くなるけど、マルチプロセスにするともう激重っす: つらみ」「Passengerにお金払う意味はそういうところにあるんですね」「ですね: あるいはPuma使うとか」

「それ以前の問題として、アプリケーションコードの中でワーカーごとのメモリ確保量がめちゃくちゃ多いようなコードって、そりゃもうコードが悪い」「(笑)」「よくあるのが、たとえば大容量ファイルのアップロード: 6万行ぐらいある巨大なExcelファイルとかCSVファイルを1リクエストでパースし始めると、それをメモリ上に展開するときに一瞬爆発的にメモリを食ってGCが動きそうになったり: そういうのは書き方が悪くて、非同期化するなりしないといけない」

「一般的な普通のRailsアプリなら、とりあえずPumaにしとけばいいと思う: Pumaにしてからメモリ食い問題にあまり突き当たらなくなったし」

RailsConf 2017のパフォーマンス関連の話題(1)BootsnapやPumaなど(翻訳)

⚓社内Slackより: AWSの設計

Railsからはちょっと外れますが。

「何これ、ヤメテーw」というニュアンスに見えたのですが、笑いのポイントが知りたかったので社内Slackで聞いたところ、むしろ真っ当でよい設計との由。

その後kazzさんとタバコ吸いながら話していて、「Lambdaの使い方やログのとり方もうまいし意図もよくわかるし、とてもいい設計ネ!」と激賞してました。


つっつきボイス: 「いい設計というかまあ、AWSみたいなマネージドなサービスで、こういう今流行のサーバーレス!なサービスとして静的Webサイト配信を組み立てようとすればこうなる感じですね」「こういうのが普通?」「というよりこうするしかない」

「このとき読み飛ばしてたけど、static websiteって書いてあったんですね」「そw: たかがstatic websiteを配信するためだけにこんだけのAWSサービスを動員しなきゃいけないという」「そこが笑いのポイントでしたか!なるほど〜すっきりした!」「普通ならさくらのレンタルサーバーあたりにぽいっと置けば済むようなしろものなのに」

「ちなみにGitHub Pagesなんてのはまさにこういう感じになってるはずですね(おっと自分で書いてたわ↑)」「あーそっか!」

「そしてこういうAWSなんかのサーバーレスなアーキテクチャ図のいいところは、このシステムがだいたいどういう機能を内包しているかが図を見ただけでだいたいわかるところなんですね」「たとえばこの図にはRoute 53が入ってますけど、これはすなわちドメインのホスティングも書き換えをするんだなと即座に見当がつく」「GitHub Pages使ったことあればわかると思うんですが、あそこは自分のGitHubユーザー名.github.ioってドメインになるんですね: あれをやるにはDNSを動的に操作できる必要があるから、Route 53が置かれていることでそういう機能があると即わかる」「逆にRoute 53が置かれてなければ、同一ドメインでのホスティングしかサービスできないということになる」「なーるほどー」

「もちろん元のツイートでlol wtf noとか言ってるのもわかるし自分もどうかとは思うけれどw、こういうふうにできるのは1個1個が小さいマネージドサービスだからなんですね」「これが逆に物凄い神システム的なモノリスだったりすれば、図にはinputとoutputが生えてるだけで中で何やってるのか全然見当がつかない」「だから果たしてどちらがいいのか?という話にもなりうるわけです」

「そういえば今日ちょうどBPS社内勉強会で新人向けにLinuxコマンドについて解説したんですけど、あれもまさにそうで、Unixの思想である『小さなものを組み合わせてタスクを達成する』的な話と、このアーキテクチャ図は通じているところがありますね: なーんてね」

もそっとよく見えるように、babaさんの注釈画像も貼ってみます。

「『シンプル』って書いてあるのがbabaさん流のギャグかなと」「あーなるほど、書いてある場所はどれも”S”で始まってますからねw: シンプルのS」「ちなS3はSimple Storage Service」「SNSもSimple Notification Service」「シンプルなものが組み合わさってこれだけコンプレックスなものができあがったと」「(笑)」

⚓RailsとWebpackerでAngular 2をがっつり使う

今日BPS社内のチームミーティングでRailsとAngular JSの組み合わせで白熱したときに見つけた記事です。いつもはやらないのですが、既に翻訳リクエストかけて翻訳中です。仮に応答がなくても、そのまま社内で共有しちゃう予定です。

新しいRailsフロントエンド開発(1)Asset PipelineからWebpackへ(翻訳)


つっつきボイス: 「この記事は前半でRailsのJS回りの歴史をきちっと解説してくれてるんですよ」「歴史はマジ大事: どうしてこうなった?とか何でこんな設計なの?って歴史が見えないとほんとわかんない」「自分が問題に直面してないと、これが解答だと言われても判断できないですもんね」「(笑)」

⚓Rollbar.comブログ: Railsプロジェクトのエラー、トップテン(Ruby Weeklyより)

上のアンケート話でも言及されてたrollbar.comのブログです。

⚓Kubernetes運用設計ガイド


つっつきボイス: 「くばねてさんは、まあ好きな人が追いかければって感じ」「正直、BPSがKubernetesを運用することはない: 意味ないから」「そんなことするぐらいならGCPを買う」「AWSのFargateは?」「あれはまだ出てなーい」「AWSにはEKSってのがあって、これがKubernetes、そしてFargateはAWS独自のコンテナマネジメント」「EKS、何もかもAWSにしたい人は使うんだろうけど、どういう人だ?」

⚓Ruby trunkより

⚓モジュールのincludeprependを同じクラスにいっぺんにやるとModule#ancestorsがおかしい気がする

# 同バグより
module M3; end
module M1
  include M3
end

module M2
  prepend M3
end


class Sub
  include M1
  include M2
end

# [Sub, M1, M3, M2, Object, Kernel, BasicObject]
p Sub.ancestors

つっつきボイス: 「このissue、放置されてます」「こういうのイヤなんだよねーもうw: ancestorsがどうなってるとか考えたくないし」「もうちょっとわかりやすく書いてくれればなー」

⚓URI::HTML5ASCIIINCOMPATがarrayではなく式を返す

# 同バグより
➜  ~ RBENV_VERSION=2.2.5 ruby -e "require 'uri'; p URI::HTML5ASCIIINCOMPAT"
[#<Encoding:UTF-7 (dummy)>, #<Encoding:UTF-16BE (autoload)>, #<Encoding:UTF-16LE (autoload)>, #<Encoding:UTF-32BE (autoload)>, #<Encoding:UTF-32LE (autoload)>]
➜  ~ RBENV_VERSION=2.3.0 ruby -e "require 'uri'; p URI::HTML5ASCIIINCOMPAT"
"expression"
➜  ~ RBENV_VERSION=2.4.4 ruby -e "require 'uri'; p URI::HTML5ASCIIINCOMPAT"
"expression"
➜  ~ RBENV_VERSION=2.5.0 ruby -e "require 'uri'; p URI::HTML5ASCIIINCOMPAT"
"expression"

つっつきボイス: 「regressionで、2.3.0からのバグってことか」「URI::HTML5ASCIIINCOMPATとか自分使わないなきっと」

⚓64-bit fixnumの精度が落ちる

# 同バグより
assert_operator((0.0).step(bignum.to_f, 1.0).size, :>=, bignum) # 精度が落ちることがある

つっつきボイス: 「RubyのNumericは自動で切り替わるから、そこのあたりか」

⚓Integerのアトミックなincr/decrが欲しい

# 同issueより
counter = 0
counter.incr # => 1
counter.incr(10) # => 11
counter.incr(-1) # => 10
counter.reset # => 10
counter # => 0

つっつきボイス: 「Rubyのようにすべてがオブジェクトだと素朴なincr/decrはできないという認識でした」「Rubyには++とか--はないですからね」「この辺難しいなー: incrみたいなことをしたいとしたら破壊的メソッドとして!付けるべきかとか、10大きいとか-1大きいみたいなことを指定したいなら次の値という意味でsuccessor的な名前の方がいいんじゃないかな、とか」「だからincr要らないんじゃね?って」「このresetって…?」(以下延々)

⚓Ruby

⚓るびまがリニューアル: スマホに対応


つっつきボイス: 「るびまについに文明が訪れた!」「るびまって、みよひでさんがほぼ一人でやってるんじゃなかったかな?『いいね!』ボタンが絶妙にずれてたりするあたりに、みよひでさんがここでちょっと力尽きた感が」「偉大な人だ…」

⚓OSS Gate東京ミートアップ: Rubyでデータ処理をするツールを開発中


speee.connpass.comより


つっつきボイス: 「Rubyでデータ処理というのが目をひいたので」「スポンサーはどこかと思ったらSpeeeさんでしたか」「eは3つ」「Speeeといえばえっと、PyCallの人だ!」「道理で」

参考: (9) Kenta Murata | LinkedIn

⚓ついに4倍速になった部分も

⚓シングルトンオブジェクトをRubyのデフォルトの引数に使う

# 同記事より
class Foo
  NOT_PROVIDED = Object.new

  def bar(one, two: NOT_PROVIDED)
    puts one.inspect
    if two == NOT_PROVIDED
      puts "not provided"
    else
      puts two.inspect
    end
  end

  private_constant :NOT_PROVIDED
end

つっつきボイス: 「デフォルト引数にちょっとしたValue Objectを入れたいみたいな話かな: よくあるやつ」「Object.newじゃなくてもたとえばハッシュにするとか」

⚓Rubyの「deep hash」(Hacklinesより)


同記事より

# 同記事より
def set_value!(hash, value = nil, &fn)
  lead_in    = @paths[0..-2]
  target_key = @paths[-1]
  new_hash = hash
  lead_in.each { |s| new_hash = new_hash[s] }
  new_value = block_given? ?
    yield(new_hash[target_key]) :
    value
  new_hash[target_key] = new_value
  hash
end
# Hehehehehehe
def set_value(hash, value = nil, &fn)
  set_value!(deep_clone(hash), value, @fn)
end

つっつきボイス: 「XF?あ、gemのようだ」「transformってどことなく数学用語っぽいですね」

「ふーむ、式を与えると、ハッシュなりオブジェクトに手を突っ込んで集めてきてくれるやつっぽい?: 何か名前あったなー、こういう探索して引っこ抜いてくるみたいなやつ」「関数型っぽい感じですね」「READMEに書いてある、HaskellのLensって書いてあるのがそれなのかな?」「これはそろそろ社内Haskell勢を召喚しなくては」

# 同リポジトリより
# Read left to right
%w(1 2 3 4).map(&Xf.pipe(:to_i, :succ))

# Read right to left
%w(1 2 3 4).map(&Xf.compose(:succ, :to_i))

people = [{name: "Robert", age: 22}, {name: "Roberta", age: 22}, {name: "Foo", age: 42}, {name: "Bar", age: 18}]

people.map(&Xf.scope(:age).get)
# => [22, 22, 42, 18]

「上の例だとage:の値をかき集めてる: データ処理なんかによさそう」「データ構造のネストが込み入ってきたときとかにこんなふうに書きたいかもね」「こういうのって『いつか使ってやるゾ』って心の中で準備しといて使う機会を日頃から探してないと使うの忘れちゃうんですよね」「これなんてeachでループ書けば全然普通にできることだし」

⚓台湾でRubyxElixirカンファレンスが明日から開催

観測範囲では、Matzやhsbtさんやko1さんやigaiga555さんなどが昨日あたりから現地入りしているようです。ところで、Matzだけは「さん」を付けるのが自分の言語感覚的にどうもしっくりこないので、それだけの理由で敬称略しています。悪しからず。


お大事にどうぞ。

⚓その他記事など




つっつきボイス: 「このelsifの説明割りと納得できる: Eiffelがそうだからというのも含めて」「ほぼ使わないけどなっ」



つっつきボイス: 「Guildの話、ネットにもほとんど見当たらなかったので」



⚓SQL

⚓PostgreSQLのデータ型: Date/Timestamp/Time Zones(Postgres Weeklyより)


つっつきボイス: 「いろんなDBMSのデータ型の扱いは、一度は比較して調べておくといいですよ」「特にtimestampはDBMSによって違ってたりすることがあるんで: timestampがnullableなものもあれば、MySQLみたいにtimestamp(あとtextarea)にnullをセットできないのはdatetimeにして切り抜けたりとか」「ほー!」

⚓スライド: PostgreSQL 10のパフォーマンス向上に役立つ機能

⚓GCPのCloud SQLでPostgreSQLをサポート(Postgres Weeklyより)

⚓JavaScript

JavaScriptの扱いが軽いわけではなくて、情報が多すぎて手が回らないのが現状です。

⚓めちゃ大規模なJavaScriptアプリを設計する(JavaScript Weeklyより)

⚓VuePress: 静的サイトジェネレーター(JavaScript Weeklyより)


vuepress.vuejs.orgより

URLからしてVuejs本家ですね。

⚓OracleがiOS向けJSエディタアプリが商標を侵害と主張?

⚓Hyper 2: Electronベースのターミナルソフト(JavaScript Weeklyより)


つっつきボイス: 「自分のターミナルがJSで動いてもなぁ…やるなら安定してクラッシュしなくて履歴をいくらでも取れることを要求したいし」

⚓CSS/HTML/フロントエンド

⚓Chrome 67でHPKP廃止: CTに移行か

参考: Certificate Transparency の仕組みと HPKP から Expect-CT への移行 | blog.jxck.io — こちらのサイト、いつも参考になります。


つっつきボイス: 「略語って大変」「このjxck.ioの人はHTTP回りの新技術めちゃめちゃ詳しい」「mozaic.fmの人ですよね」「それだっ」

参考: mozaic.fm

⚓フルCSSで構築された美サイト


つっつきボイス: 「これはてブでめちゃ上がってたやつ」「お仕事探してるのかな?」「たぶん学生なんじゃ?」

⚓CLIP STUDIO TABMATE: タブレットを多用するデザイナーの左手を強化するデバイス

前からあるようですが一応。

⚓Gmailの新機能

知っとかないと割りと困ると思います。いっそ世界中のメールが全部Gmailに集約されちゃえばいいんです。

⚓言語よろず部屋

⚓cargo src: Rustのソースを追うツール


ncameron.orgより

社内Rust/Haskell勢の方からのおたよりで知りました。

⚓C2: 次世代のC言語?


同サイトより

こちらも社内Rust/Haskell勢の方からのおたよりで知りました。

参考: C言語の現代化を目指すC2

⚓ノリノリmattnさん

Go言語については、mattnさんを追いかけていればだいたい面白そうなことやってます。

参考: Big Sky — mattnさんのブログです。

⚓SwiftKotlinという変換ツール出現

同リポジトリより


つっつきボイス: 「へー、割りと1対1対応してる」「その言語にしかないようなオブジェクトクラスとかでなければ、最近の言語はその気になれば割りと相互変換できるんかな」

Parrotってあったのを思い出したのですが、最近どうなんでしょう。

⚓GopherCon 2018

というツイートを見かけて、もしやと思ったらと今こんなのやってるそうです。今の今まですっかり見落としてました(´・ω・`)。

  • Russ Coxの講演

Go Time 77: Dependencies and the future of Go with Russ Cox – Listen on Changelog.com

⚓こんなところで

⚓「Cコンパイラ自作」集中講義ですって

⚓その他

⚓GoogleがDFPの旧いAPI提供を終了

GoogleのDoubleClick for PublishresというGoogleのサービスです。

参考: DFPことはじめ。基本的な用語と設定手順。 - Qiita

⚓DNS over HTTPSって?

サイトブロッキング絡みの記事で「DNS over HTTPSが普及する前の今ならXXできる」という記述を見かけたのですが、その記事がどこだったか思い出せなくてXXが何だったのかわからずじまい…。

⚓BBCが効果音を無料公開

⚓Twitterの視覚障害者支援機能


つっつきボイス: 「alt属性を入れられるみたいな感じ?」

⚓HackerRankの2018年スキルアンケート結果

恐ろしいぐらい凝りに凝ったビジュアル表示です。デザイナーにも眺めて欲しい感じです。


research.hackerrank.comより

次に学びたい言語の5位がRubyでした。


research.hackerrank.comより


つっつきボイス: 「Rが意外に高い??今からR学びたい人ってデータ解析やる人しかいないだろうなー」「Juliaが意外に低い」「見ようによってはそれほど差がないという気もしますね」「Rは昔ちょっと使ったなー: チャートを描くときとか」「3分ぐらいなら触ったことありました」

⚓皆さんのCDPR対策は大丈夫?

参考: EU一般データ保護規則 - Wikipedia

⚓その他のその他


同記事より


参考: インパクトファクター - Wikipedia



つっつきボイス: 「うん、これはもう既に普通というか当然ですね: 巷で流れてくるAIとか機械学習とかビッグデータとか言ってるやつはだいたいビッグデータじゃないし、フィルタでできることをやってるという感じ」「本当に機械学習が必要なのって、たとえばこのデータから何が取れるのかわからん、みたいなものだろうし」「私将棋に暗いんで知りませんでしたが、この山本一成さんって将棋AIのすごい人なんですね」「山本さんはもう超有名っす」「むちゃくちゃ有名っすね: Turing Complete FMの文字コードの回でも言ってた、あの逆さまの将棋フォント作っちゃった人↓」

参考: フォントはフォントに大事|山本 一成@Ponanza|note


⚓番外

⚓和で評価するか調和平均で評価するか


scrapbox.io/nishioより

参考: 調和平均 - Wikipedia

⚓徹夜が身体に良くない理由


つっつきボイス: 「このあたりはねー、個人差あるっしょ」

⚓ミクロ2題


sciencealert.comより


⚓World of Tanksの戦車リサーチはガチ

現在,我々の会社には2200人の社員がいて,そのなかにはフルタイムで戦車の歴史的研究をしている社員が12人います。図書館や美術館は基本ですが,ロシアの各地には日本の戦車が残っています。遺棄された戦車を実際に調べにも行きました。
同記事より


つっつきボイス: 「ちょwwこれw」「フルタイム、12人ってw」「下手すると日本の研究者より詳しいぞこれ!」
「なんかこう、アサシン クリード オリジンズの最近出たエジプト編みたいなやつで、発見されたばかりのピラミッドが当然のようにゲームに速攻実装されてたみたいな世界ですね」「何それ~」「それむしろ実装しなかったら悔しくて夜も眠れないんじゃ」

参考: 『アサシン クリード オリジンズ』には、先週発見された「ピラミッド内の謎の巨大空間」がすでに実装されていた?隠し部屋が確認される | AUTOMATON

⚓超精密かつ斬新なリズム

この方々の体内クロックは、一番細かい音符の少なくとも2倍速はあります(インドやアラビアやアフリカやラテンの音楽ならまったく平常運転)。体内での分周によって8分音符や4分音符といったパルスを得て、それを出力しています。
より振動数の高い内部クロックを体内で分周することで、音符の長さに関わらずテンポが極めて安定します。

このあたり、気が向いたらポエム記事にしようと思います。


今週は以上です。皆さまよいゴールデンウィークを!

Twitterより

バックナンバー(2018年度)

週刊Railsウォッチ(20180420)NGINX Unit正式リリース、Rubyをメモリリークさせてみる、美しいコード宇宙ほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやRSSなど)です。

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

Hacklines

Hacklines

Postgres Weekly

postgres_weekly_banner

Frontend Focus

frontendfocus_banner_captured

JavaScript Weekly

javascriptweekly_logo_captured


Rails tips: RSpecテストを遅くする悪い書き方3種(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails tips: RSpecテストを遅くする悪い書き方3種(翻訳)

テストが遅くなる原因はそれはもうさまざまで、コードに関係するものもあればそうでないものもあります。今回は、specを高速化して改善するちょっとした変更のコツをご紹介します。

1. truefalseを常に期待する場合はbe_truthybe_falseyを避ける

まずは以下のコードをちょっとご覧ください。

expect(true).to be_truthy
expect(1).to be_truthy
expect('string').to be_truthy
expect(nil).to be_falsey
expect(false).to be_falsey

どのexpectationもパスしますので、falsetrueが返っているとお考えになるかもしれませんが、何か不具合があってこれ以外のものが返されると、テストでキャッチできなくなってしまう可能性があります。

解決方法

値そのものを指定します。

expect(true).to eq(true)
expect(1).to eq(true)
expect('string').to eq(true)
expect(false)to eq(false)
expect(nil).to eq(false)

2. FactoryBot.buildは避ける

訳注: 原文のFactoryGirlはリンクを除いてFactoryBotに置き換えました。

FactoryBot.buildを呼び出してもデータベースにレコードは作成されないとお思いかもしれませんが、まあUse Factory Girl’s build_stubbed for a Faster Test Suiteをご覧ください。ファクトリー内で関連付けが宣言されていると、レコードが作成されてしまいます。たとえば次のようなファクトリーがあるとしましょう。

FactoryBot.define do
  factory :user do
    contact
    company
  end
end

そしてFactoryBot.build :userを呼び出すと、データベースにレコードが2件作成されます。テストの冒頭でファクトリーを初期化してexampleを10件実行すれば、レコードが20件も作成されます。このレコードが不要であれば、改善の余地が大いにあります。

解決方法

FactoryBotl.build_stubbedを使うことです。こちらはデータベースにレコードを作成することはありません。

3. スタブの代わりにModel.newするのは避ける

以下の例で考えてみましょう。シンプルなSampleClassクラスとSampleApiクラスがあります。

class SampleApi
  def login; end
end
class SampleClass
  def call
    api.login
  end

  private

  def api
    SampleApi.new
  end
end

そしてSampleClass#callメソッドをテストしたいとします。

require 'spec_helper'

describe SampleClass do
  describe '#call' do
    it 'calls API' do
      api = SampleApi.new
      allow(SampleApi).to receive(:new).and_return(api)
      allow(api).to receive(:login)

      sample_class = SampleClass.new
      sample_class.call

      expect(api).to have_received(:login).once
    end
  end
end

ここまではよさそうに見えます。お次はSampleApiに新しいメソッドを追加してそれを#loginメソッドより前に呼び出したいとします。やってみましょう。

class SampleApi
  def login;end
  def before_login_action;end
end
class SampleClass
  def call
    api.before_login_action
    api.login
  end

  private

  def api
    @api ||= SampleApi.new
  end
end

テストを実行してみるとやはりgreenのままです。クラスの実装を変更したのにこうなったというのは悪い知らせです。スタブ化されてないインスタンスを使うと、実行されたメソッドに対する制御が失われてしまいます。この手の問題を洗い出すのは大変で、テスト駆動開発(TDD)アプローチを行わない開発者がメソッドを更新した場合は特にそうです。

解決方法

代わりにinstance_doubleを使います。api = SampleApi.newapi = instance_double(SampleApi, login: double)に置き換えれば、ちゃんとエラーが出力されます。

Double "SampleApi (instance)" received unexpected message :before_login_action with (no args)

この解決法は元の書き方とくらべても決して遅くはなく、しかも完全にコントロール可能です。メソッドがn回実行されることを期待するメソッドに対してinstance_doubleを組み合わせれば、期待どおりの結果が得られます。

お知らせ: RSpec & TDDの電子書籍を無料でダウンロード

もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。

関連記事

Rails: テスティングアンチパターン –前編(翻訳)

Rails: テスティングアンチパターン –後編(翻訳)

TestProf: Ruby/Railsの遅いテストを診断するgem(翻訳)

Railsのビューは頭悪そうなぐらいシンプルに保つべし(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Railsのビューは頭悪そうなぐらいシンプルに保つべし(翻訳)

Railsプロジェクトに長年携わっているうちに、最初は純真きわまりないビューだったのが、いつしかネストだらけの複雑怪奇なRubyコードと入り組んだHTMLを煮込んだようなものに変わり果てていました。こうなってしまうと、理解するのも大変なら改修するのも大変です。ビューの取るに足らないような部分をちょっぴり修正するだけでも、エンドツーエンドテストを辛抱強く書いてはバグがつぶされたかどうかを確認することになります。しかもこの種のテストはテストスイートの総実行時間に激しく影響するので、これ以上テストを増やしたくないと思うのが人情です。重要でも何でもないエッジケースならなおさらです。テストにはそうしたexpectationが全部詰まっているので、エンドツーエンドテストが増えれば増えるほどビューの改修が困難になります。

ビューの機能をどれだけ増やさずにいられるかが決め手です。ビューのロジックが複雑になればなるほど、テストも困難になります。複雑になれば、HTMLやらボタンのクリックやらCSSのセレクタなどなど、考えなければならないことがその分増えるからです。

この問題のシンプルな解決方法は、ロジックを別のクラスに移動することです。別クラスに切り出されたロジックならば単体テストも楽になります。単体テストは実行も早く、書くのも簡単です。

そこから抜粋したビューで考えてみましょう。少々込み入ったロジックも含まれています。

<div class="checkout">
  <% if @order.line_items.count > 0 %>
    <% if (@order.total - @order.paid) > 0 %>
      <div class="outstanding-amount"><%= number_to_currency(@order.total - @order.paid) %></div>
    <% end %>
    <div class="all-the-line-items"></div>

    <% if @order.cancelled_at.nil? && (@order.total - @order.paid) > 0 %>
      <%= link_to "Cancel your order", cancel_order_path(@order) %>
    <% end %>
  <% else %>
    Your order is empty!
  <% end %>
</div>

ここで何が行われているのかを読み取るには、考える必要があります。特にif条件を読み解くにはしばらく時間がかかるでしょう。

Railsプロジェクトの場合、ロジックの移動先の有力な候補はビューデコレータです。ロジックだけをここに移し、HTMLレンダリングから出来る限り切り離します。HTMLレンダリングのテストを書くのは、単純な戻り値テストを書くよりずっと面倒だからです。

1つのデコレータにすべてのロジックを詰め込まなければならないということはありません。1つのページだけを担当するデコレータを作成することも、ページの特定のセクションだけを担当するデコレータを作成するのも、ありです。1つのデコレータが、デコレータでない別のクラスのロジックを委譲しても構いません。デコレータについて詳しくは、Railsアンチパターン: Decoratorの肥大化に譲ります。

さて、上のビューのデコレータは以下のような感じになります。

class OrderDecorator < Draper::Decorator
  delegate_all

  def checkout_possible?
    line_items.count > 0
  end

  def can_be_cancelled?
    cancelled_at.nil? && !complete?
  end

  def complete?
    unpaid == 0
  end

  def unpaid
    total - paid
  end
end

ロジックをデコレータに切り出してみると、かなり読みやすいビューになりました。

<div class="checkout">
  <% if @order.checkout_possible? %>
    <% unless @order.complete? %>
      <div class="outstanding-amount">$ <%= number_to_currency(@order.unpaid) %></div>
    <% end %>
    <div class="all-the-line-items"></div>

    <% if @order.can_be_cancelled? %>
      <%= link_to "Cancel your order", cancel_order_path(@order) %>
    <% end %>
  <% else %>
    Your order is empty!
  <% end %>
</div>

このビューにはテストの必要なエッジケースがほとんどないことが完全に明らかです。誤りは肉眼で確認してもよいくらいです。複雑なエンドツーエンドテストを書く代わりにシンプルなテストを書けば済むので、時間も節約できます。テストスイートの実行も速くなるので、TDDサイクルが落ちることもさほどありません。ビューのロジックを排除すれば、油断なく見張り続ける必要もなくなるのでエネルギーの節約にもなります。

関連記事

Railsアンチパターン: Decoratorの肥大化(翻訳)

Rails: テストのリファクタリングでアプリ設計を改良する(翻訳)

Rails tips: 知らないと損する4つのバリデーションレベル(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails tips: 知らないと損する4つのバリデーションレベル(翻訳)

考えるまでもないことですが、アプリがユーザー入力を受け取ったらバリデーションが必要になります。Ruby on Railsアプリでバリデーションといえば真っ先に思い当たるのがモデルのバリデーションです。しかしそれ以外のレベルのバリデーションについてはどうでしょう。モデルのバリデーションがあれば完璧なソリューションになるのでしょうか?今回はRailsアプリの4つのレベルのバリデーションを簡単にご紹介しつつ、それぞれのメリットとデメリットについて説明したいと思います。お題として、Userモデルのemailカラムを使います。

1. モデルレベルのバリデーション

Railsアプリでよく見られるアプローチです。emailUserのレコードに確実に存在するようにするために、以下のバリデーションを定義できます。

class User < ActiveRecord::Base
  validates :email, presence: true
end

このデータ保護方法は間違っていませんが、これだけではメールが空のUserレコードをまだ作成できてしまう点を肝に銘じてください。User#saveUser#save!を呼んでも無効なレコードはデータベースに保存されませんが、以下のメソッドを呼べば保存されてしまうのです。

  • decrement!
  • decrement_counter
  • increment!
  • increment_counter
  • toggle!
  • touch
  • update_all
  • update_attribute
  • update_column
  • update_columns
  • update_counters

これらのメソッドを用いる場合、特にupdate_で始まるメソッドについては注意が必要です。では逆に、emailカラムのバリデーションが不要な場合はどうすればよいでしょうか?その場合は:ifオプションを渡すか、コントローラレベルのバリデーションを検討しましょう。

2. コントローラレベルのバリデーション

上述したように、特定の場合に限ってバリデーションを行いたいことがあります。:ifオプションや:unlessオプションを渡してもよいのですが、バリデーションルールが複雑になって読みづらくなったりテストがしにくくなるかもしれません。そこでコントローラレベルのバリデーションが選択肢として浮かび上がってきます。これを正しく行うには、Form Objectパターンの利用をおすすめします。ただし、コントローラレベルのバリデーションはモデルレベルのバリデーションに比べてメンテの難易度がぐっと上がります。Form Objectパターンは、モデルに多数のバリデーションがあり、ときどき必須にしたいバリデーションやときどきオプションにしたいバリデーションがあるような非常に大規模なアプリでとても有用です。

訳注: Form Objectについては以下の記事やForm Objectタグなどもどうぞ。

Rails: Form Objectと`#to_model`を使ってバリデーションをモデルから分離する(翻訳)

3. データベースレベルのバリデーション

データベースレベルのバリデーションは最も安全性が高く、これをかいくぐって無効な値を保存することはできません。よく使われるのはpresenceuniquenessで、どちらもUserモデルのemailカラムにうってつけです。以下のようにマイグレーションを作成して追加します。

class AddValidationOnUserEmail < ActiveRecord::Migration
  def change
    change_column :users, :email, :string, null: false
    add_index :users, :email, unique: true
  end
end

rake db:migrateを実行すればバリデーションをテストできます。モデルのバリデーションを呼び出さないupdate_columnメソッドを使ってみましょう。メールアドレスのないユーザーを試しに保存してみます。

user = User.find(user_id)
user.update_column(:email, nil) # => raises ActiveRecord::StatementInvalid: Mysql2::Error: Column 'email' cannot be null

ちゃんとエラーがraiseされました。今度はメールアドレスが重複しているユーザーを保存してみます。

user = User.find(user_id)
user_2 = User.find(user_2_id)

user.update_column(:email, user_2.email)
# => raises ActiveRecord::RecordNotUnique: Mysql2::Error: Duplicate entry

2つのバリデーションは種類が異なります。presenceバリデーションはカラム定義に渡せばよいのですが、uniquenessバリデーションの場合はデータベースに正しいインデックスを作成しなければなりません。このエラーは、無効なデータを保存しようとしたときにもraiseされます。

user = User.find(user_id)
user.update_column(:created_at, "string")
# => raises ActiveRecord::StatementInvalid: Mysql2::Error: Incorrect datetime value

ご覧いただいたように、パフォーマンス上の意味だけではなく、セキュリティのためにも、カラムを慎重に定義して正しくインデックスを作成することが重要です。

4. フロントエンドのバリデーション

最も安全性の低いバリデーションです。ブラウザでJavaScriptをオフにしたり、コードを使ってリクエストを直接送信したり、Postmanなどのブラウザ拡張を使ったりすれば、このバリデーションをバイパスできます。

データの保護はどんな場合であっても、バックエンド側のバリデーションで最初に行うべきです。しかしフロントエンドのバリデーションは、ユーザーエクスペリエンス向上には最も適しています。私は、フォーム送信を待たずにアプリがその場でフォームのエラーを表示してくれるのが好きです。

新着記事を見逃したくない方はTwitterをフォローしてください。もちろん「hello」だけでも構いません!

お知らせ: RSpec & TDDの電子書籍を無料でダウンロード

もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。

関連記事

Rails tips: カスタムバリデータクラスを作る(翻訳)

Rails: Form Objectと`#to_model`を使ってバリデーションをモデルから分離する(翻訳)

Rails 5.2のcredential機能をKubernetesで使う(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

credential(認証情報)は英ママとしました。

Rails 5.2のcredential機能をKubernetesで使う(翻訳)

Rails 5.2で導入された新しいcredentials機能で遊んでみた結果、これが実にクールだと思えました。暗号化されたcredential(secretとも呼ばれます)をリポジトリに保存しておき、読み出す必要が生じたらマスターキーで暗号化を解除できます。

Rails 5.1で導入されたsecrets.ymlとの違いを知りたい方は、「Rails Encrypted Credentials on Rails 5.2」をご覧ください。

RailsのアプローチはShopifyのejsonと非常に似通っていますが、ejsonのような非対称暗号化を採用していない点が異なります。

新しいcredential管理は、Kubernetes上で動作するコンテナ化Railsアプリでどのように機能するでしょうか。結論から言うと、驚くほどスムースでした

ロケット科学のような難しいセットアップは不要です。Rails 5.2 credentialのデプロイがどれほど簡単であるかを本記事でご説明いたします。

$ gem install --pre rails

$ rails -v
Rails 5.2.0.rc2

$ rails new secretland --skip-javascript --skip-spring --skip-coffee --skip-turbolinks --skip-action-cable

$ bin/rails credentials:edit
# 暗号化済みcredentialをvimで開く

$ cat config/master.key
3bed2fdcb0261e6f48850de01a85fb5b
# このアプリのcredential向けマスターキーは.gitignoreにも記載されるのでgitにpushされない

お次はコンテナをビルドしましょう。まず、.dockerignoreファイルにマスターキーを追加し、コンテナにうっかり置かれないようにします(このキーをコンテナレジストリに公開したくありません)。

$ echo config/master.key > .dockerignore

コンテナのビルドには、こちらの最小限のDockerfileを用います。

FROM ruby:2.5

RUN mkdir -p /app
WORKDIR /app

ENV RAILS_ENV production
ENV RAILS_SERVE_STATIC_FILES true
ENV RAILS_LOG_TO_STDOUT true

COPY Gemfile /app/
COPY Gemfile.lock /app/
RUN bundle config --global frozen 1
RUN bundle install --without development test

COPY . /app

EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0"]
$ docker build -t kirshatrov/secretland:v1 .

このマスターキーを環境変数として実行します。

$ docker run -i -t -p 3000:3000 -e RAILS_MASTER_KEY=3bed2fdcb0261e6f48850de01a85fb5b kirshatrov/secretland:v1

大事な秘密情報をうっかり出力する残念なコントローラを書くと、以下のようになってしまいます。

このコンテナをKubenetesノードからダウンロードおよび実行できるよう、Dockerレジストリにpushします。

$ docker push kirshatrov/secretland:v1

Kubernetesリソースを作成する前に、そのための秘密情報の作成が必要です(私の場合Kubenetesの秘密情報は実際にはこれが初めてでしたが)。

$ kubectl create secret generic secretland-secrets --from-literal=rails-master-key=3bed2fdcb0261e6f48850de01a85fb5b
secret "secretland-secrets" created

$ kubectl describe secret secretland-secrets
Name:         secretland-secrets
Namespace:    default
Labels:       <none>
Annotations:  <none>

Type:  Opaque

Data
====
rails-master-key:  32 bytes

Deploymentのspecは次のとおりです。

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: secretland
  labels:
    app: secretland
spec:
  selector:
    matchLabels:
      app: secretland
  template:
    metadata:
      labels:
        app: secretland
    spec:
      containers:
      - image: kirshatrov/secretland:v1
        name: rails
        ports:
        - containerPort: 3000
        env:
          - name: RAILS_MASTER_KEY
            valueFrom:
              secretKeyRef:
                name: secretland-secrets
                key: rails-master-key

ここにトリックが潜んでいます。環境変数(RAILS_MASTER_KEY)の設定には、事前に作成した秘密情報の値から取り出したものを使っています。こうすることで秘密情報をDeploymentから切り離し、Deploymentリソースにマスターキーが漏洩することを回避できます。このYAMLをDeployment specと一緒にアプリのリポジトリにpushすることすら可能です。

追記: 本記事コメントでVictorが指摘したように、config/master.keyにファイルとして秘密情報キーをマウントする方がよいかもしれません。これにより、Railsは環境変数ではなくこの情報を使うようになります。この場合、YAMLのspec.templateの部分は以下のようになります。

spec:
  containers:
  - image: kirshatrov/secretland:v1
    name: rails
    ports:
    - containerPort: 3000
  volumes:
  - name: secrets
    secret:
      secretName: secretland-secrets
      items:
      - key: rails-master-key
        path: /app/config/master.key

このDeploymentを適用してインターネット上に公開します。

$ kubectl apply -f deployment.yml

$ kubectl expose deployment secretland --type=LoadBalancer --port=80 --target-port=3000

見事に動きました!

本記事で用いたコードはGitHubのkirs/secretlandリポジトリでご覧いただけます。

正直、こんなに何もかもがスムースに行くとは想像もできませんでした。Rails 5.2のcredential管理はコンテナ化されたアプリでも実にうまくいきますし、Kubernetesへの秘密情報のpushもコマンド一発で完了します。

今後は、このcredential情報を編集してからbin/rails credentials:editgit pushを実行するだけでproduction環境を更新できるでしょう。

本ブログの更新情報については、Twitterで@kirshatrovをフォローして下さい。

関連記事

minikubeでDockerコンテナをクラスタ構成した運用とDockerコンテナを手作業で地道に管理の運用とどっちが楽か検証しながらまとめてみた

Kubernetesでのデプロイ中に’db:migrate’や’db:seed’などのRailsタスクを管理する(翻訳)

Rails tips: あまり知られてない機能1: ActiveJobとActiveModel(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails tips: あまり知られてない機能1: ActiveJobとActiveModel(翻訳)

私はRailsガイドを通しで読んだことがなかったのですが、同ガイドを自国語に翻訳していて、それまで気づかなかったクールな機能をいくつも発見しました。皆さまのお役に立てばと思います。ActiveJobやActiveModelモジュールの知られざる機能を他にもご存知でしたらぜひお知らせください。

⚓1. ActiveJobのカスタムシリアライズ

大規模なRailsアプリを構築していれば、Sidekiqdelayed_jobといったバックグラウンド処理用エンジンをおそらくお使いでしょう。私はSidekiqが本当に好きなのですが、中にはSidekiqではできないこともあります。

class Worker
  include Sidekiq::Worker

  def perform(user)
    # 何かすごいことをやる
  end
end

user = User.first
Worker.perform_async(user)

これを扱おうと思ったら、次のようにUser#idを渡してユーザーを手動で代入しなければなりません。

class Worker
  include Sidekiq::Worker

  def perform(user_id)
    user = User.find(user_id)
    # 何かすごいことをやる
  end
end

user = User.first
Worker.perform_async(user.id)

しかし、SidekiqをアダプタにしたActiveJobを使っていればそんなことをする必要はありません。ActiveJobはデフォルトでActiveRecordオブジェクトのシリアライズとデシリアライズをサポートしていますが、カスタムデータ向けに独自のシリアライズ/デシリアライズを定義することもできます。耳寄りなお話だと思いませんか?

この辺で何かコード例が欲しくなりますね。

位置情報機能を使っていて、Locationオブジェクトがあるとします。次のように緯度(lat)と経度(lon)を渡せば新しいlocationオブジェクトを初期化できます。

location = Location.new(lat, lon)
location.city    # => どこぞの町
location.country # => どこぞの国

Locationのインスタンスをワーカーに渡して、ワーカーのperformメソッドでインスタンスにアクセスさせたいとします。どうすればできるのでしょうか?独自のシリアライザ/デシリアライザを定義して、LocationSerializerと名前を付ければよいのです。

完全に機能するよう実装するには、以下の3つのメソッドを実装しなければなりません。

  • serialize?: 指定の引数を現在のシリアライザでシリアライズできるかどうかをチェックします
  • serialize: このメソッドは、基本型の含むキーだけを含むハッシュを返さなければなりません
  • deserialize: ハッシュをオブジェクトに変換します

まずはserialize?メソッドからいきましょう。これはシンプルです。

def serialize?(argument)
  argument.kind_of?(Location)
end

指定の引数がLocationクラスのインスタンスかどうかをチェックしているだけです。

新しいlocationオブジェクトを初期化するには緯度と経度を渡す必要があるので、serializeメソッドとdeserializeメソッドも同じくシンプルです。

def serialize(location)
  super(
    "longitude" => location.longitude,
    "latitude" => location.latitude
  )
end

def deserialize(hash)
  Location.new(hash["latitude"], hash["longitude"])
end

クラス全体は次のようになります。

class LocationSerializer < ActiveJob::Serializers::ObjectSerializer
  def serialize?(argument)
    argument.kind_of?(Location)
  end

  def serialize(location)
    super(
      "longitude" => location.longitude,
      "latitude" => location.latitude
    )
  end

  def deserialize(hash)
    Location.new(hash["latitude"], hash["longitude"])
  end
end

このクラスを使いたいということをRailsに認識させなければなりません。

Rails.application.config.active_job.custom_serializers << LocationSerializer

以上でおしまいです。Locationオブジェクトを渡してperformメソッドを実行すれば、ワーカー内でオブジェクトを使えます。

⚓2. ActiveJob: 属性をJSONから読み込む

JSONフォーマットで受け取ったレスポンスを、モデルの属性にさくっとマッピングしたいと思いませんか?実は簡単な方法があるのです。JSONを受け取って、文字列から取り出した値を用いて属性を設定してくれる#from_jsonメソッドがどのモデルにも実装されています。

Userモデルにfirst_name属性とlast_name属性があれば、次のようにできます。

response = {first_name: "John", last_name: "Doe"}.to_json
user = User.new
user.from_json(response)
user.first_name # => John
user.last_name  # => Doe

マスアサインメントしても、変更したくない属性には代入されませんのでご心配なく。

response = {fake: "fake"}.to_json
user = User.new
user.from_json(response) # => raises ActiveModel::MassAssignmentSecurity::Error

⚓3. ActiveModelの属性メソッド

賭けてもよいですが、読者の皆様もきっと以下のようなコードを何度も書いたことがあると思います。

user = User.first
user.first_name = "John"
user.first_name_changed?

自分で#first_name_changed?を定義した覚えもないのにこんなことができるのは、ちょっとしたマジックです。

こんなマジックが欲しいですよね?

Scoreクラスを作成し、スキーのジャンプ競技の詳しいスコアをここに保存することにします。スコアをspeed_score(スピード)、style_score(芸術)、landing_score(着地)にそれぞれ分割するとします。

class Score
  attr_accessor :speed_score, :style_score, :landing_score
end

今度は、スコアの種類ごとに点数が最高点に達したかどうかをチェックできるようにしたいと思います(10点を最高とする)。これを実装するためにActiveModel::AttributeMethodsattribute_method_suffixメソッドが使えます。

class Score
  include ActiveModel::AttributeMethods

  attr_accessor :speed_score, :style_score, :landing_score
  attribute_method_suffix '_max?'
  define_attribute_methods 'speed_score', 'style_score', 'landing_score'

  private

  def attribute_max?(attribute)
    send(attribute) == 10
  end
end

それではこのクラスで少し遊んでみましょう。

score = Score.new
score.speed_score = 5
score.speed_score_max? #=> false
score.style_score = 10
score.style_score_max? #=> true

プレフィックスメソッドも定義できます。

class Score
  include ActiveModel::AttributeMethods

  attr_accessor :speed_score, :style_score, :landing_score
  attribute_method_prefix 'reset_'
  define_attribute_methods 'speed_score', 'style_score', 'landing_score'

  private

  def reset_attribute(attribute)
    send("#{attribute}=", 0)
  end
end

score = Score.new
score.speed_score = 10
score.speed_score # => 10
score.reset_speed_score
score.speed_score # => 0

属性メソッドは、属性を別の形式に変換する場合にも便利です。たとえば、_uriサフィックスを追加して、URI.encode(attribute)で任意の属性をエンコードするなどです。やってみたくなりませんか?

⚓4. Railsコンソールをsandboxモードにして使う

productionデータベースダンプがあり、データに影響するコードをローカルでちょろちょろっとチェックしたいけど、コードの変更を反映したくない場合があります。こんなときはsandboxモードを使いましょう。rails cコマンド実行時に--sandboxオプションをつければsandboxモードがオンになります。

sandboxモードでコンソールを実行すると、データベースにどんな変更を加えてもコンソール終了後にロールバックされます。どうぞお楽しみください!

追伸: sandboxモードにするとレコードがロックされるので、production環境での利用には十分ご注意ください。

次回をお楽しみに!

新着記事を見逃したくない方はTwitterをフォローしてください。もちろん「hello」だけでも構いません!

お知らせ: RSpec & TDDの電子書籍を無料でダウンロード

もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。

関連記事

RabbitMQはSidekiqと同等以上だと思う: 前編(翻訳)

Rails: Form Objectと`#to_model`を使ってバリデーションをモデルから分離する(翻訳)

対決!Rails wayとHanami way: コントローラ編(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。


hanamirb.orgより

対決!Rails wayとHanami way: コントローラ編(翻訳)

このところ私の関心はHanamiに向かっており、作者のLuca Guidiがこれまで選んできたアーキテクチャ設計や決定が大のお気に入りです。

そこで私は、RailsとHanamiをコンポーネントごとに比較し、記事が長くなりすぎないようそれぞれ別記事を書くことに決めました。

本シリーズの第1回目です。MVCデザインパターンになぞらえて今回のタイトルは「コントローラ」で始めることにします。

それでは、RailsとHanamiそれぞれのフレームワークにおける実際のコントローラについて見ていきましょう。

コントローラの基本

Railsの場合

Railsのコントローラは、(その前にルーターが扱った)リクエストを受け取って処理し、出力をリクエスト元(クライアント)に送信する役割を担うクラスです。

コントローラ内にある複数のpublicインスタンスメソッドは、それぞれ異なるリクエストを処理します。次のコードをご覧ください。

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index; end     # GET /users
  def show; end      # GET /users/1
  def new; end       # GET /users/new
  def create; end    # POST /users
  def edit; end      # GET /users/1/edit
  def update; end    # PATCH /users/1
  def destroy; end   # DELETE /users/1
end

上でお見せしたのはRESTfulなコントローラの例です。これはRailsで何かを行う場合のベストプラクティスのひとつであり、アプリのリソースへのアクセスを提供する場合は常にRESTfulにします。

Hanamiの場合

Hanamiのコントローラはモジュールになっており、複数のアクション(これらはクラスです)を束ねる役割だけを担います。アクションは、(こちらもその前にルーターが扱った)特定のリクエストを処理して出力をクライアントに送信する役目を担います。

# apps/web/controllers/users/index.rb
module Web::Controllers::Users
  class Index
    include Web::Action

    def call(params)
    end
  end
end

# apps/web/controllers/users/show.rb
module Web::Controllers::Users
  class Show
    include Web::Action

    def call(params)
    end
  end
end
...

上の2つのアクションで、何が行われているかはだいたい見当がつくかと思います。

相違点

Railsの場合、必要に応じて1つのコントローラクラスにいくつものpublicインスタンスメソッドを持ちます。

Hanamiの場合、必要に応じて1つのコントローラモジュールにいくつものアクションクラスを持ちます。Hanamiのアクションクラスは、publicインスタンスメソッドである#callを定義するためだけに必要とされています。

どちらがどうよいのか

私の意見としては、Hanamiアーキテクチャの方がクリーン度が高いと思います。というのも、1つのアクションが1つのクラスになっていることで、「1つのクラスの責務はひとつでなければならない」という単一責任の原則を満たせるからです。

一方、Railsのコントローラの問題として非常によく知られているのは、コントローラクラスがだんだん巨大化して読むのもひと目で理解するのもつらくなってしまうという問題です。「コントローラは薄く、モデルは厚くせよ」というRailsのベストプラクティスはご存知かと思いますが、コントローラが肥大化する心配がまったくないに越したことはないとは思いませんか?

Hanamiの場合、アクションの肥大化はかなり簡単に回避できます。1つのクラスが1つのリクエストを扱うことが必須になっていますし、コントローラの複数のアクションでコードが重複したら、Ruby組み込みのソリューションであるモジュールが普通に使えます。繰り返し使われるコードを1つのモジュールに配置して、それを必要とするアクションクラスでincludeすればおしまいです。次の例をご覧ください。

# apps/web/controllers/users/set_user.rb
module Web::Controllers::Users
  module SetUser
    def self.included(action)
      action.class_eval do
        before :set_user
      end
    end

    private

    def set_user
      @user = UserRepository.new.find(params[:id])
      halt 404 if @user.nil?
    end
  end
end
# apps/web/controllers/users/show.rb
require_relative './set_user'

module Web::Controllers::Users
  class Show
    include Web::Action
    include SetUser

    def call(params)
      # ...
    end
  end
end
# apps/web/controllers/users/edit.rb
require_relative './set_user'

module Web::Controllers::Users
  class Edit
    include Web::Action
    include SetUser

    def call(params)
      # ...
    end
  end
end

キリリと引き締まったコードですね。

ビューテンプレートへの変数公開

コントローラは、データをビュー層に公開する役割を担います。HanamiとRailsのやり方は非常に似通っていますが、わずかに異なる点があります。

Railsの場合

Railsでは、コントローラ内で宣言したインスタンス変数はすべてテンプレート(ビュー)からアクセスできるようになっています。

Hanamiの場合

Hanamiの場合、exposeというクラスメソッドを使わないとインスタンス変数を公開できません。次の例をご覧ください。

# apps/web/controllers/users/index.rb
module Web::Controllers::Users
  class Index
    include Web::Action
    expose :users                     # @usersインスタンス変数を公開する

    def call(params)
      @users = UserRepository.new.all
      @another_instance_variable = {} #これはビュー/テンプレートからアクセス不可能
    end
  end
end

まとめ

ここまで読んだ方なら「RailsでやれることをHanamiでやろうとするとコード量が増える」とお思いかもしれませんし、実際そうかもしれません。Railsアーキテクチャの設計は「設定より規約(Convention over Configuration)」に依存する部分が多くなっています。Railsで作業していると魔法のように感じられるのはそのためです。

一方Hanamiの規約は少なく、意図を明確にする方向に開発者を強制します。私見ですが、コントローラのアクションを個別のクラスにするアイデアは実に素晴らしいと思います。1つのリクエストを受け取るときに何が起きるか、そこだけに注意を払えばよいのですから。このアーキテクチャは正しいように思えます。

本シリーズの次回では、Railsのモデル(ActiveRecord)とHanamiのモデルドメイン(エンティティとリポジトリ)が対決します。乞うご期待!

補足: RubyKaigi 2018にもHanami登場

明日からのRubyKaigi 2018の第一日目で、Anton Davydov氏がHanamiアプリのアーキテクチャをテーマに登壇します。


rubykaigi.org/2018/より

追記: ツイートより

関連記事

Hanamiフレームワークに寄せる私の想い(翻訳)

Ruby/Railsのプロ開発者としての5年間を振り返る(翻訳)

Rails:「Pagy」gemでRailsアプリを高速ページネーション(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails:「Pagy」gemでRailsアプリを高速ページネーション(翻訳)

私はRuby on Railsがバージョン0.8の頃から開発者として使っていることもあり、その間に誇大広告気味のRubyGemを星の数ほど目にしてきました。

成熟したソフトウェア開発コミュニティと同様、特定の用途では事実標準のgemとして永らえているライブラリもありますが、他の多くのライブラリは誰もサポートしてくれないまま、採用に値しない瓦礫の山を築いています。

用途によっては1つや2つgemをあえて追加することもありますが、ほとんどどの機能にもそれぞれに相応しいチャンピオンが君臨しているものです

Railsの進化がたどった道

以下の図は、ここ10年の間に新しいライブラリやアイデアにコミュニティが貢献した様子を示しています。

年ごとのRubyGem数

当初のgem数は増加していますが、2015年をピークに以後は減少しています(成熟したとも言えるでしょうか)。私は、2018年のgem数は2009年とほぼ同レベルであろうと睨んでおり、この傾向が続くならば新しいgemの数は今後も減少し続け、安定期に入るでしょう

ここからさまざまな疑問が湧き起こります。「Railsの潜在能力は2015年がピークだったのか?」「Railsというプラットフォームの採用も減少しているのか?」「むしろこれは成熟の証で、Railsは大企業が採用してよいものになったのだろうか?」

私としては、このデータだけでは何とも言えません。むしろそうした視点とは違うアプローチによって、今日の私たちの仕事に大きなインパクトをもたらす部分に着目してみたいと思います

新しいgemの数が減少するに連れて、今の私たちの仕事に大きく影響するような画期的なものはなかなか出現しなくなるだろうと考えられます。しかしそんなことはありません。それを今から証明いたします。

Pagyとの出会い

Ruby on Railsで開発したことがある方なら、アプリのindexページをwill_paginateやKaminariでページネーションしたことがおそらくあるでしょう。Rails経験の浅い方なら、今後そうしたページネーションgemを探すことになるでしょう。

PagyはRails向けの新しいページネーションライブラリです。パフォーマンスを念頭に置いて開発されており、かつ新規または既存のRailsアプリの使いやすさを損ないません。

はい、今皆さんが思っていることを当ててみせましょう。

「お、今度のページネーションgemこそ俺たちが求めていたもの…なわけないだろ!」

そこについては私も同感です。オープンソースでソリューションが分散してしまうのはよくないと思います。しかも、このPagy gemによってもたらされる改良点のいくつかは、既にKaminariでも実装が提案されたのですが、リジェクトされました。おそらく、改善にはKaminariのコア部分で大規模な変更が避けられなかったからでしょう。

何かが華々しく登場したら、ときにはそれが本当にベストなのかどうか一歩身を引いて考える必要があると、私は固く信じています。そしてページネーションについて言うなら、このgemには疑いの余地はありません。理由を説明しましょう。

pagy-kaminari-will_paginate-memory-used-per-page-shown

この図がすべてを物語っていると私は信じています

アプリが数百〜数千ものユーザーを同時にさばいている状態では、ちっぽけな機能を1つ足すだけでもリソースに大きな影響を及ぼすことは容易に想像が付きます。20ページに対して実施した1つのテストの結果を見ると、will_paginateはうまくやっている一方、Kaminariのメモリ容量は著しく増大しています。しかしPagyはwill_paginateよりさらに優秀です。数千人ものユーザーをさばこうとするとき、この違いがもたらすインパクトを今一度考えてみてください

さらに詳しく見てみましょう。今度はそれぞれのgemのメモリフットプリントを比較します。

pagy-kaminari-will_paginate-total-memory-used

Kaminariと同じジョブを、Pagyは遥かに少ないメモリで実行できます。will_paginateもKaminariより省メモリではあるものの、Pagyと比べると7倍にものぼります。さらに皆さまに考慮いただきたいのは、will_paginateが最後にリリースされたのは1年も前のことで、リポジトリにはプルリクが何十個もたまったまま、最終コミットの日付は2017年7月のままになっている点です。

今仮に新規プロジェクトがスタートすることがあったとしても、will_paginateは選択肢には含めないでしょう。

はい、皆さんはここできっとこう思うでしょう。

「よっしゃ気に入った。ところでPagyが他のgemと比べてメモリフットプリントがここまで少ない理由が謎なんだけど、どうやってんの?」

よくぞ聞いてくださいました。ご説明いたしましょう。

pagy-kaminari-will_paginate-number-of-objects-created

実は私も、当初同じ疑問を抱いたのでした。

「Kaminariが生成するオブジェクト数が6,000個超えってどういうこと?」

もちろんwill_paginateの方が省メモリではあるものの、それでも20ページのページネーションでオブジェクト数が3,000個超えというのはあんまりです。一方Pagyのオブジェクト数は400個を下回っています。

Pagyの方が優秀な理由

pagyのどこがそんなに違うのでしょうか?ご説明いたします。

  • PagyはRubyオブジェクトではなくintegerで計算している
  • Pagyのコアコードは60行にも満たない
  • Pagyはアプリのモデルに癒着しておらず、HTMLやURLや複数形化(pluralization)や式展開を独自に生成する。
  • Pagyは、一般的なヘルパーではなく完全に特殊化したコードを用いている。
  • コードが専門化していることで、作者が1行ずつ丁寧にベンチマークを取っている(コードが100行もなければ十分可能です)。

上の理由のおかげで、pagy gemのコードはきわめて理解しやすいという実にうれしいおまけまでついています。

しかも拡張で機能を追加することも可能で、いくつかの拡張は組み込み済みなのですぐにでも使えます。

さらなる考察

私がRailsアプリを開発した当初はwill_paginateを使っていましたが、その理由は当時はその業務にベストだったからです。数か月後にKaminariがリリースされると、そちらに乗り移りました(なにぶんあまりにも昔のことなので、乗り換えた理由はよく思い出せません)。そしてつい数週間前にPagyがリリースされるまで、Kaminariを使い続けていました。

私が初めてPagyを知ったときに心底愕然としたのは、私がwill_paginateからKaminariに移行したときにパフォーマンスのことをまったく考慮していなかったことでした。後者は前者よりずっと遅かったにもかかわらず、です。今の私はパフォーマンスを考慮することを覚えました。

これまで私たちが設計・リリースしたアプリで、いったいいくつのgemを取り入れてはパフォーマンスを下げまくっていたことでしょう。

will_paginateやKaminariからPagyへの移行は実に簡単で、必要なコードはわずか数行で済みます。次回の私の記事では皆さまに関心を持っていただけるよう、具体的な移行方法を解説することをお約束いたします。

免責事項: 本記事の画像およびパフォーマンスはPagyの作者が行ったベンチマークテストを用いています。ベンチマークのソースコードはddnexus/pagination-comparisonでご覧いただけます。RubyGem採用の推移チャートには、こちらでご覧いただけるRubyGems.orgのデータを用いております。

訳注: Pagyリポジトリ

関連記事

Rails: パーシャルと`collection:`でN+1を回避してビューを高速化(翻訳)

kaminariでRubyの地雷を踏んだ


Rails tips: Railsアプリに1行書くだけでチャートを作成できるchartkick/chartable gem(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Chartableは著者自身が作成したgemです。

Rails tips: Railsアプリに1行書くだけでチャートを作成できるchartkick/chartable gem(翻訳)

Railsアプリでチャートが使えたらとても素敵ですが、正しいデータを準備してからJavaScriptでビューに表示するのはつらいことがあります。そこで今回はChartkick gemとChartable gemを用いるソリューションをご紹介いたします。

Chartkick gemは、コードを1行書くだけで美麗かつ有用なチャートを表示できます。Chartable gemは、任意のActive Recordクエリを分析可能なハッシュに変換します。この2つのgemを用いることで、チャート作成のための新しい武器が手に入ります。

設定方法

最初にGemfileに以下のgemを追加します。

gem 'chartkick'
gem 'chartable'

bundle installを実行したら、次はチャートのレンダリングに使うJavaScriptライブラリを読み込まなければなりません。これを行うには、application.jsに以下の行を追記します。

//= require Chart.bundle
//= require chartkick

分析データを取得する

Railsで作成したブログエンジンがあり、そこにArticleモデルがあるとします。そして毎月何件の記事が作成されているかを表示したいとしましょう。これを行うにはクエリをひとつ実行しなければなりません。

@articles = Article.analytics(:monthly)

4月に1件、5月に5件の記事が作成されていた場合は、次のようなハッシュが取れるはずです。

{"April 2018" => 1, "May 2018" => 5}

チャートに表示する

それではデータをビューに表示しましょう。最初に申し上げたとおり、必要なのはこの1行だけです。

<%= pie_chart(@articles) %>

これで、以下のチャートが表示されるはずです。

できました!他のチャートに変更したい場合や期間の異なるデータを取得したい場合は、以下のgemページをご覧ください。


最新記事をチェックしたい方はぜひhttps://twitter.com/pdabrowski_k1のフォローをお願いします。github.com/rubyheroに掲載される新しいRuby gemもどうぞお見逃しなく!

お知らせ: RSpec & TDDの電子書籍を無料でダウンロード

もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。

関連記事

Rails: ページタイトルはビューテンプレートの`content_for`で表示すること(翻訳)

週刊Railsウォッチ(20180608)特集「RubyKaigi 2018後の祭り」、`Enumerable#index_with`は優秀、コントローラから`@`を消し去るほか

$
0
0

こんにちは、hachi8833です。RubyKaigi 2018の余韻がまだ体内に響き渡っている気がします。よい〜ん。

各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ

梅雨入り間近のウォッチ、いってみましょう。今回のつっつきは開始が遅かったので短めです🙇。

特集: RubyKaigi 2018後の祭り

参加した皆さま、本当にお疲れさまでした。
会のサポートやドリンクアップなどのパーティイベントで多くのスポンサーの皆さまにお世話になりました。ありがとうございます。


rubykaigi.org/2018より

現時点ではまだのようですが、動画やスライドへのリンクはいずれ昨年同様公式のスケジュールにアップされるでしょう。


つっつきボイス: 「ドキュメントの専門家の端くれとして思うことなんですが、『愛』とか『感動をもらう』とか『絆』みたいな言葉って世の中でともすると無難なワーディングとして乱用されがちですよね: 状況次第では『こういっとけばいいんだろ?』みたいな意図が図らずも透けて見えてしまったりとか」「まあそれが一周回って元に戻ったりすることもありますけどね😎」

来年の会場が決定

早くも来年のRubyKaigi 2019@福岡の会場が決まりました。

参考: 【福岡国際会議場】FUKUOKA CONVENTION CENTER


つっつきボイス: 「今度の会場は遠いなー: さすがに飛行機かな」「地図でもわかるように、福岡空港って市街地に隣接したところにあるんで着陸のときにマジで住宅街が真下に見えてビビりますね」「こえー😳」「その分、地下鉄わずか数駅で繁華街の中心である天神に到着できます😊」

福岡は台湾や韓国からめちゃめちゃ近いので、そちらからの参加者も増えそうですね。あと沖縄も。

乗ったことはないのですが、福岡-釜山は通常の巡航高度に達しないうちに到着するそうです。

a_matsudaさんインタビュー

RubyKaigi 2018直前に公開された記事なので多くの方が既にお読みかと思いますが、Ruby/Railsどちらの方にも有用な話が多く、直接の関係はそれほどないにしてもRubyKaigiのナイスな副読本に思えました。インタビュアーはこれまたRubyKaigi 2018でも登壇したsotarouさん(SideCIのCTO)です。


つっつきボイス: 「お、この方がa_matsudaさんでしたか(今回初参加なのー☺️)」「ですです: a_matsudaさんはRubyとRails両方のコミッターを務めていて、RubyKaigiのオーガナイザーの一人です」「なるほど道理で司会いっぱいやってた」「Ruby/Rails両方のコミッターって、a_matsudaさん以外にはtenderloveことAaron Pattersonさんしか知らないんですが、他にいらっしゃるかな…?」

ご存知の方は@hachi8833までどうぞ🙇。

[インタビュー] Aaron Patterson(前編): GitHubとRails、日本語学習、バーベキュー(翻訳)

「インタビュー読んでて個人的に一番驚きだったのが、CRubyのソースコードに統一スタイルがないという点でした」「Kaigiでもどこかでそんな話出てましたね」「ご本人がリツィートしてたこれがわかりやすいです↓: 出元はKaigiでTRICKの司会を務めたmametterさん」「おっおー、タブ派こんなに多いし!😳」「C言語ですし歴史長いし、しょうがないのかな…」

まとめ情報

各種記事などを途中まで追っていたのですが、zundanさんが上記るびま特集号でガッツリまとめてくださっていることに気づいたので、るびまから辿るのが一番だと思います。今後の更新にも期待します🙇。

TechRachoでは、Kaigiのまとめ情報を急いで出すよりは、ウォッチなどで今後じわじわ追いかけることにしようかと考えています。

後はTwitterで以下のハッシュタグを眺めるのがいいかもです。

TRICK 2018のまとめはひとまず以下で。

同ハッシュタグのツイートを紹介していると本気できりがないので、泣く泣く1/10以下に絞り込みました。

こちらの↓Rubyアンケートも楽しいですね。

Rails: 今週の改修(Rails公式ニュースより)

今回はすべてRails公式情報からです。

(Rails 6)Enumerable#index_withが追加

# PRより
# before
POST_ATTRIBUTES.map { |attr_name| [ attr_name, public_send(attr_name) ] }.to_h

# after
POST_ATTRIBUTES.index_with { |attr_name| public_send(attr_name) }
# こんなこともできる: Enumerableをハッシュに変換するときとか
# before
WEEKDAYS.each_with_object(Hash.new) do |day, intervals|
  intervals[day] = [ Interval.all_day ]
end

# after
WEEKDAYS.index_with([ Interval.all_day ])
# https://github.com/rails/rails/pull/32523/files#diff-33d6b41213775779b35c3f31915e9f98R60
+  # Enumerableの項目をキーにし、ブロックで戻された値をハッシュに変換する
+  #
+  #   post = Post.new(title: "hey there", body: "what's up?")
+  #
+  #   %i( title body ).index_with { |attr_name| post.public_send(attr_name) }
+  #   # => { title: "hey there", body: "what's up?" }
+  def index_with(default = INDEX_WITH_DEFAULT)
+    if block_given?
+      result = {}
+      each { |elem| result[elem] = yield(elem) }
+      result
+    elsif default != INDEX_WITH_DEFAULT
+      result = {}
+      each { |elem| result[elem] = default }
+      result
+    else
+      to_enum(:index_with) { size if respond_to?(:size) }
+    end
+  end

つっつきボイス: 「おー、ハッシュが返ってくるということですか: beforeみたいにmapからto_hでつなげてハッシュを取るというのは定番で、ボクもよくやるけど、む、むむ、この新しいEnumerable#index_withメソッドは優秀ですね〜!😍」「やっぱりうれしいヤツだったんですね」「afterのこのシンプルさ☺️!: Rails 6から使えるですかー」「masterなんでそうなりますね: これRuby本家にあってもいいヤツかもー」「( ・∀・)イイ!!」

PRのコメントにも同じ意見がありました。

Wanna to have this method in ruby too! 👍
同PRより

(Rails 6)mail gemを自動でeager load

production環境で、mail gem内のファイル自動読み込み中にSidekiqのワーカースレッドが全部デッドロックするという問題が発生したことがあった。このmail gemは、config.eager_load = trueを設定したにもかかわらずActionMailerでrequireされていた。mail gemはActiveSupportのではなくRubyネイティブの自動読み込みを使っているので、ActiveSupportが自動でeager loadingできない。Mail.eager_autoload!というメソッドがあるしちゃんと動くのに、単にActionMailerがeager loading中にこれを呼んでなかった。この呼び出しをActionMailer Railtieのeager_load_namespacesに追加したことで、eager_load!イニシャライザの実行中にMail.eager_autoload!呼び出しされるようになった。
同PRより大意

# https://github.com/rails/rails/pull/32808/files#diff-d8d52836afe031f24a03fc96d5d6c8c4R56
+  def self.eager_load!
+    super
+
+    require "mail"
+    Mail.eager_autoload!
+  end
 end

つっつきボイス: 「短かったんで雑に訳してみました」「これはもろにバグ🕶」

「ところですっごく初歩的なこと聞いちゃいますけど、superってこうやってメソッド定義の冒頭に書くこともあるんですね」「えっ?自分はむしろsuperと言えば冒頭に書く方が普通かと思ってたけど」「あー、たまたまメソッド定義のラストでsuperを呼ぶコードを立て続けに見る機会があったんで思い込みしてたかも…失礼しましたー😅」「このコードの継承関係から言うと、まずsuperで元のを呼んで、それからゴニョゴニョすると: ちなみにJavaでは先頭にしか書けません🧐、確かコンストラクタでは、だったかな」「おー」

参考: 【Java】 継承とコンストラクタ super( )の意味 | 一番かんたんなJava入門

「逆にRubyではsuperは自由に置けるんで、今言ってたように先にゴニョゴニョしてからsuperを呼ぶこともできる(superに別の引数を渡したいときとかね🤓)」「そうでしたかー: super慣れしてなくって」「まーあんまり慣れたくはないですが😆」

参考: メソッド呼び出し(super・ブロック付き・yield) (Ruby 2.5.0)

(Rails 6)Range#===Range#cover?

ruby/ruby@989e07c(訳注: 原文リンク切れ)から、Range#===で従来のinclude?に変えてr_cover_p内部関数を使うようになった。このため、少なくとも8b67a02からActiveSupport::CoreExt::Rangeの挙動がドキュメントどおりにならなくなった。

このパッチはRange#cover?Range#===をオーバーライドして3つをすべてCompareWithRangeという1つのモジュールに配置した。
失敗時のログはhttps://travis-ci.org/rails/rails/jobs/380939901#L1224-L1247を参照。
同PRより大意

# https://github.com/rails/rails/pull/32938/files#diff-1b097291d613c82352e751d35befbea6R2903
+(1..10) === (3..7)  # => true
+(1..10) === (0..7)  # => false
+(1..10) === (3..11) # => false
+(1...9) === (3..9)  # => false
+
 (1..10).include?(3..7)  # => true
 (1..10).include?(0..7)  # => false
 (1..10).include?(3..11) # => false
 (1...9).include?(3..9)  # => false

-(1..10) === (3..7)  # => true
-(1..10) === (0..7)  # => false
-(1..10) === (3..11) # => false
-(1...9) === (3..9)  # => false
+(1..10).cover?(3..7)  # => true
+(1..10).cover?(0..7)  # => false
+(1..10).cover?(3..11) # => false
+(1...9).cover?(3..9)  # => false

つっつきボイス: 「これはテストの方が見やすいかなと思って」「ここが直感とズレると確かにつらいわー」

(Rails 6)xor_byte_stringsを70%高速化

# 同PRより
# Ruby 2.5.1の場合
Warming up --------------------------------------
             current     6.519k i/100ms
                 new    10.508k i/100ms
Calculating -------------------------------------
             current     84.723k (_ 0.4%) i/s -    423.735k in   5.001456s
                 new    145.871k (_ 0.3%) i/s -    735.560k in   5.042606s

Comparison:
                 new:   145870.6 i/s
             current:    84723.4 i/s - 1.72x  slower
# https://github.com/rails/rails/pull/32931/files#diff-c07f67f5a7946bbd9c38ba8283aea967L402
       def xor_byte_strings(s1, s2) # :doc:
-        s2_bytes = s2.bytes
-        s1.each_byte.with_index { |c1, i| s2_bytes[i] ^= c1 }
-        s2_bytes.pack("C*")
+        s2 = s2.dup
+        size = s1.bytesize
+        i = 0
+        while i < size
+          s2.setbyte(i, s1.getbyte(i) ^ s2.getbyte(i))
+          i += 1
+        end
+        s2
       end

つっつきボイス: 「例のpack()を使わない方向で書き直してるからそのおかげで速くなったのかな?、と思ったら、コメントを見るとループをwhileでやったことの効果が大きかったみたい: 元のコードだと内部のyieldが遅いとかありそうではある」

「PRのコメントでも『whileで書くのはパフォーマンス上は有利だけどRubyらしくない(C言語みたくカウンタ変数i使っちゃってるし)かなー: Rubyらしくブロックのループで書くと少し遅いは遅いけど』みたいな意見がありますね」「その後で『Rubyでミクロな最適化を追求するあまり可読性を損なうのはどうかとは思う: Rubyってたぶんそういう言語じゃないし』『いや、場合によっては可読性を下げてでもパフォーマンスを取る方がいいこともあるし』みたいなやりとりの末マージされました」「whileループが死ぬほどでかいなら止めた方がいいと思うけど、これならまあ読めるし、20%速くなるならやってみていいんじゃね?」

# 同PRより
def xor_byte_strings_new(s1, s2) # :doc:
  s2 = s2.dup
  s1.size.times { |i| s2.setbyte(i, s1.getbyte(i) ^ s2.getbyte(i)) }
  s2
end

参考: pack テンプレート文字列 (Ruby 2.5.0)

(Rails 6)子トランザクションで保存に失敗した場合に親トランザクションをロールバックするよう修正

# 同PRより
class Employee < ActiveRecord::Base
end

class Company < ActiveRecord::Base
  has_many(:employees)
end

company = Company.new
employee = company.employees.new
company.save # ここで失敗しても親がロールバックしなかった
# https://github.com/rails/rails/pull/32796/files#diff-829fd5510b886395117cc530518ef7f7R400
               if autosave != false && (@new_record_before_save || record.new_record?)
                 if autosave
                   saved = association.insert_record(record, false)
-                else
-                  association.insert_record(record) unless reflection.nested?
+                elsif !reflection.nested?
+                  association_saved = association.insert_record(record)
+                  if reflection.validate?
+                    saved = association_saved
+                  end
                 end
               elsif autosave
                 saved = record.save(validate: false)
               end

つっつきボイス: 「これもマジでバグじゃん😭!」「トランザクションの一貫性、大前提ですよね」

(Rails 6)実際の(=savepointにない)トランザクションの後にトランザクションのレコードをfinalize

# https://github.com/rails/rails/pull/32911/files#diff-52c8dd8e01c039c37b33b3fcbdfe406aR19
       def committed?
-        @state == :committed
+        @state == :committed || @state == :fully_committed
+      end
+
+      def fully_committed?
+        @state == :fully_committed
       end

       def rolledback?
-        @state == :rolledback
+        @state == :rolledback || @state == :fully_rolledback
+      end
+
+      def fully_rolledback?
+        @state == :fully_rolledback
       end
...
       def rollback
         connection.rollback_to_savepoint(savepoint_name)
-        super
+        @state.rollback!
       end

       def commit
         connection.release_savepoint(savepoint_name)
-        super
+        @state.commit!
       end
# https://github.com/rails/rails/pull/32911/files#diff-174733f2db65ef1bc53e3222d6ac0e61R473
       def update_attributes_from_transaction_state(transaction_state)
         if transaction_state && transaction_state.finalized?
-          restore_transaction_record_state if transaction_state.rolledback?
+          restore_transaction_record_state(transaction_state.fully_rolledback?) if transaction_state.rolledback?
+          force_clear_transaction_record_state if transaction_state.fully_committed?
           clear_transaction_record_state if transaction_state.fully_completed?
         end
       end

つっつきボイス: 「述語メソッドの方で|| @state == :fully_committedを追加して、下の方で@state.なんちゃらを明示的に呼ぶようになったと」

(Rails 6)SQLite3の最小バージョンが3.8に変更


つっつきボイス: 「SQLite3を通常業務で使うことはあまりないかなと思ったんですが、チュートリアルなどをやる人は押さえておくといいかなと思って」「| ゜Θ゜)<そうでもないよ: 雑にrails newして急いでモック作って渡したいときとかにSQLite3ありがたいし: この間も使ったし」「あ、そうか: 機動性いいですよね」「SQLite3のデータはファイルベースなんで、リポジトリ経由で渡すときにとっても楽☺️: 捨てるときも楽☺️」「git cloneすれば即動きますしね」「別にぽすぐれでもいいんだけどね、わざわざサーバー立ててテーブル作ってとか相手にやってもらうのは面倒だし」「SQLite3のビルド、ネイティブなんでたま~にコケますけど😎」「げ😫」

以下もSQLite3の改修です。


追いかけボイス:

参考: SQLite 3.24 Released With UPSERT Support - Phoronix

Rails

Railsのコントローラから@を消し去る(Hacklinesより)


つっつきボイス: 「お、@消し去るマン!こういうの大好きー😌」「まずは冗長な書き方: Rails wayから外れるけど」「ほー、コントローラでrenderでやるっすか!」

# 同記事より
def show
  user = User.find params[:id]
  render :show, locals: { user: user }
end

「でもって、コントローラは(概念上は)要するに以下をやればいいんだろ↓みたいな話」(以下延々)

# 同記事より
controller = UsersController.new request
response = controller.show

「いろいろあって、最終的にLettableというモジュールのletメソッドでやるんだそうです」

# 同記事より
module Lettable
  def let name, &blk
    iv = "@#{name}"

    define_method name do
      return instance_variable_get iv if instance_variable_defined? iv
      instance_variable_set iv, instance_eval(&blk)
    end
    helper_method name

    define_method :"#{name}=" do |value|
      instance_variable_set iv, value
    end
    private :"#{name}="
  end
end

「これいい記事じゃない!自分も以前同じような解に辿り着いたことあったし: この記事のやり方以外にヘルパーメソッドでやる手も考えられるナ」「😀」「Rails wayから外れるのは確かだからチーム内でコンセンサスは取らないといけないけど、考え方としては大好きなヤツ😍: ビューに@で渡すのはもう仕方ないとして、それ以外はこういうのでやりたい気持ち」「おー」「読み進んでみるとやっぱりちゃんとヘルパーメソッドでもやってみせてくれてるし、やるなー」

「記事の最後の方に、TechRachoにも何度か登場している↓decent_exposure gemから実は影響を受けたってありますね」「あーあれですか、コントローラにload_resourceを書くというナニな手法の中で軽く紹介されてた: でもあれとは違ってこのやり方にはコピペの匂いがしないし、筋がいいと思う」「letってRSpecみたいだけどこれなら紛らわしくはなりにくそう」「Lettableって凄い名前だけど😆」「@インスタンス変数でやる方法しか知らない方にも知っておいて欲しいです: Rails中級向けだけど初心者にもいい記事」「これ翻訳依頼かけますね😀」


hashrocket/decent_exposureより

Railsコードを改善する7つの素敵なGem(翻訳)

「話は逸れますが、記事にも出てくるboilerplateって単語聞いたことあります?」「うんにゃ」「これはローカライズ業界で知った用語なんですが、契約書的な硬い文書なんかによくある、いわゆる定型文を指すんですよ: ボイラーの板が何で定型文なのかはよくわかりませんが🤣」「2ちゃんで言う『テンプレ』ですな🤓」

ローカライズでは、boilerplateと指定されているパラグラフはフリーズされて、間違って翻訳されないように処置されたりします。辞書で調べると、boilerplateはどうやら印刷用の鉛版から来ているようです。

ついでにdecent exposureの意味も。

個人情報などの適度な露出。インターネット上のブログやツイッターなどで、公開していい情報と伏せるべき情報の区別をわきまえること。特定の個人がかかわる情報を書き込むとき、無断で個人名を書き込まないなど。
decent exposure | アメリカ新語 | 情報・知識&オピニオン imidas - イミダスより

Railsのルーティングを複雑にしない方法(RubyFlowより)


つっつきボイス: 「お、このトピック興味あるある👀」「割と短い記事ですね」「お、ルーティングのファイルが分かれてる↓😳?!」「ほんとだ: これ今まで見たことなかったんですけど」「これ思いつかなかったわー💦: ルーティングをきれいに切り分けられるんだったら、これはありかも: ただきれいに切り分けるのって割と難しいんですよ:『どれに書いたらいいんだ問題』とかフォルダ切らないといけないとかもあるし」「でしょうねー」「sidekiqとかdeviseとかの単位ならまあやってやれないことはないかも?」

squeel gemとbaby_squeel gem

以下の記事でsqueel gemのつらみが語られていて、最後にbaby_squeel gemが紹介されていました。

参考: さよならSqueel - Qiita


つっつきボイス: 「Squeel、知ったときにはもうサヨナラだったという」「ほほー、Arelをラップするコマンド的なやつが昔にあったんですかー」「で、上の記事にこんなこと書いてありました↓」

Squeelはたしかに便利なのですが、大きな問題点として「ActiveRecordを大胆にモンキーパッチしている」ということがあります。その結果、ActiveRecordがバージョンアップするたびに、大規模な対応作業が必要となってしまう、という構造上の欠点があります。
同記事より

「(ノ∀’)アチャー、Arelってちゃんと公開されてるAPIじゃないし、そうなるわなー: ActiveRecordで<とか>とか使いたい気持ちはとてもよくわかるけど(どうして使えないんだっ😤)」「😢」「Ransackならこういう比較演算子使えるんですけどね」「baby_squeelの方はパッチではなく別メソッドでやってるんですね: 赤い長靴はいた子豚ちゃんがかわいい🐷」

「こういうgemがあるとid.count > 5みたいな書き方ができるんですね↓」「そうそう: そういうのがないと文字列しか渡せないんで?でサニタイズして渡したりとか面倒」「セキュリティ作法👮‍♂️」

# baby_sqeel READMEより
Post.selecting { id.count }.grouping { author_id }.when_having { id.count > 5 }

AWS EC2とChefでRailsアプリを設定する(Hacklinesより)

かなり長いですがひたすら手順です。

RailsのjQueryをVueに完全置き換えする(RubyFlowより)


同記事より

// 同記事より
$(document).ready(function(){
   ...
   $('#some-radio-button1').on('click', function(){
     if ($(this).is(':checked')) {
       // removing "active" classes, hiding some blocks
       // showing related block
     } else {
       // opposite of above
     }
   });
});

つっつきボイス: 「jQueryをたっぷりトッピングしたRailsアプリをVueに移行したい人って多いんでしょうね」「この記事はVueも使えるようにした、って感じなのかな?」「そういえばそんなに長い記事ではないですね」「BootstrapがjQuery必須だから、Bootstrapがある間は当分切り離せないでしょうね…」「Bootstrapで使われるくらいのjQueryは別にあってもいいかなと: 乱用しないようにルール作りとかしておけば」

「jQueryで厄介なことがあるとすれば、ネットに落ちている野良コードを拾い食いしてあたるパターン😩、しかも他の人が拾ってコピペしたやつに」「それ、私も自分で踏んだ…」「何回やられたかもう覚えてないくらい😭」

後でこんな記事を見つけましたが、昨年夏の記事でした。今無理してjQueryを完全に殺すとどこかに無理が出そうな予感です。

参考: Drop jQuery From Your Bootstrap Project (and Replace it With Vue.js!) - DZone Web Dev

JavaScriptスタイルガイド 1〜8: 型、参照、オブジェクト、配列、関数ほか (翻訳)

gitignore.io: プロジェクトに最適な.gitignoreファイルを作成できるサイト

他にもこんなのがあるそうです。

TDD伝道師インタビュー

つっつきボイス: 「t-wadaさん!」「『設計だけでコードを書けないなら断る』って最初どの意味だろうって考えちゃいましたが、読み進めてみると『設計だけやれ/コードは書くな、なら断る』ってことだったんですね」「😆」「職業柄、文章を読むときに意味をいったん可能な限り広く取ってから絞り込む癖があるんです: 人によって解釈がブレてしまう可能性を考慮するんで、自分の文章ならそういう部分をリライトすることが多いですね🙃: 急いでるとなかなかやれないんですけど」

「まーでも設計だけやらざるを得ない状況はあるけどねー」「ソフトウェアだと設計と実装って分かれているようで不可分というか、建築家と大工さんのようには分かれてないですよね」「実装を経験せずに設計だけやるというのはソフトウェアでは無茶だし、設計で無駄な実装を減らすためにも実装を知ることは不可欠だし: 自分はプログラマーとはイコール設計する人だと思ってます」「😀」「対偶を取ったら、設計できない人はプログラマーではない🕶」

その他Rails

開発チームを苦しめるマイクロサービス(翻訳)


私も翻訳で関わってきたRailsチュートリアルRailsガイドでおなじみのYassLabさんが株式会社化したそうです。おめでとうございます🎉。


yasslab.jpより

Ruby trunkより

今回は主にRubyKaigi 2018から拾う形にしました。

参考: RubyKaigi 2018 2日目 - ENECHANGE Developer Blog

Guildがマージ

yield_selfのエイリアスとしてthenが追加

RubyKaigiの大喜利でも「もっといい名前にしたい」で盛り上がったネタですね。

File.read(filename).then(&JSON.method(:parse))
rand(10).then.detect(&:odd?)


つっつきボイス: 「Arelにthenならギリセーフかな: privateなものなんだし、みんなも使わないでしょ😆」「😆」

Ruby 2.5の`yield_self`が想像以上に何だかスゴい件について(翻訳)

提案: autoloadやめない?

↑大喜利で参照されてたのはこれでよかったのかな…(後でチェック)。


つっつきボイス: 「これは大喜利でRails勢がざわついてましたね☺️: autoloadをやめたい理由って何なんでしたっけ?」「autoloadをやめるとその分起動が重くなるかわりに、動的にバインディングされなくなってライブラリが確定するからなんですね」「あーそうだったかも!」「Matzもたしかそんなことを壇上で言ってましたよね」「autoloadでproduct環境がぐらついたら困るわけですよ: まったく同じ構成のアプリなのにデプロイ先でライブラリの読み込み順序やタイミングのせいで挙動が同じにならなかったりしたらコワイ」「確かにー」「その一方で、今回昔の知り合いにKaigiの場で久々にお会いしてこの話題になったときに『肥大化したコードではインフラに応じた分岐できないから、autoloadがあると使われないコードがロードされないのでautoloadはいいヤツだと思う』ということもおっしゃってましたね」「なるほどー」

提案: Refinementやめない?

(issueがうまく見つけられませんでした🙇)


つっつきボイス: 「大喜利でこの話になった途端ツイートでお二人が😀」「いやーほんとRefinement使ってる人こんなにいるとは」

RubyのRefinement(翻訳: 公式ドキュメントより)

Ruby

RubyKaigi 2018開催中ににRuby 2.6.0-preview2がリリース(Ruby公式ニュースより)

RubyKaigiの主に大喜利でだいたい触れられていましたね。


  • JITの改良
  • RubyVM::ASTの導入
  • Proc#callblock.call
  • $SAFEのステートがプロセスグローバルに
  • etc.

RuboCop 0.57リリース

RuboCop RSpecのリポジトリも移動したようです。

JSのコールバックやクロージャをRubyのyieldとlambdaでやってみる(Hacklinesより)

// 同記事より
Array.prototype.myMap = function(cb) {
  const mappedArr = [];
  for (let elem of this) {
    mappedArr.push(cb(elem));
  };
  return mappedArr;
};
[2,4,6].myMap((num) => { return num * 3 }); 
// [6, 12, 18]

Steep: Rubyの型チェック

# 同リポジトリより
class Person
  # `@dynamic` annotation is to tell steep that
  # the `name` and `contacts` methods are defined without def syntax.
  # (Steep can skip checking if the methods are implemented.)

  # @dynamic name, contacts
  attr_reader :name
  attr_reader :contacts

  def initialize(name:)
    @name = name
    @contacts = []
  end

  def guess_country()
    contacts.map do |contact|
      # With case expression, simple type-case is implemented.
      # `contact` has type of `Phone | Email` but in the `when` clause, contact has type of `Phone`.
      case contact
      when Phone
        contact.country
      end
    end.compact.first
  end
end


つっつきボイス: 「Kaigiでも発表されたSteepはMatzの考える型チェックに近そうですね」「そんな感じー: 型アノテーションはしたくないけど指定や推定はしたいと」「Rubyのシンタックスは変えたくないということなんでしょうね」「型アノテーションだとシンタックス拡張になりますし」

「あれ、私ちょっと勘違いしてたかも? アノテーションって#のコメントに型情報を書くことかと思ってました😓」「ここでMatzがRubyに入れたくないと言ってる型アノテーションはJavaで言っているヤツのことでしょうね: Javaには@で型アノテーションする拡張シンタックスがあるので」「そうでしたか~、アノテーションにもいろいろあるけど、その中にJavaの型アノテーションがあるということか」

参考: Java SE 8再入門、型アノテーションとは何か | 日経 xTECH(クロステック)

「そういえば誰かが『本当に欲しいのは型じゃなくって、優秀なIDEなんじゃ?』ってツイートしてました」「ホントそう思うですよ~🙂: ちゃんとは知らないんですけどPythonは型チェックのあたりが何やら成功したっぽいんで、Rubyでもやってよという形で押し寄せてるってことなんでしょうね」(以下延々)

追伸: sorbet.run: StripeによるRubyの型チェック(RubyFlowより)


sorbet.runより

Stripeと言えば決済代行サービスで有名ですね。Sorbet.runは3月にリリースされ、RubyKaigi 2018にも登壇してたのですが、今回見られなかった(´・ω・`)。


stripe.comより

参考: A practical type system for Ruby at Stripe. - RubyKaigi 2018

他にもType Systemといえば、Dmitry Petrashko, Paul Tarjan, Nelson Elhage A practical type system for Ruby at Stripe.があります。彼らはStripeの方ですが、なんとStripeにある数百万行ものRubyのコードには漸進的に型を適用するしくみがあるそうです。本当なのか。この発表はその取り組についての紹介になる予定です。
RubyKaigi 2018 タイムテーブル徹底解説より(強調は編集部)

Stripe決済を自社サービスに導入してわかった5つの利点と2つの惜しい点

EnvMem: メモリプロファイラ

Noah Gibbsさんのプレゼンにも登場した本人作のgemです。Rubyの環境変数でパフォーマンスチューニングするうえで便利なプロファイラです。自分でも動かしてみました。

↓このあたりの記事を翻訳していなかったらプレゼン追いきれなかったと思います。

Rubyのヒープをビジュアル表示する(翻訳)


つっつきボイス: 「Noah Gibbsさん、去年の広島でのRubyKaigiにもお見えだったのに自分がまったく気づいてなかったので😢、今回リベンジで記事翻訳許可のお礼を述べました: クマのぬいぐるみといつも一緒で、スライドの冒頭にもクマちゃん映ってました」

Ruby 2.5.0はどれだけ高速化したか(翻訳)

その他Ruby


つっつきボイス: 「pentomino?」「あー、何だったっけこれ…(画像でググる)」「これかー↓」「テトリスとは微妙に形が違う」「実はこのパズル苦手😓」「まさか!数学専攻だったのに?」「これ系のパズルは解けても何故かあまり快感を感じられなくて…(群とか集合とかが好きなのっ)」


  • 「Tell Don’t Askの原則」: 初めて知りましたが、Martin Fowlerさんのブログにありました。あえて言うなら「オブジェクトにデータがあるかどうかいちいち聞くな、指示だけ出せ」という感じでしょうか。(RubyFlowより)


クラウド&コンテナ

AWS CodeCommitとは

アレクサとAWSのAI


aws.amazon.comより


つっつきボイス: 「記事の内容云々より、技術系でない一般系のニュースサイトにこうやってAWSのチャートが掲載されたことが感慨深かったので🤓」「次はテレビですな📺」

みずほ銀行のプロジェクトがいよいよ

クラウドとはちょっと違いますが。

SQL

DB対決: Cassandra vs TimescaleDB(Postgres Weeklyより)


timescale.comより

TimescaleDBは前回のウォッチで取り上げましたが、Cassandraは初めて知りました。


https://ja.wikipedia.org/wiki/Apache_Cassandraより

参考: Apache Cassandra - Wikipedia

その名前から、ついこちらのいい話を思い出してしまいます。

アポローンに愛され、アポローンの恋人になる代わりに予言能力を授かった。しかし予言の力を授かった瞬間、アポローンの愛が冷めて自分を捨て去ってゆく未来が見えてしまったため、アポローンの愛を拒絶してしまう。憤慨したアポローンは、「カッサンドラーの予言を誰も信じないように」という呪いをかけてしまった。カッサンドラーは、パリスがヘレネーをさらってきたときも、トロイアの木馬をイリオス市民が市内に運び込もうとしたときも、これらが破滅につながることを予言して抗議したが、誰も信じなかった。
カッサンドラー - Wikipediaより(強調は編集部)


つっつきボイス: 「この話、お話づくりの雛形としてすっごくよくできてて好きなんです😁: アポローンも予言能力を与える前にそれに気づけとw」「😁」「ギリシャ神話で面白いのが、神には『一度言ったことを取り消す能力がない』という重大な設定上の縛りがあることなんですね: せっかく超能力があっても万能感台無し🤣」「ロールバック不可🥖」

神のこの制約を逆手に取ってチートをかけるしたたかな人間すら登場しています。

里中満智子は「マンガ・ギリシア神話7」でこの場面を、カッサンドラーとやりたい一心のアポローンがカッサンドラーの気を引こうとしてつい予言能力をプレゼントしてしまったと解釈していました。

そんなこんなでカッサンドラという名前は昔から不吉なニュアンスがあったのですが、今は割と普通に女性名に使われてるようです。

PostgreSQLのストレージをビット操作で効率アップ(Postgres Weeklyより)

odyssey: PostgreSQL向けマルチスレッドコネクションプール/リクエストルーター(Postgres Weeklyより)

JavaScript

Jest 23がリリーズ(JavaScript Weeklyより)

参考: Jest 23 リリース: 🔥 爆速で快適なテストを【日本語翻訳】 | maesblog

TypeScript 2.9がリリース(JavaScript Weeklyより)

Node.jsにおける設定ミスとは

deno: V8向けのセキュアなTypeScriptランタイム(JavaScript Weeklyより)

CSS/HTML/フロントエンド

Styled-componentsとは


styled-components.comより

// styled-components.comより
const Button = styled.a`
  /* This renders the buttons above... Edit me! */
  display: inline-block;
  border-radius: 3px;
  padding: 0.5rem 0;
  margin: 0.5rem 1rem;
  width: 11rem;
  background: transparent;
  color: white;
  border: 2px solid white;

  /* The GitHub button is a primary button
   * edit this to target it specifically! */
  ${props => props.primary && css`
    background: white;
    color: palevioletred;
  `}
`

GDPRで右往左往

無料で使えるアールデコ・アイコン

ただしAdobe XD専用でした(´・ω・`)。

参考: アール・デコ - Wikipedia

左がアール・ヌーヴォー、右がアール・デコ(対称が特徴)だそうです。

スライド: テスト自動化あるある

ITエンジニアに眼福を

15,000円のかき氷。

ITエンジニアをやっていて、IT以外の世界につい無関心になってしまうというのは割とありそうなので、料理に限らずいろんな分野のトップクラスの「いいもの」に一度は触れる機会を持てるとよい気がします。
本当においしい食べ物が、普段の食べ物とまったく次元を異にする別世界だった体験が忘れられません。

言語よろずの間

yew: RustのWebフレームワーク

// 同リポジトリより
use yew::services::console::ConsoleService;
use yew::services::timeout::TimeoutService;

struct Context {
    console: ConsoleService,
    timeout: TimeoutService<Msg>,
}

impl Component<Context> for Model {
    fn update(&mut self, msg: Self::Message, context: &mut Env<Context, Self>) -> ShouldRender {
        match msg {
            Msg::Fire => {
                let send_msg = context.send_back(|_| Msg::Timeout);
                context.timeout.spawn(Duration::from_secs(5), send_msg);
            }
            Msg::Timeout => {
                context.console.log("Timeout!");
            }
        }
    }
}

最近のGo言語 by JetBrains


jetbrains.comより

futureとtaskがRustの標準ライブラリ入り

その他言語




その他

マイクロソフトがGitHubを75億ドルで獲得

既にさんざん話題ですが一応。


blog.github.comより

↑以前はなかったディズニー成分がとっても気になります。

参考: これでわかる!マイクロソフトがGitHubを買収する理由 - M&A Online

インテリジェントな耳栓

普段の会話は聞こえ、大音量になると自動的に

だいぶ昔によくイベントでお付き合いしていたPA会社があったのですが、その会社が担当する野外ライブが年を追うごとに明らかにどんどんボリュームがアップしていて、「もしかして担当者さん、難聴始まってるのでは?」と慄いたことがあります。

何で読んだか思い出せませんが、この有毛細胞は大音量にさらされると抜けてしまい、一度抜けると二度と再生しないのだそうです。

参考: 音響外傷 - Wikipedia

英語の時制


科学論文における時制の使い分け | Editage Insightsより

見積もり


つっつきボイス: 「ガチ見積もりです」「ガチ」

「Dependency Injection」の訳語とは

誤訳と聞いて。


つっつきボイス: 「『の』を入れるかどうかって難しいですよね…」「『の』を使うとどうにでもつながっちゃいますし…」

「依存性(を必要なときだけ外部から)注入(する手法)」の略だと思えば、元の語がそもそも略しすぎな気もしました。

なお、業界が変わるとDIが指すものも変わります。

参考: DI(ダイレクトボックス)について|サウンドハウス — 音響関連
参考: Direct instruction - Wikipedia — 初等教育法の一種

教師の教え方を完全に型にはめるという意味で型破りな教育法。教師は最初から最後までシナリオに沿ってテンポよく進め、生徒(少人数が前提)にもテンポよく一斉に答えさせる。完全にマニュアル化され、教師に何の専門性も創造性も要求しないにもかかわらず、絶対計算による吟味の結果、ほかのどんな教育法よりも優れた結果を出していることが判明している。DIで育った生徒は単にすべての成績がよいだけでなく、高次の思考力にも優れ、自尊心も高められた。特に、成績の悪い子どもほどDIが効果的であるという。ご多分に漏れず現場教師および関係者から猛反発を食らい、当時のブッシュ大統領が後押ししたにもかかわらず、未だにほとんど普及していない。
その数学が戦略を決める』より抜粋・再構成

Aidemy.net: AIプログラミング学習サービス

未踏事業関連だそうです。

参考: AIプログラミング学習サービスAidemy「未踏ジュニア」に協賛 - 産経ニュース

番外

覚えておこう

盲亀の浮木

「盲亀の浮木」という言葉を知ったのは藤子・F・不二雄の『一千年後の再会』という短編漫画でした。

参考: 盲亀の浮木(モウキノフボク)とは - コトバンク

所変わればチートシート変わる

可視光って狭!

遠方にしてもの凄い精度


今週は以上です。土日は身体を休めましょう!

バックナンバー(2018年度)

週刊Railsウォッチ(20180525)特集: RubyKaigi 2018いよいよ来週、Rails vs Hanami対決、命名にはシソーラス使おうほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやRSSなど)です。

Ruby 公式ニュース

Rails公式ニュース

RubyFlow

160928_1638_XvIP4h

Hacklines

Hacklines

Postgres Weekly

postgres_weekly_banner

JavaScript Weekly

javascriptweekly_logo_captured

Railsのフロントエンドのノウハウ#1: システムテスト編(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

記事を2本に分割しました。日本語タイトルは内容に即したものにしています。

Railsのフロントエンドのノウハウ#1: システムテスト編(翻訳)

記事の長さ: 8分

Rails 5以降の新しい機能のひとつに「システムテスト」があります。システムテストは、Capybaraを用いたフル装備のフロントエンドテスト機能を提供し、通常のユーザーが体験する完璧なWebエクスペリエンスを提供する本物のブラウザウィンドウを用いて各テストを実行します。この機能がRailsに組み込まれたことでテストがエレガントになり、テストに全力で集中できるようになります。

さらにうれしいことに、標準のRailsジェネレータを用いてモデル名や属性フィールドを生成すると、標準のCRUDシステムテストを自動生成してくれるので、フロントエンドフレームワークの差し替え作業が大幅にシームレスになります。

今回の記事では、前回の記事で用いたRails+VueJSアプリでコード変更が生じたときにシステムテストをすべてパスさせてご覧にいれます。その他に、テストで有用なJavaScriptの知識についてもいくつかご紹介いたします。

フロントエンドアプリを更新する

ここでは、前回の「VueJS Components with CoffeeScript for Rails」記事で使ったアプリを変更します。前回までのソースコードはdanielpclark/vue_exampleでご覧いただけます。

システムテストを実行するには、プロジェクトディレクトリで以下のコマンドを実行します。

rails test:system

訳注: 初めて実行する人はyarnをインストールしたうえでyarn add rails-erb-loaderを実行しておきましょう。場合によってはyarn install --ignore-enginesの実行も必要かもしれません。

リソースの作成と更新の部分でエラーが2件表示されるはずです。「Unable to find visible field “Body” that is not disabled」というエラーメッセージが表示されます。

上述のエラーはこのフィールドでのみ表示されます。これがこのテストで最初のエラーであるためです。テストの順序を変更すると、作成や更新にあるすべてのフィールドについて同様にテストが失敗します。これはテストするフィールドをCapybaraから探索する方法に関連しているはずです。

訳注: 手元のRuby 2.5.1/Rails 5.2.0環境では、最初だけ上述のエラーが1件表示されましたが、その後エラーは表示されなくなりました。springを止めてやり直しても、やはりエラーは表示されませんでした。

Railsのフォーム系メソッドでラベルや入力フィールドを生成すると、HTMLのlabel要素にforフィールドができます。ここには、対象となるあらゆる入力のidオブジェクトの名前が含まれます。私たちが独自のVueJSコンポーネントテンプレートを用いて新しいフォームを手作りしたときに、これらのフィールドについてはテストを書いていませんでした。そのため、app/javascript/form-document.vue.erbファイル内のフォーム用フィールドを変更する必要があります。

<label>Subject</label>
<input type="text" v-model="document.subject" />

<label>State</label>
<select v-model="document.state">
  <%= options_for_select(Document.states.keys, "concept") %>
</select>

<label>Body</label>
<textarea v-model="document.body"></textarea>

上を以下のように変更します。

<label for="document_subject">Subject</label>
<input id="document_subject" type="text" v-model="document.subject" />

<label for="document_state">State</label>
<select id="document_state" v-model="document.state">
  <%= options_for_select(Document.states.keys, "concept") %>
</select>

<label for="document_body">Body</label>
<textarea id="document_body" v-model="document.body"></textarea>

CapybaraにSubjectfill_inするよう指示を出すと、Capybaraはそのラベルのコンテンツとforの値を元に対象を取得し、そのforの値で使われているidを対象とします。この方法のよい点は、それに続く入力フィールドを対象として指定しなくても、このラベルを用いて同じように対象を指定できることです。

(修正後に)rails test:systemを再実行すると、同じエラーメッセージが表示されます。これはRails 5の設定でprotect_from_forgeryがテスト環境で使われていないことに関連するはずで、そのためCSRFトークンが生成されなくなっています。私たちのVueJSコードが失敗したのは、メタ属性フィールドが利用できることを明示的に要求しているためです。

これは以下のいずれかの方法で修正できます。

  • 自分のVueJSコードを編集して、CSRFトークンが存在しなくても動作するようにする(上述のアドバイス)。
  • config/environments/test.rbファイルを以下のように変更する。
# Forgery protection
config.action_controller.allow_forgery_protection = true

フロントエンド側のフォーム送信の実装によっては、ApplicationControllerに以下のコードがないと動かない可能性もあります。私たちの場合はこれは不要になります。

protect_from_forgery prepend: true

原注: 問題発生の原因を突き止めるには、RAILS_ENV=testを指定してrails serverを実行する必要があります。これにより、Documentリソースのneweditリソースを表示したときにブラウザのコンソールでJavaScriptのエラーが発生してる様子を確認できるようになります。

rails test:systemを実行すると、今度は「Update DocumentボタンやCreate Documentボタンが見つからない」というエラーメッセージに変わります。これはRailsが送信ボタンとして生成したと命名スキーマがフォームヘルパーで使われている可能性があるため、テストを更新して現在のボタン名を反映する必要があります。

test/system/documents_test.rbを開き、click_onの対象を"Create Document""Update Document"から"Submit"に変更します。これでテストを実行すると、今度は正しいflash通知が結果の中に見当たらないというメッセージが表示されますので、このflashメッセージを追加する必要があります。以下のapp/views/layouts/application.html.erbアプリケーションテンプレート内の<body>タグの内側に以下を追加します。

<% flash.each do |name, msg| -%>
  <%= content_tag :div, msg, class: name %>
<% end -%>

続いてコントローラのupdateアクションやnewアクションを更新して、適切なflash通知が表示されるようにします。app/controllers/documents_controller.rbを以下のように変更します。

def create
  @document = Document.new(document_params)

  respond_to do |format|
    if @document.save
      flash[:notice] = 'Document was successfully created.'
      format.html { redirect_to @document, notice: 'Document was successfully created.' }
      format.json { render :show, status: :created, location: @document }
    else
      format.html { render :new }
      format.json { render json: @document.errors, status: :unprocessable_entity }
    end
  end
end

def update
  respond_to do |format|
    if @document.update(document_params)
      flash[:notice] = 'Document was successfully updated.'
      format.html { redirect_to @document, notice: 'Document was successfully updated.' }
      format.json { render :show, status: :ok, location: @document }
    else
      format.html { render :edit }
      format.json { render json: @document.errors, status: :unprocessable_entity }
    end
  end
end

これでrails test:systemを実行すると、システムテストがすべてパスするようになります。


関連記事

Rails 5.1以降のシステムテストをRSpecで実行する(翻訳)

Rails 5: WebSocketのマルチセッションをMiniTestとシステムテストでテストする(翻訳)

Rails: ActiveRecord::Relationで生SQLは避けよう(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

訳注: 原文のActiveRelationは訳文でActiveRecord::Relationに変更してあります。

Rails: ActiveRecord::Relationで生SQLは避けよう(翻訳)

ActiveRecord::Relationは、ActiveRecordの検索やクエリエンジンを強化する、柔軟で強力なツールです。

次のようには書かないこと

ActiveRecord::Relation#whereメソッド内で生SQL文字列を式展開(interpolation)で直接書く。

Person.where("name = #{ params[:name] } AND hidden_at IS NULL")

これもやらないこと

?による安全性の高い式展開を用いてユーザー入力を「arrayスタイル」で生SQL文字列で書く。

Person.where('name = ? AND hidden_at IS NULL', params[:name])

次のように書くこと

「hashスタイル」の構文で書く。

Person.where(name: params[:name], hidden_at: nil)

そうすべき理由

最初の2つで使われている生SQL手書きは、ActiveRecord::RelationがRailsに(バージョン3.0で)マージされたときまでは、データベースクエリを指定する唯一の方法でした。しかし、上述の「hashスタイル」の方が柔軟性においても安全性においても上です。

1つ目の例は非常に危険です。ユーザーから渡されたパラメータを文字列の式展開で直接使っていますが、こういう書き方は絶対にしないでください。さもないとSQLインジェクション攻撃にさらされ、インターネットに潜む悪意のあるユーザーがあなたのデータベースに対して破壊的またはデータをさらけ出すステートメントの実行を試みることができてしまいます。

2つ目の例で用いた「arrayスタイル」では、渡されるデータがサニタイズされるため、1つ目の「stringスタイル」による#whereよりはましな方法です。しかし、SQLを生書きしないといけない点は変わりません。せっかくRubyで楽しくコードを書いているというのに、ActiveRecord::Relationが生成してくれる完璧なSQLでハッピーになれる道を選ばない理由があるでしょうか。

3つ目の例で用いた「hashスタイル」は、2つ目の「arrayスタイル」よりも短く、クリーンで、エディタのシンタックスハイライトもよりきれいに表示されます。コードは、書くときよりも読むときの方が重要なのです。

#whereに文字列を渡すと、渡した文字列がそのまま生成されたSQLで使われます。テーブル名も不要ですし、タイポとも無縁です。

# hashスタイルの場合
> Person.where(name: 'Andy', hidden_at: nil).to_sql
=> "SELECT \"people\".* FROM \"people\" WHERE \"people\".\"name\" = 'Andy' AND \"people\".\"hidden_at\" IS NULL"

# stringスタイルの場合
> Person.where('name = ? and hidden_at is null', 'Andy').to_sql
=> "SELECT \"people\".* FROM \"people\" WHERE (name = 'Andy' and hidden_at is null)"

この「hashスタイル」構文が評価されると、データベースのテーブル名がクエリに含まれ、SQLの精度も向上します。これにより、複数のモデルでJOINしたりクエリを生成したりするときのエラーを削減できます。

hashスタイルのさらに便利な点は、データベースアダプタが変更されたときにも生成されるSQLの互換性が保たれることです。

そうすべきではない理由があるとすれば

文字列による条件を使うと、使っているデータベースの特定のSQL方言(フレーバー)への依存が生じる可能性があります。ただし、これが問題になるとすればよほど難解なデータベース固有SQL(検索や地理情報クエリなど)を使う場合ぐらいです。上述の例のように素直なSELECT構文なら問題になりません。

さらに、いったんproductionにデプロイされた後でデータベースをPostgreSQLからMySQLに(あるいはその逆)に移行することは、実際にはまずありません。よほどマゾヒスティックな性格の方なら別ですが。

また、本記事でご紹介した例は非常にシンプルなものです。現実には、使っているデータベース固有の文字列引数を#whereに与える必要が生じることは多々あり、それ自体はまったく問題ありません。しかし選択の余地があるならば、柔軟かつスコープの明快な方式を選ばない理由があるでしょうか。

関連記事

Rubyでの文字列出力に「#+」ではなく式展開「#{}」を使うべき理由

Rails: Shopify流「非同期マイグレーション」でデプロイを安定させよう(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

内容に即した日本語タイトルにしています。

Rails: Shopify流「非同期マイグレーション」でデプロイを安定させよう(翻訳)

スキーマ変更を伴うコードをデプロイするたびに、bin/rails db:migrateを実行して新しいActive Recordマイグレーションを適用しなければなりません。これは共通で使われるデプロイスクリプトです(Capistranoを参照)。

デプロイの一環としてマイグレーションを走らせることは、多くの企業で用いられているデフォルトのアプローチですが、Railsコミュニティはどうしたわけかこれ以外の方法を検討したことがありません。マイグレーションを行うと、その分リリース手順が複雑になりませんか?

  • マイグレーションが失敗した場合、デプロイを取り消すべきか?
    • 取り消ししたいのであれば、マイグレーションが失敗するまでに新しいコードが短時間production環境で実行されていた可能性があるが、ロールバックしたときにその新しいコードがさらなる問題を引き起こすかもしれない。
  • 利用しているデータベースが複数ある場合(シャーディングを使っている可能性もある)、それぞれのデータベースにマイグレーションを適用しなければならなくなる。
  • マイグレーションに時間がかかると(数時間)、その間デプロイを完了できなくなる。
    • マイグレーションを実行するアクターでssh接続が失われた場合など
  • クラウド環境(HerokuやKubernetes)の場合、マイグレーションを実行する「after deploy」的フックが使えるとは限らない。

本記事では、デプロイ手順からマイグレーションを削ぎ落とす方法について解説します。Shopifyが辿り着いた非同期マイグレーションなる手法は、「デプロイが終わってから最終的に適用する」「制御は人間が行う」がポイントです。

非同期マイグレーションのしくみ

最初に、db:migrateで実際に何が行われているかを理解しておく必要があります。

Active RecordのRakeタスクを見てみると、ActiveRecord::Base.connection.migration_context.migrateに対してcallを行っていることがわかります。ここがマイグレーション実行のエントリポイントでなければなりません。(ENV['VERSION']のような)引数を付けずに呼び出すと、MigrationContext#migrateがマイグレーション用のクラスごとにMigrationProxy作成し、続いてMigrator.new.migrate呼び出します

マイグレーションが呼び出されるしくみを理解できましたので、これを非同期的な手順として再設計し、デプロイ中にマイグレーションが走らないようにする準備が整いました。マイグレーションをバックグラウンドジョブから実行するという方法はどうでしょうか?

ペンディング中のマイグレーションがあると、そのたびにバックグラウンドジョブに送り込んで実際のマイグレーションをそこで実行し、結果を出力するというアイデアです。どんな実装が可能か見てみましょう。

最初に、(sidekiq-cronなどのツールを用いて)ペンディング中のマイグレーションがあるかどうかを数分おきに繰り返しチェックするジョブをスケジューリングする必要があります。

class MigrationAutoCannonJob < ApplicationJob
  def perform
    return unless migration_context.needs_migration?

    pending_migrations = (migration_context.migrations.collect(&:version) - migration_context.get_all_versions)
    # ここで実行する!
  end

  private

  def migration_context
    ActiveRecord::Base.connection.migration_context
  end
end

マイグレーションは複数同時に実行できない「ブロッキングプロセス」であることを忘れてはなりません。直前のマイグレーションが完了するまでは次のマイグレーションを実行してはならないのです。また、マイグレーションの実行状況を監視できるようにしておきたいので、実行状況を追いかけるためのActiveRecordモデルを1つ作成してみましょう。

$ rails generate model async_migration version:integer state:text
# app/models/async_migration.rb
class AsyncMigration < ApplicationRecord
end
# 必ずuniqueインデックスを追加すること!

それではこの自動機関砲的なジョブ(auto cannon job)を更新して実行状況をトラッキングし、一度に1つのマイグレーションだけが実行されるようにしてみましょう。

class MigrationAutoCannonJob < ApplicationJob
  def perform
    return unless migration_context.needs_migration?

    if AsyncMigration.where(state: "processing").none?
      AsyncMigration.create!(version: pending_migrations.first, state: "processing")
    end
  end

  def pending_migrations
    (migration_context.migrations.collect(&:version) - migration_context.get_all_versions)
  end

  # ジョブが続く

このジョブによってasync_migrationsテーブル内にエントリが1つ作成されますが、これは他のエントリが”processing”でない場合に限って行われます。これによって複数のマイグレーションが同時に走ることを防止します。ジョブそのものが競合に対して保護されていない点には注意を払う必要がありますが、スケジューリングされたインスタンスは1つしかないので、その点は大丈夫です。

それでは、マイグレーションを実際に行うモデル用のコールバックを1つ作成してみましょう。

class AsyncMigration < ApplicationRecord
  after_commit :enqueue_processing_job, on: :create

  private

  def enqueue_processing_job
    MigrationProcessingJob.perform_later(async_migration_id: id)
  end
end

AsyncMigrationが作成されるたびに、実際のマイグレーションを実行するMigrationProcessingJobがキューに送り込まれます。このジョブがどのようなものか見てみましょう。

class MigrationProcessingJob < ApplicationJob
  def perform(params)
    async_migration = AsyncMigration.find(params.fetch(:async_migration_id))

    all_migrations = migration_context.migrations
    migration = all_migrations.find { |m| m.version == async_migration.version }

    # 実際のマイグレーション
    ActiveRecord::Migrator.new(:up, [migration]).migrate

    async_migration.update!(state: "finished")
  end

  def migration_context
    ActiveRecord::Base.connection.migration_context
  end
end

まだまだ足りないものがありますが、アイデアについてはこれで把握できることでしょう。2つのジョブと1件のデータベースレコードを組み合わせることで、バックグラウンドで1件ずつ実行されるマイグレーションをスケジューリングできます。

本記事のコード例はいずれも「作り中そのもの」である点にご注意ください。もっときちんとやりたいのであれば、以下の点にも配慮が必要になるでしょう。

  • エラーハンドリングがまったく行われていない。マイグレーションがエラー終了した場合にAsyncMigrationのステータスを更新したいところ。
  • 最大リトライ数がジョブで定義されていない。マイグレーションのリトライまでやりたいかどうか検討したいところ。
  • マイグレーションの所要時間の測定と永続化もやっておきたいところ。

可能性はいくらでも考えられます。管理UIを構築してマイグレーションを監視できるようにすることもできますし、マイグレーションの完了または失敗をSlackチャンネルに通知することもできます。

Shopifyでは数百にのぼるデータベースシャーディングを扱っており、スキーマ変更のたびにシャーディングひとつひとつでマイグレーションを実行しなければなりません。マイグレーション手順がデプロイスクリプトの一部に組み込まれていると、リリースプロセスがぐらついてしまうでしょう。Shopifyでは代わりに、リリースの完了後に最終的に提供される「非同期マイグレーション」を用いています。非同期マイグレーションは、Shopifyが誇る1日50回を超えるリリースを支える重要な機能のひとつです。

マイグレーションのステータスを次のようにSlackチャンネルに流すことも可能です。

こうした機能が心にズドッと突き刺さりましたら、私のチームで一緒に働きましょう!ぜひShopifyの採用ページをご覧ください。

関連資料

最新記事を追いかけたい方はTwitterで@kirshatrovのフォローをお願いします。

関連記事

Rails: データベーススキーマをダウンタイムなしで変更する(翻訳)

Rails: マイグレーションを実行せずにマイグレーションのSQLを表示する(翻訳)

Rails: PostgreSQLのマイグレーション速度を改善する(翻訳)

Viewing all 1381 articles
Browse latest View live