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

Ruby 2.5新メソッド: #delete_prefix と #delete_suffix(翻訳)

$
0
0

概要

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

Ruby 2.5新メソッド: #delete_prefix と #delete_suffix(翻訳)

本記事は、BigBinaryのRuby 2.5シリーズのひとつです。

先ごろRuby 2.5.0-preview1がリリースされました

Ruby 2.4の場合

Projects::CategoriesControllerという文字列があり、Controllerという文字を削除したいとします。このような場合は#chompメソッドを使えます。

irb> "Projects::CategoriesController".chomp("Controller")
  => "Projects::Categories"

しかし同じ文字列からProjects::を削除したい場合、#chompに対応するメソッドがないので、subに頼る必要があります。

irb> "Projects::CategoriesController".sub(/Projects::/, '')
  => "CategoriesController"

Naotoshi Seoはこんな単純なタスクで正規表現を使いたくないと考え、こうしたタスクをカバーできるメソッドがRubyに必要であると提案しました。

このとき提案されたメソッド名には、remove_prefixdeprefixlchompremove_prefixhead_chompなどがありました。

Matzdelete_prefixというメソッド名を勧め、こうしてこのメソッドが誕生しました。

Ruby 2.5.0-preview1の場合

irb> "Projects::CategoriesController".delete_prefix("Projects::")
  => "CategoriesController"

これで、プレフィックスを#delete_prefixで削除し、サフィックスを#chompで削除できます。名前が対象的でないと感じられたため、#delete_suffixメソッドも追加されました。

irb> "Projects::CategoriesController".delete_suffix("Controller")
  => "Projects::Categories"

Elixir、Go、Python、PHPでどのようにこのタスクを扱っているかについては#12694の議論をご覧ください。

関連記事

Ruby 2.5新メソッド: Dir.children と Dir.each_child(翻訳)


週刊Railsウォッチ(20171215)Ruby 2.5.0-rc1リリース、Ruby 2.4.3セキュリティ修正、Ruby 3.0で変わるキーワード引数、HTML 5.2 RECリリースほか

$
0
0

こんにちは、hachi8833です。クリスマスが迫るとRuby周りが忙しくなりますね。

今週のウォッチ、いってみましょう。

臨時ニュース1: Ruby 2.5.0-rc1リリース


www.ruby-lang.orgより

ついさっき出ました。実は先ほど以下のセキュリティ修正でrbenvを更新したときにrc1の文字が見えていたのでついでにインストールしちゃいました。

臨時ニュース2: Rubyセキュリティ修正リリース

今朝発表がありました。

修正内容

以下の2点です。必要な方はお早めにアップデートしましょう。

プレスリリース(ダウンロードリンクあり)


私もローカル環境とオレオレRailsアプリをRuby 2.4.3にアップデートしました。

Rails: 今週の改修

commit差分から見繕いました。

System TestにFirefox headless driverを追加

# actionpack/lib/action_dispatch/system_testing/driver.rb#67
             browser_options.args << "--headless"
             browser_options.args << "--disable-gpu"

+            @options.merge(options: browser_options)
+          elsif @browser == :headless_firefox
+            browser_options = Selenium::WebDriver::Firefox::Options.new
+            browser_options.args << "-headless"
+
             @options.merge(options: browser_options)
           else
             @options
           end
         end

         def browser
-          @browser == :headless_chrome ? :chrome : @browser
+          if @browser == :headless_chrome
+            :chrome
+          elsif @browser == :headless_firefox
+            :firefox
+          else
+            @browser
+          end
         end

つっつきボイス: 「Firefox Quantamが速くなったらしいのと関係あるのかな」

参考: Firefox Quantumが激重になる問題が発生中

非推奨メソッドがpublicに変わらないよう修正

# activesupport/lib/active_support/deprecation/method_wrappers.rb#60
               deprecator.deprecation_warning(method_name, options[method_name])
               super(*args, &block)
             end
+
+            case
+            when target_module.protected_method_defined?(method_name)
+              protected method_name
+            when target_module.private_method_defined?(method_name)
+              private method_name
+            end
           end
         end

つっつきボイス: 「お、お、こんなバグあったとは」「非推奨メソッドが削除されるまでの間ということですね」

非推奨のBigDecimal#newを削除

# activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb#6
      module OID # :nodoc:
         class Decimal < Type::Decimal # :nodoc:
           def infinity(options = {})
-            BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1)
+            BigDecimal("Infinity") * (options[:negative] ? -1 : 1)
           end
         end
       end

つっつきボイス:BigDecimal#newじゃなくてBigDecimalになるのか」「RailsのActiveSupportなのかなと思ったらRubyの方だった」「newが非推奨になったのはなぜなんだろう?」「オブジェクトの同一性を担保するためなのかなと思ったけどnewあってもなくてもobject_id違うな↓」

追記: 「IntegerやFloatでnewできないのに合わせたんじゃないんすかね?」

RailsのSTIを修正

# activerecord/lib/active_record/relation.rb#55
-    def new(*args, &block)
-      scoping { @klass.new(*args, &block) }
+    def new(attributes = nil, &block)
+      scoping { klass.new(scope_for_create(attributes), &block) }
     end

つっつきボイス: 「STI: Single Table Inheritance」「この記事↓見るとSTI結構嫌われてる雰囲気ですね」「STIは有用なこともあると思うけど使ったことほとんどなかったナー: カラム増えるし」

参考: みんなRailsのSTIを誤解してないか!?

使われていないwebpack定数を削除

// activestorage/webpack.config.js#1
-const webpack = require("webpack")
 const path = require("path")

 module.exports = {

つっつきボイス: 「誰も使ってないので削除したそうです」

Rails

RailsのArelを使ってコンポジション可能なQuery Builderを書く(RubyFlowより)

# 同記事より
scene = Scene.new(scene_params)
SceneQueryBuilder.new(scene).by_season
SceneQueryBuilder.new(scene).by_episode
SceneQueryBuilder.new(scene).by_dialogue

つっつきボイス: 「Query Builderはよく使うし、いいパターンだと思う: ただ自分はArelじゃなくて生SQLで書くけど」「同じことをActiveRecordだけで書くのはつらくなりやすい」「Builderパターンを説明するときにはSQLを例に使うのが一番わかりやすいと思ってる: みんなもっと使おう」

[保存版]人間が読んで理解できるデザインパターン解説#1: 作成系(翻訳)

Railsのロガー記事2本: ActiveSupport::TaggedLoggingとベストプラクティス

# 元記事1より
#lib/vendor_payment_logger.rb

class VendorPaymentLogger < ActiveSupport::TaggedLogging
  def initialize(logger)
    super(logger)
    logger.formatter = formatter
  end

  def formatter
    Proc.new{|severity, time, progname, msg|
      formatted_severity = sprintf("%-5s",severity.to_s)
      formatted_time = time.strftime("%Y-%m-%d %H:%M:%S")
      "[#{formatted_severity} #{formatted_time} #{$$}]\n #{msg}\n"
    }
  end
end

つっつきボイス: 「最近ロガーで悩んでると聞いたので探してみました」「そうそう、某案件のロガー設計方針」「いつだったか、ロガーはオブジェクト指向的設計からはみ出しやすいって言ってましたね」「まさしく」

リレーションだけが更新されるとupdate_attributeでSQLが発行されない

Gobyの作者@st0012さんからの情報です。Rails 5のissue #25503が以下のやり取りの後未だにcloseしていないそうです。この挙動は2015年に0fcd4cで追加されたそうです。

問題点を見つけた。
changed?メソッドがリレーションの更新をチェックせず、属性の更新だけをチェックしてた。
リレーションが更新されたかどうかをチェックする何かうまい方法を見つける必要がある。個別のテストは動くのにグループになるとコケるテストがあったので、条件を少し追加する必要があった。
25503#issuecomment-250984166より大意


つっつきボイス: 「仕様なのかバグなのか?」「ActiveRecord::Persistence#update_attributeではリレーションの更新には触れてないですね」「ところで『update_attributeはバリデーションがきかないゴミだぞ』って誰か言ってた」「update_attribute(key, value) というAPIも残念」「バリデーションが効かないからゴミというわけではない(過去にやらかした経験上)」

参考: ActiveRecord の attribute 更新方法まとめ

Rubyアプリが劣化する様子をレストランになぞらえて説明する

Passengerでお馴染みのオランダPhusion社のブログ記事です。レストランに皿洗い機を導入後に処理量を増やすとどうなるかという形でサーバーのワーカーの扱いについて解説しています。


同記事より

テストにおけるstubのコスト

stubは速いしスイスイ通るし便利だけど使い所に注意という記事です。

# 同記事より
describe Order do
  let(:customer) { Customer.new }
  let(:order)    { Order.new(subtotal: 100, customer: customer) }

  describe '#total' do
    context 'a customer has fee' do
      before do
        customer.fee = 21
      end

      it 'returns the total price which includes fee' do
        expect(order.total).to eq(121)
      end
    end

    context 'a customer has no fee' do
      before do
        customer.fee = 0
      end

      it 'returns the total price without any fee' do
        expect(order.total).to eq(order.subtotal)
      end
    end
  end
end

つっつきボイス: 「単体テストだとstubを使う意味ないこと多いっすね」

Passwordless: Railsアプリでパスワードなし認証するgem

class User < ApplicationRecord
  validates :email, presence: true, uniqueness: { case_sensitive: false }

  passwordless_with :email # <-- ここ
end

つっつきボイス: 「とりあえずPasswordlessという名前はわかりやすくていいかも: Deviseに比べれば」

[Rails] Devise Wiki日本語もくじ1「ワークフローのカスタマイズ」(概要・用途付き)

ankaneさんの膨大なメモ

groupdateなどさまざまなgemを出しているAndrew Kaneことankaneさんがいろんなドキュメントをリポジトリに置いているのを見つけました。内容はRails/PostgreSQL/セキュリティ/データサイエンスなどさまざまです。


つっつきボイス: 「BPSもDocbaseにMarkdownドキュメントを社内でじゃんじゃん共有しているけど、それと同じようなノリなのかも」「publicな場所に置いているのエライ」

Markdownで書けるドキュメントコラボレーションサービスを比較する

コーディングスタイルで揉める理由

『オブジェクト指向設計実践ガイド』などでお馴染みのSandi Metz氏の記事です。

  • なぜスタイルガイドを使うのか
  • どのスタイルガイドがいいのか
  • なぜチームで反対されるのか
  • 自分流でやりたいんだけどダメ?
  • チームで合意を取り付けるには
  • 新しいスタイルガイドが性に合わないときは?

つっつきボイス: 「これはどうしてもどこかで揉めるやつ」「永遠の課題」「基本はプロジェクトの既存スタイルに沿うことにして、迷ったらスタイルガイドに沿う、と言うだけなら簡単ですけどね」

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

ワーカープロセスを安全に切り替える

factory_girl改めfactory_botなどでお馴染みのThoughtbot社のブログ記事です。

# 同記事より
#!/usr/bin/env ruby

STDOUT.sync = true

def info
  "PID=#{Process.pid} WORKER_VERSION=1"
end

puts "TICK: Starting process... #{info}"

trap "TERM" do
  puts "TICK: Received SIGTERM #{info}"
  sleep 5 # 重い作業を終了中ということにする
  puts "TICK: Graceful shutdown #{info}"
  exit
end

loop do
  puts "TICK #{info}"
  sleep 2 # 重い作業を実行中ということにする
end

doorkeeper: RailsでOAuth2認証するgem

OAuth2に特化した認証gemです。Railsの他Grapeフレームワークにも対応しているそうです。

# doorkeeper-gem/doorkeeperより
class Api::V1::ProductsController < Api::V1::ApiController
  before_action :doorkeeper_authorize! # 全アクションでトークンを必須にする

  # 以下アクション
end

つっつきボイス: 「doorkeeper、結構定番らしいけど初めて知ったので」「これもいい名前かな」「そうっすか?Doorkeeper.jpと紛らわしそう」

Rails Developer Meetup 2017より


railsdm.github.ioより

大盛況だったRails Developer Meetup 2017のスライドからです。参加したかったー(´・ω・`)

ふつうのRailsアプリケーション開発

とても参考になります。

yuba: Railsの抽象化支援gem

generatorで生成できます。

# willnet/yubaより
rails generate yuba:service create_artist
rails generate yuba:form artist
rails generate yuba:view_model artist_index

rancher: コンテナを継続的に管理

deppbotとtachikoma.io: セキュリティや依存関係の自動更新

Ruby trunkより

Ruby 3でキーワード引数がかなり変わる見通し

詳細は今後多少変わるのかもしれませんが、既にMatzがRubyWorld Conference 2017などでRuby 3で本物のキーワード引数を導入すると言明しているそうです。

# 以下の呼び出しは「キーワード引数」を渡す
foo(..., key: val)
foo(..., **hsh)
foo(..., key: val, **hsh)

# 以下の呼び出しは「通常の引数」を渡す
foo(..., {key: val})        # {} で囲まれている
foo(..., hsh)
foo(..., {key: val, **hsh}) # {} で囲まれている

# 以下のメソッド定義は「キーワード引数」を受け取る
def foo(..., key: val)
end
def foo(..., **hsh)
end

# 以下のメソッド定義は「通常の引数」を受け取る
def foo(..., hsh)
end

Ruby 2のキーワード引数はHashオブジェクト(キーはすべてシンボル)の通常の引数であり、最後の引数として渡されている。この設計を選んだのは互換性のためだったが、かなり複雑になっていて、動作が直感的にならない多くのエッジケースの原因になっていた。
Ruby 3ではキーワード引数は通常の引数と完全に分離される(ブロックパラメータが通常の引数と完全に分離されているのと同様に)。
この変更によって互換性が失われる。キーワード引数を渡したり受け取ったりする場合は、常に({}などで囲まない)むき出しのシンボル値かdouble-splat**が必要になる。
次のような移行パスを考えている:
* Ruby 2.6か2.7あたりで警告を出すようにする: 通常の引数がキーワード引数と解釈可能な場合(またはその逆)
* Ruby 3.0で新しい文法に完全に移行する
#14183より大意


つっつきボイス: 「最初#14176 Unclear error message when calling method with keyword argumentsにしようかと思ったんですが、その後でこのissueが流れてきたので」「Railsも順次対応不可避…ゴク」「実は@st0012さんがちょっと前からGobyのキーワード引数周りの仕様で矛盾に突き当たって悩んでいるんですが、これを見たら無理もないかも」「本当のキーワード引数

Ruby 2.0.0リリース! – キーワード引数を使ってみよう

そこからsplat演算子(*)やdouble-splat演算子(**)の話題になりました。

「splat演算子って、引数とパラメータのどっちにも置けるからまたややこしい」「正直ぱっと見て分からないw」

参考: Rubyの配列展開 *[a, b, c]

Ruby

Ruby 2.5でStructにkeyword_init: trueでキーワード引数が使える

こちらもキーワード引数にちょっと絡みます。

Foo = Struct.new(:a, :b, keyword_init: true)
Foo.new(a: 1, b: 2) #=> #<struct Foo a=1, b=2>

つっつきボイス: 「これはとてもよい」「最初ハッシュが渡せるのかと思ったらキーワード引数だった」「とはいうもののStructにするより普通にクラス書いちゃうことの方が多い: Structって規模感で悩むのと、ダメな使い方になる可能性もあるんで」「Struct#newインスタンスを継承すべからずとかですね」

Rubyスタイルガイドを読む: クラスとモジュール(2)クラス設計・アクセサ・ダックタイピングなど

シングルトンクラスの本当の使われ所

私たちが普通クラスメソッドと呼ぶものは、技術的には、それぞれのシングルトンクラスで定義されたクラスオブジェクトの(シングルトン)インスタンスメソッドです。
同記事より


www.puzzle.chより


つっつきボイス: 「この図↑を見た限りでは、シングルトンクラスも加味すると継承パスはこんなふうに二本立てともみなせるということかな」

RubyConf 2017で発表したお: Rubyで2Dゲーム作った

記事のスライドの見せ方が見事です。


www.blacktm.comより


つっつきボイス: 「↑Apple IIがつい懐かしくって」

最近のYARV-MJIT


つっつきボイス: 「70fpsはoptcarrotのパフォーマンスでしょうね」

Rubyと型


つっつきボイス: 「Matzと_ko1さんの二人がこんなに手こずるなんて、型理論ってどんだけ難しいんだと思ってこの翻訳記事↓見つけました」

参考: 「型」の定義に挑む

「話がバートランド・ラッセルの数理論理学から始まってて、抽象度むっちゃ高い」「きわめつけは以下↓: 厳密に定義しすぎると発展を妨げるって」「(数学科出身につき)それ気持ちわかるわー」「エンジニアの発想じゃなくて数学者の発想だなーと思いました」

“型の定義とは何か?” この質問に明確かつ正確な回答を与えられれば、そこから生じる多くの誤解や無意味な議論を避けることができる。しかし、この質問に対して、そのような明確かつ正確な回答を持ってしまった場合、科学の前進を鈍化させ、”知識の成長を妨げ”、”調査の道筋を既知の狭いチャネル内に引き入れてしまう”ことにもつながりかねない。
postd.ccより

参考: Wikipedia: 型理論

JavaScript

LeaderLine: 指示線を描画するJSライブラリ


github.com/anseki/leader-lineより


つっつきボイス: 「これ大好きー: だいぶ昔にこういうのを力技で実装したときはうまくいかなかったのを思い出した(´・ω・`)」

parcel人気急上昇?

10日しないうちに★10,000に迫る勢いです。

github.com/parcel-bundler/parcelより

設定ファイルなしでも動くというのが殺し文句のようです。


つっつきボイス: 「JS環境選びってギャンブル」「今からでも遅くはない?」

参考: webpack時代の終わりとparcel時代のはじまり

図と例で学ぶawaitasyncFrontend Weeklyより)


nikgrozev.comより


つっつきボイス:asyncとかpromiseはほんと面倒」

JavaScript: 5分でわかるPromiseの基礎(翻訳)

mapforEachの違い

パフォーマンスも含めて比較した記事です。

// 同記事より
arr.forEach((num, index) => {
    return arr[index] = num * 2;
});
// arr = [2, 4, 6, 8, 10]
let doubled = arr.map(num => {
    return num * 2;
});
// doubled = [2, 4, 6, 8, 10]

つっつきボイス:map()forEach()はRubyの#map#eachとだいたい同じようなものだと思ってる」「他の言語でforeachを見た気がするけどどの言語だったかな…」

参考: Wikipedia-ja Foreach文

CSS/HTML/フロントエンド

HTML 5.2 RECがリリース

今年10月に出たHTML 5.1 2nd Editionの立場は…

目立つ部分だけ目次レベルでざっとdiff取ってみました(左がHTML 5.2、右がHTML 5.1 2nd Edition)

最近の非同期CSS読み込み(Frontend Weeklyより)

今ならrel="preload"でJSなしでできるということです。

<link rel="preload" href="mystyles.css" as="style" onload="this.rel='stylesheet'">

はじめてのCSS Gridレイアウト(Frontend Weeklyより)

CodePenを多用して説明しています。

See the Pen CSS Grid: Calendar by Geoff Graham (@geoffgraham) on CodePen.


つっつきボイス: 「そういえばbabaさんが『gridって直下の要素しかgridできないのが不便なんですよね』って言ってました」「display: flex;が見えてあれ?と思ったけどliの中だけだった」

GoogleがSEOスターターガイドを改訂

肝心のガイドはまだ日本語になっていません。

Unicode Consortiumが追加待ちのグリフにフィードバックを募集


blog.unicode.orgより


つっつきボイス: 「文字が全部鼻に見えるw」「これレビューできる人世界に何人というレベルでは」

今気づきましたが、17372r-n4923-dam1-chart.pdfに旧かな文字まで並んでいて絶句してしまいました。もはや誰得。


17372r-n4923-dam1-chart.pdfより

その他

Microserviceアーキテクチャのパターン


microservices.ioより

昨日BPS社内のエンタープライズ・アーキテクチャ勉強会でMicroserviceのアーキテクチャに言及されたので探してみました。

エンタープライズアプリケーションアーキテクチャパターンを読む: 1.概要

Googleが動画を自動で漫画に変換するアプリを実験公開


research.googleblog.comより


つっつきボイス: 「うう、すげー」「コマ割りまでやってる」「今後結婚式に出席したらきっと山ほど見られますよ」

参考: Google、動画を自動で漫画化する実験アプリ公開。AIポートレートカメラマンやスクラッチ動画生成も

Recho: SlideShareなどにTwitterレスを表示できるChrome拡張


つっつきボイス: 「ほとんどニコ動」

go-torch: Goプログラムをブラウザでビジュアルプロファイリングするツール


つっつきボイス: 「この間もこんなグラフ見ましたね」「名前ど忘れ…frame graphだった」

参考: Go言語のプロファイリングツール、pprofのWeb UIがめちゃくちゃ便利なので紹介する

コードをフェンスの向こうに放り投げる

正規表現のよさげな本

番外

Ruby Tuesday


つっつきボイス: 「asakusa.rbが火曜日開催だから?」「そういうことか」

中国語にひらがなの「の」が絶賛混入中

人間デコーダー

写真は残念ながら削除されたようです。


つっつきボイス: 「この郵便屋さんものすごく優秀なんじゃ」「普通迷宮入りですよね」

むやみになつかしい感


つっつきボイス: 「大昔にTK-80↓の横でラジカセでFM聴いてたら実行/停止に応じていろんな音階が出たのをつい思い出しちゃって」

磁石とクリップ


今週は以上です。

バックナンバー(2017年度後半)

週刊Railsウォッチ(20171208)最近のRailsフロントエンド事情、国際化gem、mallocでRubyのメモリが倍増、るびま記事募集ほか

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

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

Ruby 公式ニュース

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

Frontend Weekly

frontendweekly_banner_captured

Ruby: Proxyパターンの解説(翻訳)

$
0
0

概要

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

パターン名は英語で表記します。

Ruby: Proxyパターンの解説(翻訳)

本記事では、Proxyパターンとその種類について学びます。パターンの種類ごとにRubyでの実装を行います。

Proxyパターンの目的

最初にProxyパターンの目的を押さえておきましょう。「Design Patterns: Elements of Reusable Object-Oriented Software」という書籍ではProxyを次のように説明しています。

別のオブジェクトの代理(サロゲート: surrogate)やプレースホルダを提供することで、オブジェクトへのアクセスを制御する

ProxyパターンはSurrogateとも呼ばれています。

同書で使われている例は私のお気に入りです。

あるドキュメントエディタに、グラフィックオブジェクトをドキュメントに埋め込む機能があるとします。画像によってはオブジェクトの作成にコストがかかるものがあります(巨大なラスタ画像など)が、ドキュメントは素早く開けられなければなりません。そのため、ドキュメントを開くときにはコストのかかるオブジェクトをすべて作成するのは避ける必要があります。(中略)解決方法は、実際の画像の代役として振る舞う画像プロキシのような別のオブジェクトを作成することです。このプロキシは画像のように振る舞い、必要に応じてインスタンス化の面倒を見ます。

依存関係は次のような感じになります。

Ruby - image proxy

TextDocumentは最初にImageProxyを用いて何らかのプレースホルダを表示し、ImageProxyは必要に応じて実際のImageを読み込みます。

適用範囲

プロキシは、オブジェクトへの参照をもっとスマートにする必要がある場合に常に有用です。次の応用方法があります。

  1. 仮想プロキシ(virtual proxy)
  2. 保護プロキシ(protection proxy)
  3. リモートプロキシ(remote proxy)
  4. スマート参照(Smart reference)

1. 仮想プロキシ

コストの高いオブジェクトを必要に応じて作成します

上述のImageProxyはまさしくこのタイプのプロキシです。Rubyで実装してみましょう。

2つのメソッドを持つTextDocumentクラスがあるとします。

class TextDocument
  attr_accessor :elements

  def load
    elements.each { |el| el.load }
  end

  def render
    elements.each { |el| el.render }
  end
end

メソッドはloadrenderの2つだけです。最初にドキュメントのすべての要素を読み込み、続いて要素をレンダリングしたいとします。

読み込みに非常に時間のかかるImage要素があるとします。

class Image
  def load
    # ... 非常に時間がかかる
  end

  def render
    # 読み込んだ画像をレンダリングする
  end
end

画像を含むドキュメントを読み込む場合は次のようになります。

document = TextDocument.new
document.elements.push(Image.new)
document.load # => 画像のせいで読み込みが遅い

画像の読み込みに時間がかかるので、ドキュメントの読み込みにも時間がかかります。

そこで、画像の遅延読み込み(lazy loading)を実装する仮想プロキシを作成します。

class LazyLoadImage
  attr_reader :image

  def initialize(image)
    @image = image
  end

  def load
  end

  def render
    image.load
    image.render
  end
end

これで、LazyLoadImageを使えばドキュメントの読み込みが一時停止しなくなります。LazyLoadImagerender呼び出しが行われるまで画像を読み込まないからです。

document = TextDocument.new
image = Image.new
document.elements.push(LazyLoadImage.new(image))
document.load # => 速い
document.render # => 画像読み込み中なので遅い

SimpleDelegatorを使って、Decoratorパターンで行ったのと同様にLazyLoadImageからImageへの正しい委譲を実装する方法もあります。

2. 保護プロキシ

保護プロキシは、元のオブジェクトへのアクセスを制御します

これはだいたい見ての通りです。元のオブジェクトを呼び出す前に何らかの保護ルールを適用したい場合、保護プロキシでラップできます。

class Folder
  def self.create(name)
    # フォルダの作成
  end

  def self.delete(name)
    # フォルダの削除
  end
end

class FolderProxy
  def self.create(user, folder_name)
    raise '管理者以外はフォルダを作成できません' unless user.admin?

    Folder.create(folder_name)
  end

  def self.delete(user, folder_name)
    raise '管理者以外はフォルダを削除できません' unless user.admin?

    Folder.delete(folder_name)
  end
end

この例では、プロキシと元のクラスのインターフェイスが異なっていることは私も認めざるを得ません。Foldercreatedeleteのパラメータは1つしかありませんが、FolderProxyuserもパラメータに取っています。これが保護プロキシの実装として最善かどうか私もわかりません。もっとよい例がありましたらぜひコメントでお知らせください ;)

3. リモートプロキシ

リモートプロキシは、異なるアドレス空間上にあるオブジェクトをローカル環境で代表します

たとえばRPC(リモートプロシージャコール)を使いたいのであれば、RPC呼び出しを扱うプロキシを簡単に作れます。ここではxml-rpc gemを例に取ります。

次のコードでRPC呼び出しを行えます。

require 'xmlrpc/client'

server = XMLRPC::Client.new2("http://myproject/api/user")
result = server.call("user.Find", id)

私たちの代わりにRPC呼び出しを扱うリモートプロキシを作成します。

class UserProxy

  def find(id)
    server.call("user.Find", id)
  end

  private

  def server
    @server ||= XMLRPC::Client.new2("http://myproject/api/user")
  end
end

これで、異なるアドレス空間上にあるオブジェクトへのアクセスに使えるプロキシができました。

4. スマート参照

スマート参照は、オブジェクトへのアクセス時に付加的な操作を実行する生のポインタを置き換えるのに使います

利用法の1つは、最初に参照が行われるときに永続オブジェクトをメモリに読み込むことです。

このタイプのプロキシを使うことで、レスポンスのメモ化(memoization)を作成できます。

たとえば、重たい計算を代行するサードパーティ製ツールを使うとします。そうしたサービスでは専用のgemを提供するのが普通です。gemがどのようなものか考えてみましょう。

class HeavyCalculator
  def calculate
    # しばらく何か計算する
  end
end

サードパーティ製gemなので、ここではメモ化を追加できません。かつ、HeavyCalculatorの呼び出しを長時間待ちたくありません。こんなときは、メモ化の追加を代行するスマート参照プロキシを作成します。

class MemoizedHeavyCalculator
  def calculate
    @result ||= calculator.calculate
  end

  private

  def calculator
    @calculator ||= HeavyCalculator.new
  end
end

これで、MemoizedHeavyCalculatorを用いてcalculateを必要なだけ何度でも呼び出せるようになりました。しかも実際の呼び出しは1回だけにとどまり、以後の呼び出しにはメモ化された値が使われます。

プロキシをスマート参照として使う方法を応用すれば、ログ出力を追加することもできます。何らかの見積もり機能を提供するサードパーティ製サービスがあり(当然サービスのコードは変更できません)、サービスの呼び出しごとにログ出力を追加したいとします。これは次のように実装できます。

class ExpensiveService
  def get_quote(params)
    # ... リクエストを送信
  end
end

class ExpensiveServiceWithLog
  def get_quote(params)
    puts "次のparamsで見積もりを取得しています: #{params}"

    service.get_quote(params)
  end

  private

  def service
    @service ||= ExpensiveServiceWithLog.new
  end
end

ExpensiveServiceはサードパーティのコードなので実装は変更できません。しかしExpensiveServiceWithLogを用いればどんなログ出力でも必要に応じて追加できます。ここでは簡単のためputsで出力しています。

その他の関連パターン

Proxyパターンのある種の実装はDecoratorパターンの実装と極めて似ていますが、両パターンの目的は異なります。Decoratorパターンはオブジェクトに責務を追加しますが、Proxyパターンはオブジェクトへのアクセスを制御します。

ProxyパターンはAdapterパターンに似ていることもあります。しかしAdapterパターンは適用先のオブジェクトに別のインターフェイスを提供するためのものです。Proxyパターンは、適用先のオブジェクトと同じインターフェイスを提供します。

追伸: プロキシオブジェクトは、元のオブジェクトが持つすべてのメソッドに応答するべきです。上の例ではプロキシクラスの元のオブジェクトが持つメソッドを単に実装しましたが、元のクラスにもっと多くのメソッドがある場合は、プロキシクラスでも同じコードが多数繰り返すことになるかもしれません。

Proxyパターンの実装とDecoratorパターンの実装はほぼ同じなので、プロキシから元のオブジェクトにメソッドを委譲できるSimpleDelegatorの記事をお読みいただくことを強くおすすめいたします。

お読みいただきありがとうございました。

関連記事

Ruby: Chain of Responsibilityパターンの解説(翻訳)

Railsで重要なパターンpart 2: Query Object(翻訳)

Railsで重要なパターンpart 1: Service Object(翻訳)

ベテランRubyistがPythonコードを5倍速くした話(翻訳)

$
0
0

概要

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

ベテランRubyistがPythonコードを5倍速くした話(翻訳)

私はかれこれ10年以上Rubyコードを書きまくっていますが、最近は修士科目の関係でPythonコードも書きまくっています。RubyとPythonは多くの点で違っていますが、パフォーマンス上の特性やコードの最適化といった面では似ています。本記事では、私が最近最適化したPythonコード片を題材に、Rubyコードの場合の高速化手順と比較してみることにします。

本記事について「Pythonインタプリタの高速化かと思った」というご感想をいくつもいただきました。それはそれできっとクールだったでしょうが、この記事はそちらではなく、インタプリタ言語を使うコードの高速化を扱います。ここで学んだことは、PythonにもRubyにも通用します。

そのコードをご覧に入れる前に、ご存じない方のために一応私のパフォーマンス方面について書いておきます。私の趣味のひとつがRubyのパフォーマンスであり、Railsアプリのベンチマークに使われるderailed benchmarks gemのメンテナでもあり、多くのプロジェクトでパフォーマンス関連のプルリクを大量に送りました。Railsのend-to-endリクエストをほぼ12%高速化するプルリクを送った後、なりゆきで「すべての文字列をfreezeする」運動の促進担当者になったこともあります。パフォーマンス向上といっても、そのほとんどはhash(Pythonではdict)やarray(Pythonではlist)の代入の削減によって得られたものです。

警告: 私はlistとarrayを互いに交換可能なものとして用いる傾向があります。メソッドと関数についても同様です。これらが同じでないことも、これを見てカッとなって眼から血を吹く人がいることも承知していますので、どうかご容赦ください。

私にとってパフォーマンスの改善はこれが初めてではありません。

何かを高速化する前には、仕組みを理解しておかなければなりません。次の課題を設定します。

コードの仕組みについて関心のない方は、最適化でできることを解説する次節「ロジックをDRYにする」までスキップしていただいて結構です。

本記事で最適化するコードは、チェスに似たゲーム盤上でクイーンの移動方法をすべて特定する必要のあるゲームのプレイで用いられるものです。かつ、このゲーム盤の高さと幅は変更可能とします(実際のチェス盤とは異なることもある)。また、ゲーム盤のステートはlistのlist(Rubyではarrayのarray)で表現します。

空白は0で表します。ゲーム盤のステートは次のような感じになります。

height = 7
width  = 7
board_spaces_occupied = [
    [  1,  0,  1,  1,  1,  0,  0],
    [  1,  1,  0,  1,  1,  0,  1],
    [  1,  1,  1,  1,  0,  0,  1],
    [  1,  0,  1,  0,  1,  1,  1],
    [  1,  0,  0,  1,  1,  1,  1],
    [  0,  0,  1,  0,  0,  1,  1],
    [  0,  1,  1,  0,  1,  1,  1],
]

詳細はもっと複雑ですが本記事とは無関係なので、さしあたってここまで理解できれば十分です。きっとRedditで「numpy使えばいいのに」とツッコミが入ることでしょう。numpyはこの特定の代入規則に合わなかったのです。

有効な移動先を特定するタスクを完了するため、次のPythonコードの変種を授業で与えられました。

BLANK = 0

def get_legal_moves_slow(move):
    r, c = move

    directions = [ (-1, -1), (-1, 0), (-1, 1),
                    (0, -1),          (0,  1),
                    (1, -1), (1,  0), (1,  1)]

    fringe = [((r+dr,c+dc), (dr,dc)) for dr, dc in directions
            if move_is_legal(r+dr, c+dc)]

    valid_moves = []
    while fringe:
        move, delta = fringe.pop()

        r, c = move
        dr, dc = delta

        if move_is_legal(r,c):
            new_move = ((r+dr, c+dc), (dr,dc))
            fringe.append(new_move)
            valid_moves.append(move)

    return valid_moves

def move_is_legal(row, col):
    return 0 <= row < height and\
           0 <= col < width and\
           board_spaces_occupied[row][col] == BLANK

元のコードは授業で使われたものですが、本記事に合わせて形式を少し簡単にしてあります。
heightwidthboard_spaces_occupiedはいずれもグローバルにアクセスできる変数です。

移動元として(5, 4)を渡すと、クイーンの移動先が2つしかないことが正しく示されるので、正常に動作していることがわかります。

print get_legal_moves_slow((5,4))
# => [(6, 3), (5, 3)]

Rubyistのために補足すると、このかっこは「タプル」であり、イミュータブルなarrayに含まれているとお考えください。Rubyならさしずめ[[6, 3], [5, 3]]のような感じになるでしょう。

このコードを高速化するには、そこで行われていることを理解する必要があります。ロジックを追ってみることにします。

クイーンは、自分のいる場所から縦横斜めに移動できます。移動距離は、途中に遮るものがない限り盤上でいくつでも移動できます。この動作を表すために、クイーンのすぐ周りにある空き位置をすべて見つけたいと思います。空き位置は、横方向の(row)移動と縦方向(column)の移動で表します。

directions = [ (-1, -1), (-1, 0), (-1, 1),
                (0, -1),          (0,  1),
                (1, -1), (1,  0), (1,  1)]

次に各空き位置を列挙して、クイーンがルールに違反せずにそこに移動できるかどうかをチェックします。

これを行うには、各移動方向をループして現在の横位置と縦位置に結合し、有効かどうかをそれぞれチェックします。

移動方向が有効な場合は、有効なマスの位置だけではなく、有効なマスへの到達方法(移動方向など)も保存します。この「移動方向」を繰り返しその先に展開して、他の有効な移動も見つけます。

fringe = [((r+dr,c+dc), (dr,dc)) for dr, dc in directions
        if move_is_legal(r+dr, c+dc)]

注意: rはrow(横方向)、cはcolumn(縦方向)を表します。drは横方向の差分、dcは縦方向の差分を表します。きれいな変数名は使われていません。なお文法をググりたいRubyist向けに補足すると、Pythonのこのようなlist列挙形式は「list comprehension」と呼ばれます。

これで、展開可能なfringe listを得ました。これをビジュアル表示する例を見てみることにします。

訳注: fringeは「付随」「メインでない」といったニュアンスです。コードと整合するよう英ママとしました。

完全に空になっているゲーム盤上に(3, 3)を置くと、そこから左斜め上のマスへの移動は(2, 2)になります。そこへの移動は、移動方向を表すarrayの最初の要素(-1, -1)を用います。これで、fringe listは次のようになります。

# [((row, column), (delta_row, delta_column))]
  [((2,   2),      (-1,        -1))]

その先を展開したい場合は、現在の位置に同じ移動方向(-1, -1)を適用して(1, 1)を得ます。

これで、利用できないマスに到達するまで、空きマスの周囲にある展開の必要な位置を即座に得ることができます。

次のコードは、展開を最後まで行います。

valid_moves = []
while fringe:
    move, delta = fringe.pop()

    r, c = move
    dr, dc = delta

    if move_is_legal(r,c):
        new_move = ((r+dr, c+dc), (dr,dc))
        fringe.append(new_move)
        valid_moves.append(move)

このコードでfringe listの各要素を列挙し、削除してから、移動が有効かどうかをチェックして、横方向の移動差分と縦方向の移動差分を適用して新しい位置を得ます。そしてこの新しい位置をfringe listに追加し、元の位置もvalid_moves listに追加し、後者が戻り値になります。

ここまでご理解いただけましたでしょうか。ここではループ内で同じゲーム盤に対してこのコードが何度も繰り返し呼び出されるので、ループの実行時間にまんべんなく影響します。

以上で、コードの動作と最適化方法について理解いただいたかと思います。

この後のセクションでは、RubyとPythonのどちらにも適用できたスクリプト言語の最適化手法のコンセプトについて解説します。Pythonコードを高速化する方法を理解できれば、任意のスクリプト言語を高速化できるようになります。最終的なコードやベンチマークは最後に示します。

ロジックをDRYにする

最適化の基本のひとつは、作業の重複を解消することです。ある要素をfringe listに1つ置くと、その位置はlist comprehensionで既に有効性がチェック済みになりますが、そのlistから他の要素を取り出すときに同じ要素について有効性を繰り返しチェックしてしまっています。つまり、8つのマスが有効な場合、各マスを2回ずつチェックしなければならなかったということです。

このチェックを1回に減らせば高速化できます。

オブジェクトよりロジックを優先する

スクリプト言語におけるオブジェクト作成は、メモリもCPUサイクルも消費します。このメモリを後でGC(ガベージコレクション)するときに、CPUサイクルを余分に消費してしまいます。

オブジェクトによっては、作成のコストが低いものがあります。複雑なオブジェクトは、その分作成やコピーに時間がかかります。list(Rubyで言うarray)やdict(Rubyで言うhash)は、stringよりもずっと作成コストが高くなります。同様に、stringよりもintegerの方が作成コストは低くなります。

このあたりの説明は難しいので、無理矢理な例を作りました。None(Rubyで言うnil)またはNoneでない値を1つ与えられ、これをlistに追加して後で処理するとします。次の例をご覧ください。

my_list = [value] # <== ここでlistに割り当てられる
if None in my_list:
    return

# ...

上の例では、listと関係ない場合であってもlistの割り当てが発生してしまいます。そこで、次のようにarrayの割り当てが発生する前に値をチェックすればずっと高速になります。

if value is None:
  return
my_list = [value]
# ...

もちろん、最初の例のようなコードを書く人はまずいないことは承知していますが、要点はご理解いただけると思います。

オブジェクトは遅く、ロジックは速い。これはRubyとPythonのどちらにも通用する真理です。

シリアライズ方法に注意

オブジェクト生成削減の際に考えておく点が2つあります。オブジェクト生成の実際のオーバーヘッドと、そのオブジェクトの参照方法や使用方法です。オブジェクトが割り当てられた後、どのように操作されるのでしょうか。

ここでは、データをシリアライズして内部から値を取り出し、タプルからの値の取り出しにコストがかかる様子を観察したいと思います。この例では、位置をタプルから展開しています。

move = (1, 0)
# ...
r, c = move

rcの取り出しコストはゼロではありません。次のよく似た2つの関数をご覧ください。

def function_one(move):
    r, c = move
    return r + c

def function_two(r, c):
    return r + c

タプルを渡すよりも、function_twoのように値をバラして渡す方がはるかに高速です。kernprofでベンチマークを取ってみたところ、function_oneではタプルから値を取り出さないと利用できないため、倍の時間がかかりました。

パフォーマンスを最大限に改善するためには、データ操作を可能な限り最小限に絞るのが理想的です。

ループ内のリテラルに注意

JITを持たないスクリプト言語で、次のコードが呼び出されるたびに何が起きるでしょうか。

valid_moves = []

呼び出しコストの非常に高いarrayが割り当てられます。コード例ではループ内でリテラルを明示的に使っていませんが、このコードがどのように実行されるかについても考慮が必要になることがあります。本記事のコード例では、このget_legal_moves_slow関数は何度も何度も繰り返し呼び出されます。上のコード例でforwhileは使われていないからといって、このコードがループ内に置かれないとは言い切れません。

この場合、改変が発生してこの関数が呼び出されるたびにvalid_movesが必要になります。変更されない静的な値がいくつもあることにお気づきでしょうか。

次の場合で考えてみましょう。

directions = [ (-1, -1), (-1, 0), (-1, 1),
                (0, -1),          (0,  1),
                (1, -1), (1,  0), (1,  1)]

比較的読みやすいこのちっぽけなコードが呼び出されるたびに、1つのlist、8つのタプル、16のinteger参照が割り当てられてしまいます。このlistは決して改変されないのですから、これを関数の外に追い出して、起動時に1回しか作成されないグローバル定数に保存すれば、割り当てを大幅に削減できます。

チェックせずにスキップせよ

同じチェックを2回繰り返さないことについては既に述べました。最も高速なコードは「コードを書かないこと」です。理想のチェック回数は、ずばりゼロ回です。

そのために安全でないコードを書けということではありません。しかし、絶対ありえないシナリオの存在に気づくことができれば、そのチェックは不要になります。

コード例のどこに適用すればよいでしょうか。次をご覧ください。

def move_is_legal(row, col):
    return 0 <= row < height and
           0 <= col < width and
           board_spaces_occupied[row][col] == BLANK

ゲーム盤のマスが空いているかどうかをチェックする前に、ゲーム盤からはみ出していないかどうかをチェックしています。

どうすればこのチェックを削除できるでしょうか。1つの方法は、ゲーム盤の周りに境界を設定して、移動を展開したときにマスが空いていないことがわかるようにすることです。しかしこの方法では他の計算の難易度が上がってしまいます。

ロジックを曲げずに行う方法は1つありますが、それについては後述します。ここでのポイントは、チェックロジックを削除可能かどうかを検討することです。

メソッドがなければ問題もなくなる

メソッド呼び出し(Pythonでは関数呼び出し)のコストはゼロではありません。あるメソッドが呼び出されると、インタプリタはそのメソッドが存在する場所を探索してからコードを呼び出さなければなりません。

メソッドはコードをクリーンかつ理解しやすくするうえで有用ですし、ボトルネックの99%はメソッド呼び出しではないので、メソッドを削ってしまえと書くのはためらわれます。メソッドの削除は最適化としては非常に微細なものですが、それでも一片の真実はあります。

メソッド探索の回数についても考えてみましょう。Pythonの実装でlistのインデックスが何回探索されるのかは私も知りませんが、Rubyの場合はメソッドとして探索されます。

board_spaces_occupied[row][col] == BLANK

したがって、上のコードでは探索が1回ではなく2回実行されます。1回目はboard_spaces_occupied[row]にアクセスします。listが返されると、colを介して2つ目の要素にアクセスします。

メソッドを削除して操作の回数を削減できれば、1つのデータ構造に対して操作を実行することで高速化できるはずです。

ありがたいことに、メソッド呼び出し回数やarrayのインデックス参照などは、(言語の)コア開発者によって最適化されているので十分速いのが普通であり、したがって最適化する意味がありません。

言い換えると、プログラムを歪めてまで探索やメソッド呼び出しの回数を減らしてはいけません。(プログラムを歪めずに)探索を簡単に減らせるなら、ぜひそうしましょう。

ベンチマークを取る

パフォーマンス厨の皆さんに警告: コードのパフォーマンスをチューニングするときには、必ず変更前/変更中/変更後にベンチマークを取りましょう。本記事でのアドバイスはほとんどの場合一般的に通用しますが、特定のユースケースには適していないこともありえます。だからこそベンチマークは絶対省略しないでください。ベンチマークの正しい取り方はまったく別の問題なので、別の機会にしようと思います。

ときにはルールを完全無視

オブジェクトの数が多い方が有利な場合もあります。キャッシュを使えば、メモリと引き換えにCPUを節約できます。移動方向を保存するarrayを定数に移すことについては既に説明しましたが、それを微細なレベルで行います。つまり、コードをメモリ上に強制的に常駐させるのです。これと引き換えに、オブジェクト再構築のためのCPUサイクルをまったく消費せずに済みます。オブジェクトがキャッシュされるからです。

どんなものをキャッシュできるのでしょうか。高さと幅が固定であることと、移動のルールが変更される可能性がないことはわかっています。ゲーム盤上の位置は変更される可能性があります。ここを考慮して、位置ごとの有効なすべての移動のlistを事前に算出しておくという手が使えます。

ゲーム盤のステートが変更される可能性に注意しなければならないので、各展開方向のlistを作成するときにこの点を考慮します。たとえば、(3, 3)から右方向への移動は次のようになります。

[(3, 4), (3, 5), (3, 6)]

(3, 5)で何か変更が生じるとどうなるでしょうか。このマスが空かどうかをチェックして削除することはできますが、その場合(3, 6)も到達不能になります。移動方向ごとにlistが1つずつあるので、埋まったマスが最初に見つかった時点で列挙を中断すればよいのです。

ここまでの最適化をまとめて行う

まず、移動方向をarrayに保存します。

STAR_DIRECTIONS = [  (-1, 0), (1,  0), # 上下
                     (0, -1), (0,  1), # 左右
                     (1, -1), (1,  1), (-1, -1), (-1, 1)] # 斜め

移動の順序は重要ではありません。この後をご覧いただければわかります。

次にマスが空いているかどうかの探索を(2回ではなく)1回の呼び出しで行い、個別のrow/columnの組み合わせではなくタプルに基くようにしたいと思います。そこで、位置をインデックスとして持つdict(Rubyで言うhash)を1つ作成します。

def build_blank_space_dict():
    blank_space_dict = {}
    for c in range(0, height):
        for r in range(0, width):
            blank_space_dict[(c, r)] = (board_spaces_occupied[c][r] == BLANK)
    return blank_space_dict

上を次のように使います。

def move_is_legal_from_dict(move):
    return 0 <= move[0] < height and
           0 <= move[1] < width and
           blank_space_dict[move]

次が最もコストの高い部分です。ゲーム盤での有効な移動をすべて事前に算出してキャッシュしたいと思います。この作業のコストは高いのですが、起動時に1回行えば済むので、再計算が不要になります。

def calculate_first_move_guess_dict():
    first_move_guess_dict = {}
    for r in range(0, height):
        for c in range(0, width):
            rc_tuple = (r, c)
            first_move_guess_dict[rc_tuple] = []
            for delta_r, delta_c in STAR_DIRECTIONS:
                valid_guesses = []
                dr = delta_r
                dc = delta_c
                move = (r + dr, c + dc)
                while move_is_legal_from_dict(move):
                    valid_guesses.append(move)
                    dr += delta_r
                    dc += delta_c
                    move = (r + dr, c + dc)

                first_move_guess_dict[rc_tuple].append(valid_guesses)
    return first_move_guess_dict

長くなりましたが、変更後のコードは上のようになります。ここで行っているのは、上述のget_legal_moves_slow()と基本的にまったく同じです。大きな違いは、rowやcolumnごとの可能な移動を二重ループの中でビルドしていることです。

move_is_legal_from_dict()で移動の有効性をチェックしてからlistに追加していますが、これに気づくことが重要です。これは位置がゲーム盤からはみ出しているかどうかのチェックなので、削除します。これで、後でゲーム盤のステートをチェックするときに同じチェックを繰り返さずに済みます(先ほど「後述する」と書いたのはこれです)。

お待ちかねの最終的なメソッドは次のようになります。

def get_legal_moves_fast(move):
    valid_moves = []
    for direction_list in first_move_guess_dict[move]:
        for move in direction_list:
            if blank_space_dict[move]:
                valid_moves.append(move)
            else:
                break # そこから先の方向への移動はすべて無効

    return valid_moves

有効な移動のarrayをfirst_move_guess_dict[move]で探索します。ゲーム盤が3×3の場合、位置(1, 1)の結果は次のようになります。

[[(0, 1)], [(2, 1)], [(1, 0)], [(1, 2)], [(2, 0)], [(2, 2)], [(0, 0)], [(0, 2)]]

サブarrayにある各要素をループで列挙し、blank_space_dict[move]でチェックします。有効な場合は有効な移動に追加し、無効な場合は内側のループをbreakします(その位置から先の方向はクイーンの移動ルール上無効であるため)。

最後に、有効な移動のタプルのlistを1つ返します。

パフォーマンスの比較はどのように行えばよいでしょうか。私はkernprofで確認しました。

$ kernprof -l -v perf.py
Wrote profile results to perf.py.lprof
Timer unit: 1e-06 s

Total time: 1.17439 s
File: perf.py
Function: get_legal_moves_fast at line 53

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
    53                                           @profile
    54                                           def get_legal_moves_fast(move):
    55    100000        41991      0.4      3.6      valid_moves = []
    56    900000       378690      0.4     32.2      for direction_list in first_move_guess_dict[move]:
    57   1000000       491422      0.5     41.8          for move in direction_list:
    58    200000       106333      0.5      9.1              if blank_space_dict[move]:
    59    200000       116717      0.6      9.9                  valid_moves.append(move)
    60                                                       else:
    61                                                           break # rest of direction is invalid
    62
    63    100000        39242      0.4      3.3      return valid_moves

Total time: 5.80368 s
File: perf.py
Function: get_legal_moves_slow at line 69

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
    69                                           @profile
    70                                           def get_legal_moves_slow(move):
    71    100000        79230      0.8      1.4      r, c = move
    72
    73    100000        78106      0.8      1.3      directions = [ (-1, -1), (-1, 0), (-1, 1),
    74    100000        75957      0.8      1.3                      (0, -1),          (0,  1),
    75    100000        94609      0.9      1.6                      (1, -1), (1,  0), (1,  1)]
    76
    77    900000       739500      0.8     12.7      fringe = [((r+dr,c+dc), (dr,dc)) for dr, dc in directions
    78    800000      1704089      2.1     29.4              if move_is_legal(r+dr, c+dc)]
    79
    80    100000        79558      0.8      1.4      valid_moves = []
    81
    82    500000       406481      0.8      7.0      while fringe:
    83    400000       471503      1.2      8.1          move, delta = fringe.pop()
    84
    85    400000       323095      0.8      5.6          r, c = move
    86    400000       321289      0.8      5.5          dr, dc = delta
    87
    88    400000       736811      1.8     12.7          if move_is_legal(r,c):
    89    200000       214678      1.1      3.7              new_move = ((r+dr, c+dc), (dr,dc))
    90    200000       215883      1.1      3.7              fringe.append(new_move)
    91    200000       189134      0.9      3.3              valid_moves.append(move)
    92
    93    100000        73752      0.7      1.3      return valid_moves

本記事をスマートフォンでお読みの方向けに、重要部分を以下に抜粋しました。

Total time: 1.17439 s
File: perf.py
Function: get_legal_moves_fast at line 53

Total time: 5.80368 s
File: perf.py
Function: get_legal_moves_slow at line 69

最適化前のメソッドの所要時間は5.80368秒、改善後のメソッドでは1.17439秒にまで短縮されました。パフォーマンスは約5倍向上しました。

これをさらに高速化することは可能でしょうか。

説明してませんでしたが、操作によってはコストの高いものがあります。私の見たところ、Pythonのlistに対するappend()は最適化があまり進んでいません。同じlistへのappend()を繰り返すのではなく、おそらく固定サイズのarrayを1つ初期化してそのインデックスに要素を追加し、続いてNone(Rubyで言うnil)をすべて削除してからarrayをリサイズする方法も考えられます。繰り返しになりますが、これが本当かどうかを確認するにはベンチマークをもれなく取ってください。

numpyが使えるのであれば、何か高速化の方法があるかもしれません。GVLを持たないプラットフォーム(RubyもPythonもこれには該当しません)であれば、移動方向ごとのループを並列化できるかもしれません。別のスレッドに置くためにスケジューリング時間を使う値打ちはおそらくありませんが。

また、私がPythonについて何か取りこぼしている点があるかもしれませんし、すべてを高速化できる方法を私が知らない言語もあります。これまで述べてきたのは、(Pythonの)itertoolsモジュール、functoolsモジュール、operatorモジュールについてです。

更に言うと、私のコードは元のものより複雑で、良くも悪くも凄いことになっています。作業内容も保存されるステートも多く、メモリ消費も増加しています。とは言うものの、私にとって重要なのは最後の「有効な移動を取得する」関数の呼び出し回数でした。この関数は読みにくいぐらい短く簡潔ですが、一方で各部分の協調動作について多くの知識を要求します。

私はコーディング時間のほとんどを、人間にとって読みやすいコードを書くことに費やしています。パフォーマンスが5倍アップするのは結構な話ですが、同僚のコードメンテ時間が10倍になったら何にもなりません。パフォーマンスではCPUやRAMを考慮しますが、同様に人間にとってのコストも考慮しましょう。私は普通、最初に遅いコードを書き、次にプロファイリングを行い、重要なホットスポットだけを最適化します。今回は、ここが私にとってのホットスポットでした。

お読みいただきありがとうございます。

関連記事

ベンチマークの詳しい理解と修正のコツ(翻訳)

メモリを意識したRubyプログラミング(翻訳)

Ruby 2.5のパフォーマンス改善点(翻訳)

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

RailsアプリをAWS Elastic Beanstalkにデプロイする手順(翻訳)

$
0
0

概要

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

RailsアプリをAWS Elastic Beanstalkにデプロイする手順(翻訳)

前回の記事はElixirをElastic Beanstalkにデプロイする方法でしたが、今回は数あるフレームワークの中でもRailsを愛する会社であるSyndicodeより、RailsアプリをAWS Elastic Beanstalkにデプロイする方法のチュートリアルをお送りいたします。なお弊社ではRails開発者を絶賛募集中です。

Elastic Beanstalkについて簡単におさらいします。これはAWS(Amazon Web Services)上にアプリを設定する手順を自動化するクラウドデプロイメントサービスです。本チュートリアルではコマンドラインインターフェイス(CLI)を用いてAWS Elastic BeanstalkにRailsアプリをデプロイする方法をご紹介します。

1. Elastic BeanstalkのCLIをインストールする

Macの場合、HomeBrewでaws-elasticbeanstalkをインストールします。

brew install aws-elasticbeanstalk

Homebrewを使ったことがない場合やLinux環境の場合は、pip(Pythonのパッケージ管理ツール)でインストールします。

sudo pip install awsebcli

Windowsの場合は以下を実行します。

pip install awsebcli

2. Raisアプリをgit cloneする

ここではhttps://github.com/engineyard/todo.gitのサンプルアプリをgit cloneします。Elastic Beanstalk CLIで自分のRailsアプリを使いたい場合は、アプリをGitリポジトリに置く必要があります。

git clone https://github.com/engineyard/todo.git

3. IAMロールを作成する

AWSサイトの手順に従って、IAMロールを2つ作成します。必要なのはaws-elasticbeanstalk-ec2-roleaws-elasticbeanstalk-service-roleです。アプリや環境はElastic Beanstalk CLIで作成しますが、これらのIAMロールの作成だけはElastic Beanstalkコンソールで[Create New Application]をクリックして行います。この手順は一度だけ行う必要があります。以下を順に実行します。

  • Elastic Beanstalk consoleを開きます。
  • [Create New Application]を選択します。
  • ウィザードに従って[Permission]ページまで進みます。
  • [Next]をクリックしてIAM consoleを開きます。
  • [Allow]を選択してロールを作成します。

4. Elastic Beanstalk CLIのセットアップ

cd todo
eb init

AWS Access KeyとAWS Secret Access Keyが1つずつ必要です。ない場合は、IAM consoleでユーザーを1つ作成してcredentialをダウンロードします。リージョン、利用するアプリ([Create New Application])、プラットフォームのバージョンはデフォルト値で構いません。共有するToDoアプリには既にプラットフォームのバージョンが含まれています。独自のRailsアプリを使う場合は、Ruby 2.3 Pumaを必ず追加してください。

5. 環境を作成する

eb create todo_production

Elastic BeanstalkはSecurity Group、ELB、Auto Scalingグループを作成します。3分もすれば環境が整い、以下でアプリをチェックできるようになります。

eb open

環境の情報


Engine Yardのサンプルデプロイアプリより

以下にご注意ください。

  • Rubyは/opt/rubies/ruby-2.3.4の下に置かれます
  • Railsアプリは/var/app/currentの下に置かれます
  • ユーザー名はWebアプリ名が使われます

6. データベースを作成する

このToDoアプリの設定は、実際のアプリで使うには少々正しくない点があります。ToDoアプリのconfig/database.ymlではSQLite3データベースを使っているため、データベースを設定していなくても動いてしまいます。todo_production環境がEC2インスタンスを1つ持っているだけなので、さしあたってこれで十分です。しかしAuto ScalingグループでEC2インスタンスがもうひとつ作成されると、新しいインスタンスは独自のSQLite3データベースを持ちます。これは期待する動作ではないでしょう。データベースを1つにまとめるには、config/database.ymlを削除します。DATABASE_URLを使うので、このファイルは不要になります。

git rm config/database.yml
git commit -m 'Remove database.yml'

RDS consoleでRDSインスタンスを1つ作成します。MySQLまたはPostgreSQLを使えます。このToDoアプリのGemfileにはmysql2 gemとpg gemが両方入っているので、どちらもサポートされています。オプションのデータベース名フィールドには「todo」と入力します。論理データベースは、RDSインスタンスの作成後に作成されます。セキュリティのため、[Publicly Accessible]はNoに設定します。RDSインスタンスの準備が整ったら、Detailsアイコンをクリックして[Security Group]をクリックします。rds-launch-wizard-2 (sg-041b107e)のような文字列が表示されます。

[Security Group]ページで、[Inbound]、[Edit]の順にクリックし、PostgreSQL用のルールを追加します。sourceでCustomを選択してElastic Beanstalk環境のSecurity Groupを入力します。sgと入力するとSecurity Groupのリストが表示されます。

正しいSecurity Groupが見つからない場合は、グループ名にElastic Beanstalk環境idが使われています。環境idを取得するには、eb statusと入力します。私の場合、環境idがe-kq7hjkf7dtで、Security Group名がawseb-e-kq7hjkf7dt-stack-AWSEBSecurityGroup-44MI138FQVGとなっています。AWSEBLoadBalancerSecurityGroupを含む名前は選択しないでください。


Engine Yardのキャプチャ画像より

これでElastic Beanstalkによって作成されたEC2インスタンスがRDSインスタンスにアクセスできるようになるはずです。このElastic Beanstalk環境に紐付けられるRDSインスタンスを作成することもできますが、この環境をterminateするとRDSインスタンスもterminateしてしまうためおすすめできません。

7. DATABASE_URLを設定する

RDS credentialを使ってDATABASE_URL環境変数を設定します。形式はdb_type://username:password@hostname:port/db_nameです。たとえば、PostgreSQLインスタンスを作成した場合は次のcredentialを使います。

user: engineyard
password: mysecretpassword
hostname: eypostgres.cjb9zibjzcpd.us-west-2.rds.amazonaws.com
port: 5432
db name: todo

続いて以下を実行します。

eb setenv DATABASE_URL=postgres://engineyard:mysecretpassword@eypostgres.cjb9zibjzcpd.us-west-2.rds.amazonaws.com:5432/todo

次に、アプリをdatabase.ymlなしでデプロイしてページを開きます。

eb deploy
eb open

以上で、単独のRDSインスタンスを用いるElastic Beanstalk上でRailsアプリが動きました。

8. Secret Key Base

この環境変数を使わない場合は、SECRET_KEY_BASEを設定するか、encrypted Rails secretsを使う場合はRAILS_MASTER_KEYを設定します。bundle exec rake secretでsecret key baseを新しく生成します。

eb setenv SECRET_KEY_BASE=cccae61c0c117c787745b596655caa50062dc3fc739505df02e209d9e737a2f39ab484d20e63d5937e1c58901e81109523807f66be421728851fecc2262ed5a8

9. SSH

eb initを実行すると、public keyを追加できます。新しいkeyペアを作成することもできます。eb initを既に実行した場合は、--interactiveオプションを付けて再度実行することもできます。eb sshを実行して、自分の環境のEC2インスタンスに接続します。

10. 以上でおしまいです

AWS Elastic BeanstalkにRailsアプリをデプロイする簡単な方法をご紹介いたしました。サポートされるRubyバージョンやAppサーバーに制約があるため、一部については制御できないことがあります。Sidekiqなどのバックグランドワーカーについては改良が必要です。現時点では、.ebextensionsにファイルをひとつ作成し、SidekiqワーカーをRailsアプリと同じインスタンスで実行する必要があります。中規模アプリでは、Sidekiq専用のインスタンスを用意すべきです。

もっと詳しく知りたい方は、ぜひweekly newsletterをご購読ください。

関連記事

無料で使えるAWSアカウント用セキュリティ監査ツールの紹介(翻訳)

Rails 5.2を待たずに今すぐActiveStorageを使ってみた(翻訳)

Rubyのクラスメソッドをclass

$
0
0

概要

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

Rubyのクラスメソッドをclass << selfで定義している理由(翻訳)


https://pixnio.com/nature-landscapes/winter/landscape-sky-winter-snow-ice-water-tree-nature-outdoor-reflectionより

クラスメソッドは私の同僚の間で常に議論や反論の種になっています。クラスメソッドは的確かつ有用と考える人もいますが、実際にはコードの読みやすさや管理のしやすさを損ないがちな邪魔者だと感じる人もいます。私はRubyのオブジェクト指向的な本質を信奉しており、オブジェクトで考えることを(読むのも!)好んでいますが、私には後者が真実になる傾向があることに気づきました。よく言われるように、クラスメソッドがどうしても必要になることもあります。ファクトリーメソッドから、ActiveRecordモデルのカスタムクエリメソッドで使われる複雑なメタプログラミングインターフェイスにいたるまで、クラスメソッドを全否定することはできません。もちろんクラスメソッドの利用は控えめにすべきではありますが(詳しくはCode Climateのこちらの良記事をご覧ください)。

本記事ではクラスメソッドそのものの良し悪しについては言及せず、クラスメソッドが必要になった場合のクラスメソッドのスタイル上の記法について議論します。

コーディングスタイルとスタイルガイド

Rubyスタイルガイドでは、クラスメソッドはdef self.methodという記法を用いるのが望ましいとされています。このスタイルでは、より明示的な def ClassName.methodという記法が批判されていますが、より謎めいたclass << selfという記法も補助的にサポートされています。記法の実際の表示については該当のセクションをご覧ください。

組織内でスタイルガイドを共有し、それに従うことは重要です。Sandi Metz氏の良記事では次のように指摘されています。

(略)スタイル上の選択は多くの場合任意であり、純粋に個人の好みの問題です。スタイルガイドを選択することは、ほとんど重要でない点で意見が割れてしまった場合の合意を形成するということです。スタイル(そのもの)が重要だということではなく、スタイルが揃うことが重要です
Why we argue styleより

この指摘は実にもっともです。しかし、スタイルを正しく選択できることもやはり重要です。服装と同じく、コードのスタイルには開発者としての信条や価値観や哲学が反映されるのですから、この問題についても十分理解が必要です。私たちの誰もが多くのクラスメソッドを定義していますが、果たして私たちはクラスメソッドの動作を理解しているのでしょうか。


https://images.askmen.com/1080×540/2015/11/06-042951-men_s_fashion_must_haves.jpgより

シングルトンクラス

上の問いに答えるために、Rubyのオブジェクトモデルについて取り急ぎ調べる必要があります。一般に、Rubyのメソッドはクラスに保存され、データはオブジェクト(クラスのインスタンス)に保存されます。これはかなり一般的な知識なので、次の例でもう少し追ってみましょう。

an_array = [1, 5, 10]

an_array.averageを実行すると、Arrayやそのスーパークラスにaverageメソッドが定義されていないので、次のようにNoMethodErrorエラーが出力されます。

an_array.average
# NoMethodError: undefined method `average' for [1, 5, 10]:Array

Arrayにモンキーパッチを適用してaverageメソッドを定義してもよいのですが、このメソッドはan_array以外では不要なのであれば、次のようにすることもできます。

def an_array.average
  reduce(:+) / count.to_f
end

これで次のように動きます。

an_array.average
# => 5.333333333333333

同じメソッドをArrayの別のインスタンスで実行しようとすれば、次のようにまたNoMethodErrorが出力されます。

another_array = [1, 3, 7]
another_array.average
# => NoMethodError: undefined method `average' for [1, 3, 7]:Array

この理由は、Rubyが舞台裏でaverageメソッドを特殊なクラスに保存しているからです。an_arrayだけがその特殊なクラスを指しています。つまり自身のシングルトンクラスです。

an_array.singleton_class
# => #<Class:#<Array:0x007fcf27848750>>

an_array.singleton_methods
# => [:average]

Rubyでは、どんなクラスのどんなインスタンスにも必ず自身のシングルトンクラスがあり、そこにシングルトンメソッドが保存されます。先ほど定義したメソッドもシングルトンメソッドであり、シングルトンクラスに保存されています(ただし例外として、Numericオブジェクトはこれに該当しません)。

あるオブジェクトのメソッドを呼び出すと、Rubyは最初にそのオブジェクトのシングルトンクラスでメソッドを探索し、それから通常のクラスや先祖クラスのチェインを探索します。

クラスメソッドはすなわちシングルトンメソッドである

Rubyではクラスもオブジェクトなので、クラスメソッドはClassの特定のインスタンス上に定義された単なるメソッドです。次の例で考えてみましょう(gist)。

class Example
  def self.a_class_method; end
  def an_instance_method; end
end

実際の動作を見れば理屈はすぐわかります。

Example.is_a? Object
# => true

Example.class
# => Class

Example.singleton_class
 => #<Class:Example>

Example.instance_methods(false)
 => [:an_instance_method]

Example.singleton_class.instance_methods(false)
 => [:a_class_method]

instance_methodsの引数をfalseで呼び出すと、継承されたメソッドを除いたメソッドリストが返されます(instance_methods)。

記法を選ぶ

ついにRubyのクラスメソッドを正確に理解できました。これでコーディング方法について詳しく議論する準備が整いました。本記事のタイトルでおわかりのように、私はdef self.method記法よりclass << self記法を好んでいます。理由はおわかりでしょうか?

私はメソッドを、それらが属しているクラスの内部で定義したいのです。class << self記法ならこのアプローチ、すなわちメソッドを実際のシングルトンクラスのスコープ内で定義していることがはっきりと伝わります。しかしdef self.methodを使うと、複数のスコープにわたってメソッドを定義していることになります。通常のクラススコープの中にいるにもかかわらず、コードのどの場所でも特定のインスタンスにメソッドを定義できるRubyの機能を使っているのです。クラス定義内におけるselfは、その場所における現在の(クラス自身などの)Classインスタンスを指します。このため、def self.methodを使うとスコープを飛び越えることになり、私にはこれがおかしいと感じられるのです。

def self.method記法にはもうひとつ疑問があります。その理由は、privateやprotectedなメソッドを定義できることです。Rubyには、クラスメソッドをprivateとして宣言するためのprivate_class_methodメソッドが用意されていますが、protectedメソッドについては同等のものがありません。また、privateなクラスメソッドでは、各メソッドを個別に宣言しなければなりません(たとえば、クラスの途中で単純にprivateを用いることはできません: これはそのクラスのインスタンスメソッドに適用されるからです)。要は、class << selfの方が実際にはより明確だということです。

予想される反論

Sandi Metzの記事で指摘されているように、スタイルに関する議論はときとして開発者の間で感情的にこじれてしまうことがあります。class << self記法について予想される反論として次のようなものが考えられます。

  • 大きなクラスだとどれがクラスメソッドだかわかりにくい: 私もその点にはまったく同意です。コード量が450行もある神クラスなら、まずリファクタリングしてより小さなクラスに分解しつつdef self.method記法は変えないようにすべきでしょう。いくらスクロールしても終わらないほどコードが多ければ、スコープが変わる場所を簡単に見落としてしまいます。しかしそれでもそのクラスはリファクタリングすべきです
  • 明快でなくなる: これは一方的な見方ですし、根拠も不十分です。def self.methodはまったくもって明快ではありません。前述のようにdef self.methodの本当の意味を理解できれば、def self.methodはスコープが混じるために実際には曖昧になってしまいます。この動作の背後にある理論を把握することが理解の助けになります。

  • 素直にスタイルガイドに従えばいいんじゃね?: これに対する私の回答は、動作の詳細に注意を向けることが多くの場合重要だということです。これは重大な問題ではありませんし、def self.methodは「コードの匂い」にも該当しません。実際、これ自体議論になるかどうかというトリビアな問題です。それぞれの選択肢について動作を学んだり知識を得たりすることで、初めて議論に値打ちが生まれます。さらに、class << self記法は私にとってよりよいものであり、RubyやRubyのオブジェクト指向を安定して理解できると信じています。最後に、スタイルガイドは変更/改訂される(ただし慎重にですが)可能性があることもどうかお忘れなく。

本記事が皆さんにとって有益であることを願っています。このトピックについてお気づきの点やご質問がありましたらコメント欄までどうぞ。本記事が面白い/有益だと思っていただけた方は、ぜひ👏ボタンをクリックして応援してください。

関連記事

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

Railsの`Object#try`がダメな理由と効果的な代替手段(翻訳)

週刊Railsウォッチ(20171222)定番gemまとめサイト、active_record-mtiでテーブル継承、PostgreSQL 10の非互換変更点、Railsガイド攻略法ほか

$
0
0

こんにちは、hachi8833です。Ruby 2.5リリースまでもう少しです。

Rails勉強会@東京 第93回に初参加させていただきました。2年半ぶりの開催だったそうです。

お知らせ: 年末年始にかけて週刊Railsウォッチをお休みいたします: 次回は1月12日です。

今年最後のRailsウォッチ、いってみましょう。

Rails: 今週の改修

今週の更新情報は多めなので選別してみました。

セキュリティ関連ヘッダーを追加

# actionpack/lib/action_dispatch/railtie.rb#26
     config.action_dispatch.default_headers = {
       "X-Frame-Options" => "SAMEORIGIN",
       "X-XSS-Protection" => "1; mode=block",
-      "X-Content-Type-Options" => "nosniff"
+      "X-Content-Type-Options" => "nosniff",
+      "X-Download-Options" => "noopen",
+      "X-Permitted-Cross-Domain-Policies" => "none"
     }

つっつきボイス: 「X-で始まるヘッダって確かIE向けがほとんどだったと思うはRFC定義されていない独自拡張を表している」「X-Download-Options(IE向け)はまだわかるけど、↓の良記事見るとFlashがらみのヘッダが目につくなー: Railsでそこまで手を回すのってどうなんだろ?」「secureheadersにもこのヘッダ入ってるんで、セキュリティ関係者が入れときたいと思ってるらしいことはワカッタ」「10月のウォッチで扱ったgemですね」

db.createのエッジケースを修正

スキーマキャッシュの読み込み時には現在のマイグレーションバージョンをフェッチする。
しかしデータベースが存在しない場合に接続を取れずにエラーになる。これはデータベース作成時に問題になる。
データベースがない場合はスキーマキャッシュは不要なのでエラーを無視するよう修正。
#31311より大意

# activerecord/lib/active_record/migration.rb#56
-      def current_version(connection = Base.connection)
+      def current_version(connection = nil)
+        if connection.nil?
+          begin
+            connection = Base.connection
+          rescue ActiveRecord::NoDatabaseError
+            return nil
+          end
+        end

ActiveStorage::Blobからvariantを削除

# activestorage/app/models/active_storage/blob.rb#273
  def delete
-    service.delete key
+    service.delete(key)
+    service.delete_prefixed("variants/#{key}/") if image?
 @kaspth

つっつきボイス: 「variantって、作成した後削除し忘れてつまづきがちなやつ」「ところでblobって言葉ここに限らずいろんなところで見かけるんですが、どんな意味でしたっけ」「だいたいバイナリを表すことが多いっすね」「バイナリ・ラージ・オブジェクトの略なのか」「オブジェクト指向のオブジェクトではないw」

Railsのblobについての記述は以下にありました。

blobは、そのサービス上にあるファイルの位置を示すファイルとキーについてのメタデータを含むレコードです。
rails/activestorage/app/models/active_storage/blob.rbより大意

Railsの起動メッセージがきびきび表示されるようになった

起動直後に=> Booting Railsを表示するようになりました。

# railties/lib/rails/generators/rails/app/templates/config/boot.rb.tt#3
require 'bundler/setup' # Set up gems listed in the Gemfile.
require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
+
+if %w[s server c console].any? { |a| ARGV.include?(a) }
+  puts "=> Booting Rails"
+end

つっつきボイス: 「これは地味にうれしい」「今までなかったのが不思議ですね」

foreign_keysinformation_schemaを修正

初コミットおめでとうございます。

# activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb#406
-          FROM information_schema.key_column_usage fk
-          JOIN information_schema.referential_constraints rc
+          FROM information_schema.referential_constraints rc
+          JOIN information_schema.key_column_usage fk
           USING (constraint_schema, constraint_name)
           WHERE fk.referenced_column_name IS NOT NULL
             AND fk.table_schema = #{scope[:schema]}
             AND fk.table_name = #{scope[:name]}
+            AND rc.constraint_schema = #{scope[:schema]}
             AND rc.table_name = #{scope[:name]}

つっつきボイス: 「これはMySQL向けだな: information_schemaがあるからすぐわかる」「JOINの方向を逆にしたのか」

SQLを発行したコードの行をオプションで出力

development.rbでconfig.active_record.verbose_query_logs = trueとすることでapp/views/news/show.html.erb:9:inのように出力されるようになりました。

# 26815より
Started GET "/news/popular" for ::1 at 2016-10-19 00:57:48 +0200
Processing by NewsController#popular as HTML
  Version Load (57.3ms)  SELECT  "versions".* FROM "versions" INNER JOIN "rubygems" ON "rubygems"."id" = "versions"."rubygem_id" LEFT OUTER JOIN "gem_downloads" ON "gem_downloads"."rubygem_id" = "rubygems"."id" AND "gem_downloads"."version_id" = $1 WHERE ("versions"."created_at" BETWEEN '2016-10-11 22:57:48.145796' AND '2016-10-18 22:57:48.145965') AND "versions"."indexed" = $2  ORDER BY gem_downloads.count DESC, "versions"."created_at" DESC LIMIT 10 OFFSET 0  [["version_id", 0], ["indexed", "t"]]
  ↳ app/views/news/show.html.erb:9:in `_app_views_news_show_html_erb___2784629296874387000_70222193538980'
  Rubygem Load (0.4ms)  SELECT  "rubygems".* FROM "rubygems" WHERE "rubygems"."id" = $1 LIMIT 1  [["id", 19969]]
  ↳ app/views/news/_version.html.erb:1:in `_app_views_news__version_html_erb__2744651331114605013_70222191156360'
  Version Load (0.8ms)  SELECT  "versions".* FROM "versions" WHERE "versions"."rubygem_id" = $1 AND "versions"."latest" = $2  ORDER BY "versions"."position" ASC LIMIT 1  [["rubygem_id", 19969], ["latest", "t"]]
  ↳ app/helpers/application_helper.rb:23:in `gem_info'
  GemDownload Load (0.3ms)  SELECT  "gem_downloads".* FROM "gem_downloads" WHERE "gem_downloads"."version_id" = $1 AND "gem_downloads"."rubygem_id" = $2 LIMIT 1  [["version_id", 882133], ["rubygem_id", 19969]]
  ↳ app/models/version.rb:247:in `downloads_count'

つっつきボイス: 「これはマジありがたい!: eager-loadingしまくってるとどうなるかというのはあるけれど」「そういえばこの間joker1007さんが『activerecord-cause gemは役割を終えた』みたいなことを言ってたのはこの修正のことかな?」「ちょうどこの間のウォッチでactiverecord-cause取り上げたところだった」

nobuさんの珍しいコミット

# 81b99b2 と 2d5700b
-    @cache_path = Tempfile.create(%w{tmp cache}, Dir.tmpdir)
+    @cache_path = Dir.mktmpdir(%w{tmp cache})

# bff3ee8 と 4022f33
-    @cache_path = File.join Dir.tmpdir, Dir::Tmpname.make_tmpname('tmp', 'cache')
+    @cache_path = Tempfile.create(%w{tmp cache}, Dir.tmpdir)

つっつきボイス: 「Rails勉強会@東京で話題になってたので」「nobuさんはRubyのコミッターだからRailsにコミットするのは確かに珍しいかも」「それ用のメソッドがRubyにあるから使おうよ、ってことですね」

なおRubyとRailsの両方で活発に活動している方はAaron Patterson氏を始め結構います。

[インタビュー] Aaron Patterson(前編): GitHubとRails、日本語学習、バーベキュー(翻訳)

Rails

active_record-mti: PGネイティブの継承テーブルをARで使うgem

Rails勉強会@東京で評判がよかったやつです。

# TwilightCoders/active_record-mtiより
ActiveRecord::Schema.define(version: 20160910024954) do

  create_table "accounts", force: :cascade do |t|
    t.jsonb    "settings"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "users", inherits: "accounts" do |t|
    t.string "firstname"
    t.string "lastname"
  end

  create_table "developers", inherits: "users" do |t|
    t.string "url"
    t.string "api_key"
  end

end

つっつきボイス: 「MTI: Multiple Table Inheritance」「PostgreSQLにテーブル継承できる機能があるのは知ってたし、6系の頃に使ったことあったけど多重継承できるのか↓: READMEのサンプルでは多重継承してないけど↑」「いやほんと、ぽすぐれのテーブル継承、普通に使えるし便利やで?」「STIだとカラム増えすぎるんですよね」

CREATE TABLE boatcar () INHERITS (boat,car);

「やっぱりぽすぐれいいな」「ビュー(データベースビュー)もいいし: Railsにcreate view、ほすぃ~」「同意」「同意」

ついでに9.6でこんな記述を見つけました。

親テーブル上の検査制約と非NULL制約はその子テーブルに自動的に継承されます。 他の種類の制約(一意性制約、プライマリキー、外部キー制約)は継承されません
PostgreSQL 9.6 – 5.8. 継承より

Rails: STI(Single Table Inheritance)でハマったところ

active_mocker: ARのモックを生成するgem

これもRails勉強会で話題でした。

# 同リポジトリより
require 'rspec'
require 'active_mocker/rspec_helper'
require 'spec/mocks/person_mock'
require 'spec/mocks/account_mock'

describe 'Example', active_mocker:true do

  before do
    Person.create # stubbed for PersonMock.create
  end

end

つっつきボイス: 「schema.rbが更新されるとモックがfailする、と」「ARでDBアクセスしないなら当然速くなるな」「ただDBMSに入れてloadし直すことによって挙動変わる場合はどうなるんだろう」「fasterなテストとして普段はactive_mocker: trueを回しておいて、定期実行では普通にDB使って回す方が、DBMSが型変換したりするケースとかも考えれば安全そう」

RubyWeeklyの「2017年人気記事リスト」(Ruby Weeklyより)

年の瀬を感じる企画ですね。TechRachoで翻訳した記事もいくつか見当たりました。


rubyweekly.comより


つっつきボイス: 「英語圏のネット系マガジンは軒並み1月までお休みですね: さすがクリスマス最優先な文化圏」「TechRachoでもこの企画やればいいのに」「来週やりましょう!」

get_schwifty: ActionCableでRailsビューの一部をバックグラウンドレンダリング(Ruby Weeklyより)

# 同リポジトリより
# app/cables/calculator_cable.rb
class CalculatorCable < BaseCable
  def fibonacci
    n = SecureRandom.rand(30..40)
    calculated = calculate_fibonacci(n)
    stream partial: "calculator/fibonacci", locals: { calculated: calculated, n: n }
  end

  private

  def calculate_fibonacci(n)
    return n if n <= 1
    calculate_fibonacci( n - 1 ) + calculate_fibonacci( n - 2 )
  end
end

つっつきボイス: 「これとよく似たgemがあったなー: render_asyncだ」「TechRachoで記事にしてました↓」

Rails: render_async gemでレンダリングを高速化(翻訳)

「render_asyncは素直にjQueryで遅延loadingしていてとてもわかりやすい: その代わりStreamとかはできない」
「そしてこちらのget_schwiftyはActionCable使ってStream APIで転送するので、render_asyncよりもさらにいい感じに出せる: ただしTCPセッション消費するからworker枯渇が怖いけど」

技術的負債調査のポイント10個


codeclimate.comより


つっつきボイス: 「こちらCode Climateブログの記事です」「お、Code Climateブログって記事本数は少ないけど質がとっても高いんで信頼できる: だいたいほぼ文句付けようのないレベル」「私も読んでて同じこと思ったので次で過去記事掘り起こしてみました」

古典技術記事探訪: CodeclimateやSemaphore.ciブログ

いずれも2014年の記事ですが、今も通用しそう。


つっつきボイス: 「1は例の定番記事『肥大化したActiveRecordモデルをリファクタリングする7つの方法』で一番多かった質問『どうしてクラスメソッドでできることをわざわざインスタンスメソッドにするの?』に答えたものだそうです」「前にも話したけど、クラスメソッドで作ったものを後でインスタンスメソッドに変えるのはほんとつらい」

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

「4の図↓もよかったので」「さっきのactive_mockerとかは、まさにこのピラミッドの左下を目指すためのもの」「単体テストほど軽く速くするということですね」


codeclimate.comより

Faktory: バックグラウンドジョブサーバー

ちょっと検索しにくい名前ですが。


contribsys/faktoryより

Q. productionで使えますか?
A. なにぶん新しいプロジェクトなので、リスクを許容できるのならば。APIが安定したら1.0をリリースする予定で、自分では安定していると思います。

Q. FaktoryにはRedis必要ですか?
A. 不要です。Faktoryはスタンドアロンの64ビットLinuxバイナリであり、ジョブを回すのにFaktoryワーカープロセスが必要です。「Redis -> Sidekiq」 == 「Faktory -> Faktory worker」という関係です。

Changelog: 技術系Podcast

上のPodcastもここです。Rails勉強会でこのPodcastをチェックしている人が結構いたので。


changelog.comより


つっつきボイス: 「みっちり文字起こししているところが凄いんですよ」「こういうのがありがたいっすね: 検索でも見つけやすいし」

Decoratorを比較


  • 素のdecorator
  • Module + Extend + Super decorator
  • Plain Old Ruby Object decorator
  • Class + Method Missing decorator
  • SimpleDelegator + Super + Getobj decorator

thoughtbotの記事です。2015年ですがまとまりがよかったので。


つっつきボイス: 「ちょっと前に見つけた別のDecorator記事が構成的にちょっと残念だったので」「うんうんそういうのよくあるw」

Railsガイドで怖気づいた人向けの攻略方法


sihui.ioより


つっつきボイス: 「いっぺんに読もうとすると挫折するので最初に全体像を把握しよう的なアドバイスですね」「まーでもわかる: 最近のRailsガイドは情報てんこ盛りでガイドという感じでなくなりつつあるかなぁ」

モブプログラミング


codeclimate.comより

codeclimate.comの2014年の記事です。公式サイトには来年4月にマサチューセッツ州でカンファレンスもあるそうです。


つっつきボイス: 「ペアプログラミングの次はモブプログラミング」「モブプロって、書いている人は自分の意見を交えないルールだったかな」「集団二人羽織的な?」

参考: モブプログラミング – Woody Zuill氏とのインタビュー

Ruby trunkより

Kernel#ppが2.5で標準に

11月のウォッチでお伝えしたKernel#ppがその後本採用になっていました。

net/protocol、net/smtp、tempfile、tmpdirを誰もメンテしないなら自分がやる

_ko1さん激賞のnormalpersonさんです。


つっつきボイス:net/protocolって初めて見た」

参考: Ruby HTTPクライアントの比較表

Ruby

awesome-ruby.com: 定番gemまとめ情報


markets/awesome-rubyより

1ページに全部書いてあるところが便利そうです。
同じ名前のニュースサイトがあって少々紛らわしいですが。

Light Cable: Railsなしで使えるActionCable実装

# 同リポジトリより
Rack::Builder.new do
  map '/cable' do
    # You have to specify your app's connection class
    use LiteCable::Server::Middleware, connection_class: App::Connection
    run proc { |_| [200, { 'Content-Type' => 'text/plain' }, ['OK']] }
  end
end

つっつきボイス: 「こういうAC実装が出てくるのは理解できる: Rackだけ使いたいときとか」「例のEvil Martiansがスポンサーになってますね」

Rubyからsymbolをなくせるか(Ruby Weeklyより)

# 同記事より
{"foo" => 1}[:foo] == 1 # trueだったらいいのに
{foo: 1}["foo"]    == 1 # trueだったらいいのに

つっつきボイス: 「だからHashWithIndifferentAccessが欲しくなる」「記事で引用されてるこれほんに↓: SmalltalkだとSymbolはStringを継承しているのにRubyはそうじゃない」

Rubyコードを関数型プログラミングっぽく書いてみた

# 同記事より
module APIDataCommons
  extend self

  def band_names(data)
    user_data(data)
      .fetch('favorite_bands')
      .map { |b| b['name'] }
  end

  def name(data)
    user_data(data).fetch('name')
  end

  private
    def user_data(data)
      data.fetch("user")
    end
end

つっつきボイス: 「あまり関数型っぽいコードには見えないかな?: あえて言うならmethod chainingをふんだんに使ったPromise的なコード」「社内Haskell勢のツッコミが待たれる」

JavaScript: 5分でわかるPromiseの基礎(翻訳)

Rubyデザインパターンとサンプルコード総ざらえ

説明は抑えめで、図とRubyコード中心です。


bogdanvlviv.github.ioより

class Task
  attr_accessor :name, :parent

  def initialize(name)
    @name = name
    @parent = nil
  end

  def get_time_required
    0.0
  end
end

つっつきボイス: 「1ページに収まっているので辞書的に便利そう」

JSON仕様「RFC 8259」と「ECMA-404 2nd Editon」リリース、UTF-8必須に


つっつきボイス: 「今話題のやつ」「404だとNot Foundに見えてしまうと誰かツイートしてた」「RFC 8259の方もどことなく蟹さんマークのNICチップっぽい名前🦀


卜部さんがこの仕様を元にすごい勢いでJSONパーサー作ってました。

Rubyでリサジュー曲線


つっつきボイス: 「何だか懐かしい感」「リサジュー曲線を見たのがとても久しぶりだったので」

参考: リサジュー図形

ずっと「リサージュ」と思い込んでました(´・ω・`)

SQL

Postgres Weeklyも1月までお休みだそうです。

PostgreSQL 10の互換性のない変更点(Postgres Weeklyより)

記事の日付は10リリース前の「16 May 2017」ですが、リリースノートと見比べるときによさそうです。


つっつきボイス: 「これは見ておくべき情報!」「ありがたい🙏

PostgreSQLのインデックス(Postgres Weeklyより)


citusdata.comより


  • B-Tree
  • Generalized Inverted Index (GIN)
  • Generalized Inverted Seach Tree (GiST)
  • Space partitioned GiST (SP-GiST)
  • Block Range Indexes (BRIN)
  • Hash

つっつきボイス: 「これも大事っすね: B-Treeで間に合うことが多いけど、データの種類や性質に応じたインデックスを選ばないとインデックスろくに効かなくなったりする」

俺のPostgreSQLチートシート(Postgres Weeklyより)


つっつきボイス: 「チートシートというほど網羅的ではないかな」「『俺のチートシート』ですね」「ポスグレって\で始まるコマンド体系がとっつき悪くって」「sudo -u postgres createuser --interactiveみたいに、名前にpgが入っていないcreateuserとかがシステム系コマンドっぽく見えてしまうのも残念」
「正直、MySQLのコマンド体系の方が親切だった分、PostgreSQLの普及が遅れた気がします」「まあ慣れればぽすぐれの方がコマンド短いから入力速いし: 結局ググるけどな!」

JavaScript

GrimmerとReactを公平に比較してみた

<!--元記事より-->
<button onclick={{action setRandomAnimal}}>Set Random Animal</button>
<ul>
  {{#each randomAnimals key="@index" as |animal| }}
    <li>{{animal}}</li>
  {{/each}}
</ul>
// 元記事より
import Component, { tracked } from '@glimmer/component';

const animals = ["Cat", "Dog", "Rabbit"];

export default class extends Component {
  @tracked randomAnimals = [];

  setRandomAnimal() {
    const animal = animals[Math.floor(Math.random() * 3)];

    this.randomAnimals = this.randomAnimals.concat(animal);
  }
}

つっつきボイス: 「GrimmerだとVue.jsっぽく書けるのか: コンパクトなのはちょっとよさそう」

npmパッケージを自作する(JavaScript Liveより)

{
  "name": "masks-js",
  "version": "0.0.1",
  "description": "A NPM package that exports functions to mask values.",
  "main": "build/index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/brunokrebs/masks-js.git"
  },
  "keywords": [
    "npm",
    "node",
    "masks",
    "javascript"
  ],
  "author": "Bruno Krebs",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/brunokrebs/masks-js/issues"
  },
  "homepage": "https://github.com/brunokrebs/masks-js#readme"
}

つっつきボイス: 「npm作るコマンドとかあるのでgem作るのと同じぐらいの感覚: ↑こういうdescription書くのが面倒だけど」「gemspecもいろいろ書かないといけないですしね」

CSS/HTML/フロントエンド

HTTP Early HintsがRFC 8297に

Early Hintsの生みの親のkazuhoさん自らのツイートです。心なしかアバターが微笑んで見えます。

アクセシビリティのテストツール

aXepa11yGoogleChrome accessibility-developer-toolsなどを紹介しています。

子要素にフォーカスしたまま親要素を表示する

See the Pen :focus-within helpful a11y thing by Chris Coyier (@chriscoyier) on CodePen.

フロントエンドテクの紹介記事です。上のCodePenでマウスオーバーするとわかります。

Firefox Quantumが速くなった理由


hacks.mozilla.orgより


つっつきボイス: 「FirefoxというかMozilla組の追い上げ半端ないですね」「Mozilla Foundationそのものは緩く統括しているだけですけどね」

参考: 爆速進化したブラウザ「Firefox Quantum」は何がどう変化したのか?

その他

GeForce/TITANのデータセンター利用について

来年2月にChromeに広告ブロック機能を実装


リモートつっつきボイス: 「Googleが広告ブロック機能を提供しちゃうのか…」

CVEは誰でも出せる

番外

英米の名門校がずらり

体重も測れる超小型計測センサー

もしかしてエネルギー問題解決?

10平方ミクロン程度のグラフェン(原子一個分の薄さの黒鉛)から、腕時計を駆動できるほどの電力が得られる可能性があるそうです。

なお、グラフェンの製造が難しくて研究が遅れていたのが、あるとき黒鉛にセロテープを貼って引っぺがすだけで簡単に作れることがわかって一気に研究が進んだそうです。

参考: 驚異の素材グラフェンの「ゆらぎ」が、無尽蔵のクリーンエネルギーを生むかもしれない:米研究結果

これは凄い


つっつきボイス: 「おおぉ」「MIDIキーボードって実はこういうことするのにとても向いていますね」「物理コントローラが豊富にあるし」「MIDIもプロトコルとして成熟してるし」

藁で作った創作動物

https://layer13.tumblr.com/post/168746265269/ithelpstodream-in-northern-japan-the-wara-art


今週は以上です。

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

週刊Railsウォッチ(20171215)Ruby 2.5.0-rc1リリース、Ruby 2.4.3セキュリティ修正、Ruby 3.0で変わるキーワード引数、HTML 5.2 RECリリースほか

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

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

Rails公式ニュース

Ruby Weekly

Awesome Ruby

RubyFlow

160928_1638_XvIP4h

Postgres Weekly

postgres_weekly_banner

Frontend Weekly

frontendweekly_banner_captured

週刊Railsウォッチ(20180112)update_attributeが修正、ぼっち演算子`&.`は`Object#try`より高速、今年のRubyカンファレンス情報ほか

$
0
0

こんにちは、hachi8833です。インフルエンザA型が身に沁みました。

2018年最初のウォッチ、いってみましょう。年末年始を挟んでだいぶ記事がたまっているのでいつもより多めです。

Rails: 今週の改修

Ruby 2.5をCIに追加

まずは縁起物コミットから。

# travis.yml
   - 2.2.8
   - 2.3.5
   - 2.4.2
+  - 2.5.0
   - ruby-head

 matrix:
   include:
-    - rvm: 2.4.2
+    - rvm: 2.5.0

PostgreSQLでbulk_change_tableをサポート

MySQLでは以前からbulk: trueが使えるそうです。

# activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb#367
+        def bulk_change_table(table_name, operations)
+          sql_fragments = []
+          non_combinable_operations = []
+
+          operations.each do |command, args|
+            table, arguments = args.shift, args
+            method = :"#{command}_for_alter"
+
+            if respond_to?(method, true)
+              sqls, procs = Array(send(method, table, *arguments)).partition { |v| v.is_a?(String) }
+              sql_fragments << sqls
+              non_combinable_operations << procs if procs.present?
+            else
+              execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty?
+              non_combinable_operations.each(&:call)
+              sql_fragments = []
+              non_combinable_operations = []
+              send(command, table, *arguments)
+            end
+          end
+
+          execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty?
+          non_combinable_operations.each(&:call)
+        end

MiniMagickでcombine_optionsをサポート

# activestorage/app/models/active_storage/variation.rb#48
   def transform(image)
-    transformations.each do |method, argument|
-      image.mogrify do |command|
-        if eligible_argument?(argument)
-          command.public_send(method, argument)
-        else
-          command.public_send(method)
+    transformations.each do |(method, argument)|
+      if method.to_s == "combine_options"
+        image.combine_options do |combination|
+          argument.each do |(method, argument)|
+            pass_transform_argument(combination, method, argument)
+          end
         end
+      else
+        pass_transform_argument(image, method, argument)
       end
     end
   end

つっつきボイス: 「MiniMagickが好きと聞いて」「好きというほどではw: ImageMagickに比べればマシかなぐらい」

[Rails] MiniMagickでPDFのページ数を取得するときはフォントエラーに注意!

PostgreSQLのrange typeでFloat::INFINITYをサポート

rangeが空文字列にならないようFloat::INFINITYに型変換するようになりました。

# activerecord/test/cases/adapters/postgresql/range_test.rb#361
+    def test_infinity_values
+      PostgresqlRange.create!(int4_range: 1..Float::INFINITY,
+                              int8_range: -Float::INFINITY..0,
+                              float_range: -Float::INFINITY..Float::INFINITY)
+
+      record = PostgresqlRange.first
+
+      assert_equal(1...Float::INFINITY, record.int4_range)
+      assert_equal(-Float::INFINITY...1, record.int8_range)
+      assert_equal(-Float::INFINITY...Float::INFINITY, record.float_range)
+    end

つっつきボイス: 「PostgreSQLのrangeって無限が使えるのか」「無限こわい」

参考: PG10マニュアル: 8.17.4. Infinite (Unbounded) Ranges

()[]を使い分けるんですね(開区間と閉区間)。Lintに怒られそう。

(lower-bound,upper-bound)
(lower-bound,upper-bound]
[lower-bound,upper-bound)
[lower-bound,upper-bound]
empty

逆関連付けで外部キーが更新されていなかったのを修正

# activerecord/test/cases/associations/has_many_associations_test.rb#2512
+  test "reattach to new objects replaces inverse association and foreign key" do
+    bulb = Bulb.create!(car: Car.create!)
+    assert bulb.car_id
+    car = Car.new
+    car.bulbs << bulb
+    assert_equal car, bulb.car
+    assert_nil bulb.car_id
+  end

つっつきボイス: 「inverse association、この間案件に出てきたナ」「逆関連付け、でいいのかな」

validationコールバックが複数コンテキストで発火しなくなったのを修正

#21069で実装されていたのがいつの間にか動かなくなっていたので修正されたそうです。

class Dog
  include ActiveModel::Validations
  include ActiveModel::Validations::Callbacks

  attr_accessor :history

  def initialize
    @history = []
  end

  before_validation :set_before_validation_on_a, on: :a
  before_validation :set_before_validation_on_b, on: :b
  after_validation :set_after_validation_on_a, on: :a
  after_validation :set_after_validation_on_b, on: :b

  def set_before_validation_on_a; history << "before_validation on a"; end
  def set_before_validation_on_b; history << "before_validation on b"; end
  def set_after_validation_on_a;  history << "after_validation on a" ; end
  def set_after_validation_on_b;  history << "after_validation on b" ; end
end
d = Dog.new
d.valid?([:a, :b])
# 修正前
d.history #=> []
# 修正後
d.history #=> ["before_validation on a", "before_validation on b", "after_validation on a", "after_validation on b"]

つっつきボイス: 「やや、before/afterコールバックのon:オプションって初めて知ったけどこれは?」「on:はコンテキストを限定するのに使うやつですね: その条件が満たされるときだけコールバックされる」「なるほど~: if書きたくないマンにはうれしい機能」「条件が複雑になったらifで書かないと見落とすかもですね」

ActiveStorageで扱う添付ファイルの拡張子を追加

# activestorage/lib/active_storage/engine.rb
+    config.active_storage.content_types_to_serve_as_binary = [
+      "text/html",
+      "text/javascript",
+      "image/svg+xml",
+      "application/postscript",
+      "application/x-shockwave-flash",
+      "text/xml",
+      "application/xml",
+      "application/xhtml+xml"
+    ]

つっつきボイス: 「content dispositionって何でしたっけ」「ファイルをインラインで表示するかダウンロードダイアログを出すかの扱いっすね」「まさにコミットメッセージに書いてあった」

String.blank?のエンコーディングがUTF-16LEでエラーになるのを修正

ActiveSupportでStringクラスを開いて修正しています。

# activesupport/lib/active_support/core_ext/object/blank.rb#104
class String
   BLANK_RE = /\A[[:space:]]*\z/
+  ENCODED_BLANKS = Concurrent::Map.new do |h, enc|
+    h[enc] = Regexp.new(BLANK_RE.source.encode(enc), BLANK_RE.options | Regexp::FIXEDENCODING)
+  end

つっつきボイス: 「UTF-16ってASCII互換じゃないしエンディアンとかBOMとかサロゲートペアとかいろいろ残念で残念で: これを標準にしちゃったWindowsって(略」「出たな文字コード厨w」

参考: Wikipedia-ja: UTF-16

属性が見つからない場合の挙動を修正

# activerecord/lib/active_record/attribute.rb#234
+        def forgetting_assignment
+          dup
+        end

つっつきボイス: 「dirty save周りの修正っすね」

#25503のupdate_attributeの挙動がついに修正

昨年末のRailsウォッチで言及した#25503 update_attribute ignores autosave relationsが2年越しでついに修正されました。

# activerecord/lib/active_record/persistence.rb#405
-      if has_changes_to_save?
-        save(validate: false)
-      else
-        true
-      end
+      save(validate: false)
      end

つっつきボイス: 「例のGobyちゃんの作者のst0012さんが、このバグが直ってないって昨年落ち込んでました」「おお!これが修正されたということは、例のQiitaの定番記事『ActiveRecord の attribute 更新方法まとめ』のupdate_attributeの記述↓も修正してもらわないと」


ActiveRecord の attribute 更新方法まとめより


Goby: Rubyライクな言語(1)Gobyを動かしてみる

Rails

Rails.application.routes.url_helpersを直接呼ぶと遅い

# 直接呼んだ場合
Requests per second:    55.08 [#/sec] (mean)
Time per request:       18.155 [ms] (mean)
# モジュールをクラスにincludeした場合
Requests per second:    117.09 [#/sec] (mean)
Time per request:       8.540 [ms] (mean)

issue #23451 Performance Regression using url_routerとそれを修正するPR#24554 Memoize the RouteSet#url_helpers moduleが前から上がっていますがまだmergeされていません。それまではinclude Rails.application.routes.url_helpersする方が速いそうです。

# app/whatever/url_helper.rb
class UrlHelper
  include Singleton
  include Rails.application.routes.url_helpers
end

Rails 5.2でMySQLの降順インデックスをサポート(RubyFlowより)

# 同記事より
create_table "reports", force: :cascade do |t|
  t.string "name"
  t.integer "user_id"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
  t.index ["user_id", "name"], name: "index_reports_on_user_id_and_name", order: { name: :desc }
end

Railsのsystem testをRSpecから使う

# 同記事より
require 'rails_helper'

describe 'Homepage' do
  before do
    driven_by :selenium_chrome_headless
  end

  it 'shows greeting' do
    visit root_url
    expect(page).to have_content 'Hello World'
  end
end

つっつきボイス: 「この間takanekoさんに教えてもらった@jnchitoさんの記事↓の方がたいてい詳しいんですが、一応」

whatson: 今後開催予定のRubyカンファレンスを一覧表示するgem

もともとRuby Conferences (& Camps) in 2018 – What’s Upcoming?をご紹介しようと思っていたのですが、リンク先がアドベントカレンダーのせいか今日になって消滅していて、その情報源であるこのgemを見つけました。
私はうれしいですがほぼ誰得なgemですね。

$ gem install whatson
$ rubyconf
Upcoming Ruby Conferences:

  in 14d  Ruby on Ice Conference, Fri-Sun Jan/26-28 (3d) @ Tegernsee, Bavaria (near Munich / München) › Germany / Deutschland (de) › Central Europe › Europe
  in 20d  RubyFuza, Thu-Sat Feb/1-3 (3d) @ Cape Town › South Africa (za) › Africa
  in 28d  RubyConf India, Fri+Sat Feb/9+10 (2d) @ Bengaluru › India (in) › Asia
  in 55d  RubyConf Australia, Thu+Fri Mar/8+9 (2d) @ Sydney › Australia (au) › Pacific / Oceania
  in 62d  RubyConf Philippines, Thu-Sat Mar/15-17 (3d) @ Manila › Philippines / Pilipinas (ph) › Asia
  in 63d  wroc_love.rb, Fri-Sun Mar/16-18 (3d) @ Wrocław › Poland (pl) › Central Europe › Europe
  in 69d  Bath Ruby Conference, Thu+Fri Mar/22+23 (2d) @ Bath, Somerset › England (en) › Western Europe › Europe
  in 95d  RailsConf, Tue-Thu Apr/17-19 (3d) @ Pittsburgh, Pennsylvania › United States (us) › North America › America
  in 105d  RubyConf Taiwan, Fri+Sat Apr/27+28 (2d) @ Taipei › Taiwan (tw) › Asia
  in 111d  Rubyhack: High Altitude Coding Konference, Thu+Fri May/3+4 (2d) @ Salt Lake City, Utah › Southwest › United States (us) › North America › America
  in 133d  Balkan Ruby Conference, Fri+Sat May/25+26 (2d) @ Sofia › Bulgaria (bg) › Eastern Europe › Europe
  in 139d  RubyKaigi, Thu-Sat May/31-Jun/2 (3d) @ Sendai › Japan (jp) › Asia
  in 161d  RubyConf Kenya, Fri Jun/22 (1d) @ Nairobi › Kenya (ke) › Africa
  in 167d  Paris.rb XXL Conf, Thu+Fri Jun/28+29 (2d) @ Paris › France (fr) › Western Europe › Europe
  in 175d  Brighton Ruby Conference, Fri Jul/6 (1d) @ Brighton, East Sussex › England (en) › Western Europe › Europe
  in 305d  RubyConf, Tue-Thu Nov/13-15 (3d) @ Los Angeles, California › United States (us) › North America › America

    More @ github.com/planetruby/awesome-events

ついでに、元サイトがhttp://planetruby.herokuapp.com/というRuby情報クローラ的なサイトになっていました。

今年のリストでは、アフリカ大陸(南アフリカ共和国とケニア)でRubyカンファレンスが開催されるのが目につきました。ケニアのはその名もrubyconf.nairuby.orgです。ナイルビー。


http://rubyconf.nairuby.org/2018より

Rails公式ニュースにも掲載されている情報ですが、今年4月開催のピッツバーグのRailsカンファレンスCFPを募集だそうです(CFP: Call for Proposal)。


つっつきボイス:Bath Ruby Conferenceって何だ?っと思ったら、Bathはイギリスの地名なんだそうです」「水上温泉みたいな?」

Rails 5.2のdeprecation情報

ほとんど走り書きなので、5.2.0リリースまでに別途まとめようと思います。

「巨大なプルリク1件と細かいプルリク100件どっちがまし?」を考える(Hacklinesより)

今回たまたま見つけたHacklinesというRuby情報クローラが面白かったのでそこからいくつか記事を拾いました。


つっつきボイス: 「元記事にも貼られているこれほんに↓」「巨大なのがhorse-sizedで、こまいのがduck-sizedということみたいです」「コミットの粒度ってほんと悩みますね」「読まされるレビュアーの立場で考えるしかないかなー」

アセットのプリコンパイルを高速化するには(Hacklinesより)

「CDNを使う」「@importrequire_tree .を避ける」などの地道な方法が紹介されています。

マイグレーションを実行せずにSQLクエリを見たい(Hacklinesより)

同記事で、#31630 Allow to run migrations in check mode (dry run)というつい最近のPRが紹介されています。まだmergeされていません。


つっつきボイス: 「マイグレーションのdry run、たまに欲しくなりますよね」

ぼっち演算子&.の方がObject#tryよりずっと高速(Hacklinesより)

ベンチマークコードと結果はGistにあります。

#同Gistより
       user     system      total        real
      check for nil:  0.040000   0.000000   0.040000 (  0.040230)
   check respond_to:  0.100000   0.000000   0.100000 (  0.101780)
             rescue:  2.080000   0.020000   2.100000 (  2.103482)
 active_support try:  0.150000   0.000000   0.150000 (  0.151765)
    safe navigation:  0.040000   0.000000   0.040000 (  0.040369)

つっつきボイス: 「safe navigation operatorってぼっち演算子のことなのね」「後者はRubyでの俗称というかあだ名っぽいですね」
「ところでぼっち演算子って.&&.のどっちでしたっけw」「わかるーw: ワイもよく迷う」
「そういえばtry!ってどう違うんだったかな」「この記事↓翻訳したときにbabaさんに教えてもらったのを末尾に追加してあります: 『ぼっち演算子が#try!と少し異なるのは、引数付きだとnilのときに引数が評価されないという点です。』」「引数があるかどうかで違う、と」

Railsの`Object#try`がダメな理由と効果的な代替手段(翻訳)

追伸: 体操座りしながら指でいじいじしている形で覚えるとよいそうです。

belongs_to関連付けクエリのリファクタリング(RubyFlowより)

「ここではスコープよりクラスメソッドの方が自分にはわかりやすかったから」だそうです。

# 同記事より
class Job < ActiveRecord::Base
  belongs_to :category

  def self.publishable
    joins(:category).merge(Category.publishable)
  end
end

Job.publishable

他にEncapsulating queries in a Rails Modelという記事もありました。


つっつきボイス: 「スコープかクラスメソッドか」「scopeは最後のリファクタでそれっぽければやればいい気がする: デフォルトはclassメソッドでいーんじゃないかな?」

マイクロサービスはチームを苦しめる(Hacklinesより)


つっつきボイス: 「記事にあったコンウェイの法則ってこれですね↓」「うんうん、官僚組織のシステムはやっぱり官僚っぽい設計になるし」

Conwayの法則とは,“組織の設計するシステムには … その組織のコミュニケーション構造をそのまま反映した設計になるという制約がある”,というものだ。つまり,チームの開発成果がその組織の内部的なコミュニケーションのあり方によって決まる,という意味である。
Conwayの法則に従った組織の成長より

参考: クックパッドとマイクロサービス — Conwayの法則に言及しています

「ところでコンウェイっていうとライフゲームの英語名Conway’s Game of Lifeを思い出しちゃいます(年バレ!)」

flag_shih_tzu: Integerカラムにビットパターンでフラグを追加するgem(Hacklinesより)

# 同記事より
class Spaceship < ActiveRecord::Base
  include FlagShihTzu

  has_flags 1 => :warpdrive,
            2 => :shields,
            3 => :electrolytes
end

shih tzuって何だろうと思ったら、中国産の犬種「西施犬」のようです。フラグとどう関連するのかは謎です。

ついでに、元記事タイトルは「博士の異常な愛情 または私は如何にして心配するのを止めて水爆を愛するようになったか」のもじりですね。

Ruby trunkより

早くもRubyに大量のコミット

PB memoさんのRubyコミット日記です。年明け早々に追いきれないほどのコミット大漁節です。


つっつきボイス: 「みんな冬休み取ったー?w」

そういえば以下の記事で、今後Rubyのリリース日が前倒しになるかもしれないという構想が語られていました。

また、クリスマスリリースはプレゼントという意味があるものの、家族を持つコミッターが増えてきたため、「少し前の22日や23日にしたほうがよいかもしれない」と語った。

Integer#powの法(modulo)が巨大な場合の結果がおかしい->修正

以下は12が正しいそうです。

irb(main):020:0> 12.pow(1, 10000000000)
=> 1
irb(main):021:0> 12.pow(1, 10000000001)
=> 1
irb(main):022:0> 12.pow(1, 10000000002)
=> 1

beginなしでdo-endブロックでrescue

1年前の変更なので2.5には反映済みです。

lambda do
  begin  #<= これがなくてもいいようになった
    raise 'err'
  rescue
    $! # => #<RuntimeError: err>
  end
end.call

つっつきボイス: 「自分もこのbeginなくていいと思う」「matzがためらいがちにacceptしてました↓」

Although I am not a big fan of this syntax, mostly because I don’t like fine grain exception handling.
But I found out many developers prefer the syntax. After some consideration, I decided to accept this.

SymbolとStringの違いに関するRDocを追加

/* 定数名、メソッド名、変数名はシンボルとして返される
*
*     module One
*       Two = 2
*       def three; 3 end
*       @four = 4
*       @@five = 5
*       $six = 6
*     end
*     seven = 7
*
*     One.constants
*     # => [:Two]
*     One.instance_methods(true)
*     # => [:three]
*     One.instance_variables
*     # => [:@four]
*     One.class_variables
*     # => [:@@five]
*     global_variables.last
*     # => :$six
*     local_variables
*     # => [:seven]
*
* Symbolオブジェクトは識別子を表す点がStringオブジェクトと異なる
* Stringオブジェクトはテキストやデータを表す
*/

つっつきボイス: 「この間この記事↓を公開した後の変更なので取り上げてみました」


Rubyのシンボルをなくせるか考えてみた(翻訳)

Ruby

Fukuoka Ruby Awardエントリー募集(1/31まで)(Ruby公式ニュースより)


www.ruby-lang.orgより

Ruby 3とJIT(Ruby Weeklyより)

Noah Gibbsさんの記事です。Optcarrotがoptimized modeで相当速くなっています。


engineering.appfolio.comより

Ruby 2.5のベンチマーク取ってみた

HexaPDFを使っています。


gettalong.orgより

Kernel#itselfにRubyの美学を見た

短い記事です。

# 同記事より
collection.each_with_object({}) { |item, accum| accum[item] = accum[item].to_i + 1 }
# ↓ここまで簡潔に書ける
collection.group_by(&:itself).transform_values(&:count)

RubyにCコード書いてメモリ共有してみた

# 同記事より
require 'inline'
class CHello
  inline do |builder|
    builder.include '<stdio.h>'
    builder.c 'int sumThem() {
      return 2 + 2;
    }'
  end
end

>> CHello.new.sumThem #=> 4

つっつきボイス: 「RubyコードにまるっとCのコードがインラインで埋まっているんですよね」「これマジ凄くない?C拡張より楽チンそう」「rubyinlineでできるみたいです」「メモリ共有にはFiddle::Pointerを使ってるそうです」

「この記事にはネタ画像がいくつか埋まってるんですが、その中でもこれ↓: シャイニングっていう昔のくっそ怖い映画の一番有名なシーンなんですが」「なんか見たことあるっちゃある感じ」


blog.rebased.plより

「この『Here’s Johnny!!』っていうセリフは、実はこの場面までの緊張感を一発で台無しにする、英語圏のこの年代の人じゃないとわからないずっこけネタなんですね」「Tonight Showという米国の長寿テレビ番組のオープニングで司会者が必ず言うセリフなんですが、日本に置き換えるとさしずめ『サザエでございま~す』とか『ぼーくドラえもん』っていう感じ: そこでそれ言うか!みたいな」

JRubyより速い


つっつきボイス: 「今見てみると2.5ががくっと遅くなってますね」「何かつっかえてるのかな?」

卜部さんの「HashDoS脆弱性との戦い」

Ruby実装の命名の由来

これもNoah Gibbsさんの記事です。

RubyBench: RubyやRailsのベンチマークサイト


rubybench.orgより


つっつきボイス:https://speed.python.org/みたいなのがRubyにもないかなと思って探したら見つかりました: 相当細かくベンチ取ってくれて楽しい」


rubybench.orgより

Ruby開発者のための5つの習慣(Ruby Weeklyより)


  1. RuboCopはいつどんなときでもかけろ
  2. git historyを汚すな
  3. お遊びプロジェクトを立ち上げてみろ
  4. Railsのソースコードを読め
  5. Railsガイドを「もう一度」読め

つっつきボイス: 「5…」「5…」

unlessのご利用は控えめに(Hacklinesより)

# 元記事より
# Example 1
unless something?
  # do something
else
  # do other thing
end

# Example 2
unless something? || another_thing?
  # do something
end

つっつきボイス:unless自体はいいけど確かにelseと一緒に使うとか勘弁w」

RubyとPythonのmixinを比較する(Hacklinesより)

みっちり長い記事です。

# 同記事より
class RunnerMixin:
    def max_speed(self):
        return 4


class SortaFastHero(RunnerMixin):
    """This hero can run, which is better than walking."""
    pass


class SortaFastMonster(RunnerMixin):
    """This monster can run, so watch out!"""
    pass

つっつきボイス:endがないと、どうもパンツはき忘れたような気持ちになってw」「Pythonコードってブラウザからコピペしたはずみでインデント消えちゃったり」「それはコピペするなということかも」

地味すぎて誰も気がついていないCRuby 2.5の新機能

mruby/c1.1 RC2リリース

  • Procクラスの実装
  • sprintfメソッドの実装
  • .classメソッドの実装
  • RangeObjectのリファレンスカウント対応
  • StringObjectのバグ修正
  • 重複した数値処理の排除
  • Rubyによるクラスの定義とインスタンスメソッドの定義を実装

各種言語のハッシュマップ実装を比較

  • Python
  • Ruby
  • Java
  • Scala
  • Golang
  • C#
  • C++

この記事のサイドバーにあったNo Magic: Regular Expressionsという記事もつい気になってしまいました。

Graphql-batchとPromise.rb

Graphql-batchはShopifyのリポジトリですね。内部でPromise.rbを使っているそうです。


つっつきボイス: 「↓こんな感じでRubyでPromiseできるみたいです」

# lgierth/promise.rbより
require 'promise'

Promise.new
  .tap(&:fulfill)
  .then { Promise.new.tap(&:fulfill) }
  .then { Promise.new.tap(&:reject) }
  .then(nil, proc { |reason| p reason })

JavaScript: 5分でわかるPromiseの基礎(翻訳)

asakusa.rb新年会

現時点でまだ空席あるようです。

KitchenCI: 複数プラットフォームをサポートするCIサービス


kitchen.ciより

driver:
  name: vagrant

provisioner:
  name: chef_zero

platforms:
  - name: ubuntu-14.04
  - name: windows-2012r2

suites:
  - name: client
    run_list:
      - recipe[postgresql::client]
  - name: server
    run_list:
      - recipe[postgresql::server]

Chefが前提のようです。

SQL

DB設計カタログサイト


つっつきボイス: 「確かにこれ凄い!」「医療とかホテルとか、よくここまで集めた」「実用に即したDB設計ってなかなか見る機会ないですよね」

PostgreSQLが「DBMS of the year 2017」に輝く(Postgres Weeklyより)

https://db-engines.com/en/rankingというランキングサイトを元にしています。


db-engines.comより


つっつきボイス: 「MongoDBとかってDBMSなんですかね?」

JavaScript

面接で聞かれるES6理論クイズ10問(解答付き)

500人以上の技術面接で使われた問題だそうです。

  1. JavaScriptのスコープを説明し、スコープの例を知っている限り列挙せよ(6点)
  2. ホイスティングを例を挙げて説明せよ(6点)
  3. prototypeの役割を例を挙げて説明せよ(6点)
  4. 3の例を拡張してprototypeの継承を説明せよ(5点)
  5. 3の例をES6構文で書き直せ(6点)
  6. thisの値を説明せよ(6点)
  7. コンテキストバインディングを例を挙げて説明せよ(3点)
  8. =====の一般的な違いを説明せよ(6点)
  9. 変数がarrayかどうかをチェックする方法を述べよ(3点)
  10. 以下のコードのどこがおかしいかを説明し、修正せよ(4点)
if ( typeof x === 'object' ) {
    x.visited = true;
}

rearmed-js: JavaScriptのArrayなどをRuby風に書けるライブラリ

// westonganger/rearmed-jsより
var array = [];
var cb = function(val, i){ };
array.any(cb=null) // returns bool
array.all(cb=null) // returns bool
array.compact(badValues=[null, undefined, '']) // returns array, accepts array or splat arguments
array.dig(*args) // returns value, accepts splat arguments or array
array.each(function(val, i){ })
...

Sinon.js: JavaScriptでmockやstubを使うライブラリ

テスティングフレームワークに依存しないそうです。

// sinonjs.orgより
it("returns the return value from the original function", function () {
    var callback = sinon.stub().returns(42);
    var proxy = once(callback);

    assert.equals(proxy(), 42);
});

NectarJS: JSコードをネイティブバイナリにコンパイル(JavaScript Liveより)

WebAssemblyにも対応しているそうです。

via GIPHY

JavaScriptオブジェクトのrest/spreadプロパティ(JavaScript Liveより)

// 同記事より
const style = {
  width: 300,
  marginLeft: 10,
  marginRight: 30
};

const { width, ...margin } = style;

console.log(width);  // => 300
console.log(margin); // => { marginLeft: 10, marginRight: 30 }

JavaScriptのhoistingを理解する(JavaScript Liveより)


medium.com/@thamizhchelvan2005より

JavaScriptの「obfuscation」とは何か(JavaScript Liveより)

obfuscationは、いわゆるminifyやuglifyより徹底的に変換をかけています。

// 同記事より
function hello(name) {
console.log('Hello, ' + name);
}
hello('New user');

// obfuscation後
eval(function(p,a,c,k,e,d){e=function(c){return c};if(!''.replace(/^/,String)){while(c--){d=k||c}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k)}}return p}('3 0(1){2.4(\'5, \'+1)}0(\'7 6\');',8,8,'hello|name|console|function|log|Hello|user|New'.split('|'),0,{}))

tui.editor: 表やグラフも扱えるMarkdownエディタ(JavaScript Liveより)

表やUML図などを直接扱えるようです。


nhnent/tui.editorより

CSS/HTML/フロントエンド

HTML 5.2の新着情報とポイント(jser.infoより)


  • <dialog>要素
  • Apple製品でのアイコン表示改善
  • <main>要素を複数持てる
  • <body>タグ内にも<style>を書ける(ただしパフォーマンス上おすすめしない)
  • <legend>タグ内に見出しタグを置ける
  • 廃止: keygenmenumenuitem、厳密なDOCTYPE
  • etc

CSSの:notセレクタを導入


つっつきボイス::notときどき使いますヨ: 繰り返し要素の最後のところだけ区切り線入れたくないときとか便利」「そうそう、これないと不便」

その他

技術トークの5つのコツ


reverentgeek.comより

  • その技術を選んだ理由を話す
  • その技術で何ができるかを話す
  • どうやったら動いたかをデモする(しくじったポイントも入れよう)
  • 参考リンクを忘れずに
  • マイクはないものと思え

meltdown: メルトダウン脆弱性の実演コード(GitHub Trendingより)

今旬のネタだけあって、10日ほどで★2400超えです。

これマジで?


つっつきボイス: 「ダチョウ倶楽部」

Go 1.10 Beta2リリース

番外

闇深そうなフォントかるた

ケンブリッジ大学の脳力測定サイト

いわゆる脳トレ的なやつです。

成功の秘訣は「大学の町の近くで育つこと」?

日本語記事: 元グーグルのデータサイエンティストが発見! 成功者の意外な共通点とは

340刷

ロシアのサーバールームお祓い事情


つっつきボイス: 「サーバールームで水撒くか普通…」

AIで転職情報を勝手にかき集めるのは…


つっつきボイス: 「この人に目をつけられたらもう逃げられないっすね」

闇落ち以外のパターンが思いつかない


今週は以上です。

バックナンバー(2017年後半)

週刊Railsウォッチ(20171222)定番gemまとめサイト、active_record-mtiでテーブル継承、PostgreSQL 10の非互換変更点、Railsガイド攻略法ほか

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

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

Ruby 公式ニュース

Rails公式ニュース

Ruby Weekly

Awesome Ruby

RubyFlow

160928_1638_XvIP4h

Postgres Weekly

postgres_weekly_banner

Frontend Weekly

frontendweekly_banner_captured

Github Trending

160928_1701_Q9dJIU


新しいRailsフロントエンド開発(3)Webpackの詳細、ActionCableの実装とHerokuへのデプロイ(翻訳)

$
0
0

概要

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

新しいRailsフロントエンド開発(3)Webpackの詳細、ActionCableの実装とHerokuへのデプロイ(翻訳)

前書き

本記事は、フロントエンドのフレームワークに依存しないRailsプレゼンテーションロジックを現代的かつモジュール単位かつコンポーネントベースで扱う方法を独断に基いて解説するガイドです。3部構成のチュートリアルで、例を元に最新のフロントエンド技術の最小限に学習し、Railsフロントエンド周りをすべて理解しましょう。

Part 2までのおさらい

こちらもお読みください:

Part 2までに、「コンポーネント」アプローチを用いてチャットアプリの骨格を組み立てました。各コンポーネントは、アプリのfrontend部分の内部のフォルダとして表現されており、それぞれが.erbパーシャル、.cssスタイルシート、.jsスクリプトの3つのファイルで構成されています。現時点でJavaScriptコードに含まれているのは、ネストしたコンポーネントを読み込むためのimport文だけです。これによってすべてのパーツがapplication.jsのエントリポイントとして含まれるようになり、Webpacker gemでこれらをまとめてCSSやJSのバンドルをビルドできるようになっています。

今回のチュートリアルの最後の章では、JavaScriptを用いてチャットが動くようにする予定です。公式のRailsドキュメントは未だにSprocketsやCoffeeScriptが前提になっているため、ActionCableをES6モジュールから用いる方法についても解説します。

「sprockets抜き」アプリが完成したら、Herokuにデプロイします。

完成版のEvil Chatアプリのコードをすぐにもご覧になりたい場合はGitHubのリポジトリをどうぞ。

ご存知かと思いますが、ActionCableの理解はそれほど簡単ではありませんので、できるだけ手順ごとに動作を明示的に解説してみます。経験豊富な開発者の知性を過小評価する意図はありませんのでご了承ください。途中でActionCableを十分理解できた方は、解説をスキップしてコードスニペットまで進めてください。コードスニペットは通常のSprockets実装と異なっているため、Railsガイド(訳注: 英語版Edgeガイドです)のコード例はWebpackで動作しません。

ActionCableのRuby部分

まずは、チャットのチャンネルの生成が必要です。

$ rails g channel chat

これでapp/channels/の内部にchat_channel.rbというファイルが作成されます。

ActionCableはRailsでWebSocketsと統合されており、サーバー側のロジックをRubyで書き、クライアント側のロジックをJavaScriptで書くことができます。ActionCableのクールな点は、ブラウザ上で実行されるJavaScriptから、サーバー側のRubyメソッドを呼び出せることです。chat_channel.rbはチャット用のメソッドを定義する場所であり、全登録ユーザーのデータのストリーミング(本チュートリアルの場合、新しいメッセージでDOMを更新する少量のHTMLです)も担当します。

チャンネル固有の機能を扱う前に、ActionCableが認証済みユーザーのみをブロードキャストすることを担保する必要があります。アプリ作成時に生成したapp/channels/application_cableフォルダの内部を見ると、WebSockets認証を担当するconnection.rbファイルがあります。Part 2の認証が非常にシンプルだったことを思い出しましょう。sessionハッシュ内に単にusernameキーを作成し、ユーザーがどんなusernameでも使えるようになっていました。以下は今回必要なコードです。

# app/channels/application_cable/connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = request.session.fetch("username", nil)
      reject_unauthorized_connection unless current_user
    end
  end
end

ここではセッションからusernameを取り出そうとしています。usernameがない場合、接続を拒否します。実際には、新しいユーザーは「log in」画面を経由するまでActionCableのブロードキャストを受け取りません。

続いてchat_channel.rbに手を加えます。

# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from "chat"
  end

  # サーバーがメッセージ形式のコンテンツを受け取ると呼び出される
  def send_message(payload)
    message = Message.new(author: current_user, text: payload["message"])
    if message.save
      ActionCable.server.broadcast "chat", message: render(message)
    end
  end

  private

  def render(message)
    ApplicationController.new.helpers.c("message", message: message)
  end
end

subscribedメソッドは接続が認証されると呼び出されます。stream_fromは、「chat」チャンネルでブロードキャストされるどんなメッセージでもクライアントに到達できるということを表します。

このsend_messageメソッドは最も興味深い部分です。今はアプリのRuby部分の内部なので、ActiveRecordと直接やり取りできます。私たちのシンプルな例では、「メッセージを1件送信する」というのは、Messageモデルの新しいインスタンスを1つ作成してデータベースに保存し、authortextが正しく設定されたmessageパーシャルをレンダリングして、生成されたHTMLを「chat」チャンネルでブロードキャストするということを意味します。

ここでご注意いただきたいのは、app/channelsの内部からはApplicationControllerrenderメソッドにも、コンポーネントをレンダリングするカスタムcヘルパーにも直接アクセスできないという点です。そこで、ヘルパーを間接的に呼び出す別のrender定義を作成します。そのために、ApplicationControllerのインスタンスを1つ作成して、ApplicationHelperモジュールで定義したヘルパーにアクセスします。今私たちが関心を抱いているのはcヘルパーなので、ApplicationController.new.helpers.cでアクセスします。

ActionCableのJavaScript部分

純粋なrails newで生成したRails 5.1アプリでは、ActionCableのクライアント部分(JavaScriptで記述されている)はアセットパイプラインでインクルードされます。私たちがapp/assetsを削除したときに、この標準的な実装も効率よく取り除かれていますので、ActionCableのJavaScriptライブラリを再度インストールする必要があります。今度はYarn経由でnpmからインストールします。

$ yarn add actioncable

さて、WebpackでActionCable(あるいは別のJavaScriptライブラリ)を用いる場合の特別な点とは何でしょうか?

Sprocketsを使うと、JavaScriptファイルが結合後に共通のスコープで共有されたものを扱うことになるため、this.jsで宣言されたものは何であってもthis.jsが事前に読み込まれていればその後のthat.jsからアクセスできます。Webpackはこの点が違っており、より抑制の効いたアプローチを採用しています。Ross Kaffenbergerの良記事から引用します。

これは、ブラウザでのJavaScriptバンドル方法のパラダイムがSprocketsとWebpackで根底から異なっていることを理解するうえで役立ちます。
この違いは、Webpackの動作の中核部分にあります。Webpackでは、SprocketsのようにJavaScriptコードをグローバルスコープで結合するのではなく、個別のJavaScriptモジュールをクロージャ経由で個別のスコープに仕切っているので、モジュール間のアクセスをimport経由で宣言することが必須になります。これらのJavaScriptモジュールは、デフォルトでは一切グローバルスコープには公開されません。

私たちはES6のexport文やimport文を多用しなければならなくなります。しかし私たちは、最初にfrontend内にclientフォルダを作成しています。ActionCableの(JavaScript)クライアントはここに置きます。

$ mkdir frontend/client
$ touch frontend/client/cable.js

cable.jsは、「cable」コネクションのconsumerインスタンスの作成に使われます。Sprocketsで書かれた標準的なRailsサンプルでは、これはグローバルなAppオブジェクトの一部として作成されるのが普通です。公式のActionCableドキュメントや世にあまたあるチュートリアルでは次のようなコードが使われています。

// これはコピペしてはいけません!
(function() {
  this.App || (this.App = {});

  App.cable = ActionCable.createConsumer();
}).call(this);

このコード例を、私たちのモジュールベースのシステムに合わせて調整する必要があります。また、consumerが作成済みの場合には既存のコネクションを再利用してcreateConsumer関数の再呼び出しを避けたいと思います。そのためにグローバルなwindow変数を使いたくないので、別のアプローチを採用します。私たちのcable.jsモジュールは、コネクションのインスタンスをconsumer内部変数に保持し、createChannel関数をexportします。この関数は既存のconsumerをchatチャネルにサブスクライブするか、新しいconsumerインスタンスを1つ作成します。それではコードをcable.jsに書いてみましょう。

// frontend/client/cable.js
import cable from "actioncable";

let consumer;

function createChannel(...args) {
  if (!consumer) {
    consumer = cable.createConsumer();
  }

  return consumer.subscriptions.create(...args);
}

export default createChannel;

createChannel関数は汎用なので、consumerを特定のチャンネルにサブスクライブしたいどんな箇所からでも正しい引数を与えて使うことができます。したがって、サーバー側のchat_channel.rbのRubyコードに対応するクライアント側JavaScriptコードとなるファイルが別途必要になります。このファイルをchat.jsと呼ぶことにしましょう。

$ touch frontend/client/chat.js

コードは次のとおりです。

// frontend/client/chat.js
import createChannel from "client/cable";

let callback; // 後で関数を保持するための変数を宣言

const chat = createChannel("ChatChannel", {
  received({ message }) {
    if (callback) callback.call(null, message);
  }
});

// メッセージを1件送信する: `perform`メソッドは、対応するRubyメソッド(chat_channel.rbで定義)を呼び出す
// ここがJavaScriptとRubyをつなぐ架け橋です!
function sendMessage(message) {
  chat.perform("send_message", { message });
}

// メッセージを1件受け取る: ChatChannelで何かを受信すると
// このコールバックが呼び出される
function setCallback(fn) {
  callback = fn;
}

export { sendMessage, setCallback };

この部分は難解なので、説明のテンポを落としてじっくり見てみましょう。
細かな動作は次のようになっています。

  • cable.jsからcreateChannel関数をimportします。
  • この関数に2つの引数を与えて呼び出します。チャンネルの名前(Rubyのsome_channelのような名前はJavaScriptではSomeChannelとし、両者の命名慣習を壊さないようにしなければならない点に注意)と、ActionCableの標準コールバック(connecteddisconnectedreceived)を定義するオブジェクトです。ここで必要なのはreceivedコールバックのみです。このコールバックは、ブロードキャストされたデータをJavaScriptオブジェクトの形式として引数として持つチャンネルブロードキャストをconsumerが受け取ると呼び出されます(RubyとJavaScriptオブジェクトの変換はRails自身が行います)。
  • ここから少々ややこしくなります。messageオブジェクトを受信したら、何らかの関数を呼び出す必要があります。コンポーネントのこの部分は、必要に応じてDOMを扱う方法を責務上知っていなければならないので、この関数をここで定義したくありません。そこで、setCallbackという汎用的な関数を1つ作成します。この関数は、正しいコンポーネントから呼び出されると、メッセージ受信後に呼び出したいコンポーネント固有のあらゆる関数を保存するcallback変数を変更します。
  • sendMessageは、コネクションインスタンスのperformメソッドを呼び出します。ここはActionCableの最も魔術的な部分であり、JavaScriptからRubyのメソッドを呼び出します。これはchat_channel.rbからsend_messageメソッドをトリガして、messageオブジェクトを引数として渡します。この{ message }という記法は、ES6の{ message: message }のショートハンドです。ここではペイロードがmessageキーの下にあることを前提としています。このコンテキストにおける「message」は、メッセージフォームに含まれるユーザー(visitor)の種類を表す単なるテキストです。
  • 最後に、モジュールからsendMessagesetCallbackを両方ともexportし、後でコンポーネントで使えるようにします。

明確なメッセージを1件送信する

それでは最初にメッセージの送信を扱いましょう。この責務を引き受けるべきコンポーネントはどれでしょうか?Part 2では、個別のメッセージ用にmessageコンポーネントを、メッセージのリスト用にmessagesコンポーネントを、テキストの送信にはmessage-formを使いました。ブルーの大きな「Send」ボタンはmessage-formの内部にあるので、ここに置くのが正解です。frontend/components/message-form/message-form.jsのコードを変更しましょう。

// frontend/components/message-form/message-form.js

// client/chat.jsからsendMessageをimportする必要がある
import { sendMessage } from "client/chat";
import "./message-form.css";

const form = document.querySelector(".js-message-form");
const input = form.querySelector(".js-message-form--input");
const submit = form.querySelector(".js-message-form--submit");

function submitForm() {
  // sendMessageを呼び出し、その結果Rubyのsend_messageメソッドが呼ばれて
// ActiveRecordでMessageインスタンスが作成される
  sendMessage(input.value);
  input.value = "";
  input.focus();
}

// コマンドキー(またはCtrlキー)+Enterでメッセージを送信できる
input.addEventListener("keydown", event => {
  if (event.keyCode === 13 && event.metaKey) {
    event.preventDefault();
    submitForm();
  }
});

// ボタンをクリックして送信してもよい
submit.addEventListener("click", event => {
  event.preventDefault();
  submitForm();
});

動作を確認しましょう。もう一度サーバーを起動して認証し、メッセージボックスに適当なテキストを入力してコマンド+Enterキーを押し、Railsログを調べると次のように表示されます。

chat_channelの最初のブロードキャスト

chat_channelの最初のブロードキャスト

これで、フォームを送信すると、バックエンドでMessageインスタンスが新たに1つ作成され、メッセージのパーシャルが生成されてActionCableですべての登録ユーザーにブロードキャストされます。残るは、HTMLで受け取った文字列をDOMに挿入してページに表示するだけです。

受信したメッセージ

新しいメッセージをその都度動的にページに挿入する責務を負うのはmessagesコンポーネントです。元々このコンポーネントはデータベース内のすべてのメッセージをレンダリングする責務を負っていることがその理由です。

ここで行う必要があるのは、chat.jsモジュールのsetCallback関数を呼び出して、ブロードキャストされたメッセージを引数として受け取る別の関数に渡すことだけです。もう一度おさらいしましょう。chat.jsモジュールは、chatチャンネルで何かがブロードキャストされると、常にreceivedイベントに対して何か操作を行える状態になりますが、正確な操作については(明示的に示すまでは)関知しません。これを行うには、実行したい関数をsetCallbackに渡します。

messages.jsの新しいコードは次のとおりです。

// frontend/components/messages/messages.js
import { setCallback } from "client/chat";
import "components/message/message";
import "./messages.css";

const messages = document.querySelector(".js-messages");
const content = messages.querySelector(".js-messages--content");

function scrollToBottom() {
  content.scrollTop = content.scrollHeight;
}

scrollToBottom();

// ActionCableで新しいメッセージを1件受け取るたびに
// このコード片を呼び出すよう`chat.js`に伝える
setCallback(message => {
  content.insertAdjacentHTML("beforeend", message);
  scrollToBottom();
});

ここでchat.jsモジュールに渡しているのは、メッセージのリストを上にスクロールして、新しいメッセージのHTMLを下に追加するだけのシンプルな関数です。これで、2種類の異なるブラウザを立ち上げて、それぞれ別のニックネームでログインしてチャットしてみると、以下のようにすべて正常に動作していることがわかります。

異なるブラウザで動作するチャット

異なるブラウザで動作するチャット

Herokuにデプロイする

いよいよアプリをHerokuにデプロイして、本番環境でもチャットできることを確認しましょう。最初にHerokuアカウントを用意し、自分のPCにHeroku CLIがインストールされていることを確認します。これでターミナルでherokuコマンドが使えるようになります。

アプリのデプロイを準備するうえで必要な点がいくつかあります。

最初に、既存のProcfilerails serverwebpack-dev-serverの実行に使われる)をProcfile.devに変更します。devなしのProcfileはHerokuで使います。また、本番環境ではwebpack-dev-serverが実行されないようにしたいと思います。

Procfile.devは次のようになります。

server: bin/rails server
assets: bin/webpack-dev-server

メインのProcfileにはserver行だけを残します。

server: bin/rails server

注意: この変更を行った後でアプリをlocalhostで実行したい場合は、hivemind Procfile.dev(使っているプロセスマネージャによってはovermind s -f Procfile.devforeman run -f Procfile.devなど)で起動する必要があります。

次に、ビルドタスクがHeroku側で認識されるようにする必要があります。

RubyアプリをプッシュしていることがHeroku側で認識されると、assets:precompileを起動しようとします。これはアセットパイプラインでアセットをビルドするのに昔から使われているタスクです。しかしWebpackerを使う場合は、別のyarn:installタスクとwebpacker:compileタスクを呼び出す必要があります。

最新バージョンのRailsとWebpacker(3.2.0)は、Sprocketsを無効にしてあってもassets:precompileでSprocketsを起動できます(試しにローカルでbundle exec rails assets:precompileを実行してみると、パッケージがビルドされてpublicフォルダに置かれる様子を見ることができます)。

ただし本記事執筆時点では、Rails 5.1.4とWebpacker 3.2.0による「Sprockets抜き」アプリのHerokuでのビルドは失敗しました。Vladimir Dementyevのおかげで回避方法がわかりました。Rakefileで明示的にassets:precompileを定義する必要があります。

# Rakefile
require_relative 'config/application'

# この行を追加
Rake::Task.define_task("assets:precompile" => ["yarn:install", "webpacker:compile"])

Rails.application.load_tasks

RailsとWebpackerのコントリビューターは現在も本番環境でのアセットのビルドをできるだけ楽にする最善の方法を模索中なので、この部分は将来変更される可能性があります。すべてが落ち着いて、追加のハックなしでHerokuでアプリをビルドできるようになれば理想です。

また、HerokuでActionCableを動かすためには本番でRedisを有効にする必要もあります。Gemfileのgem 'redis', '~> 3.0'のコメントを解除してください(注意: バージョン4はRails 5.1のActionCableで認識されません: 5.2で修正予定)。

config/cable.ymlproductionに、urlの正しい設定が含まれていることを確認します。

development:
  adapter: async

test:
  adapter: async

production:
  adapter: redis
  url: <%= ENV["REDIS_URL"] %>
  channel_prefix: evil_chat_production

REDIS_URL環境変数に正しいRedisサーバーのURLを設定するために、Heroku Redisアドオンを使います。

そして最後に、config/environments/production.rbに以下の行を追加してください。

config.secret_key_base = ENV["SECRET_KEY_BASE"]

secrets.ymlをソースコントロールにコミットしない場合は、この行が必要です(ただしRailsの「encrypted secrets」を設定していない場合はこの行を追加すべきではありません)。

ついにデプロイ準備ができました。

$ heroku create YOUR_APP_NAME
$ heroku addons:create heroku-redis:hobby-dev

数分後にHeroku Redisアドオンが作成されたら(heroku addons:infoでステータスを確認できます)、次を実行します。

$ git add . && git commit -m "prepare for deploy"
$ git push heroku master

アプリのビルドが完了したら、heroku run rails db:migrateを実行してproductionのデータベースを準備します。すべてうまくいけば、デプロイしたアプリをheroku openでブラウザに表示できます。

うまく動いた方、おめでとうございます!

補足: 静的なアセットについて

今回ビルドしたアプリでは静的なアセットを使っていませんが、Webpackerで静的なアセットを扱う方法についても触れておく価値があると思います。ここでは画像を扱いたいとしましょう。最初に、画像の置き場所を決める必要があります。frontendフォルダの下のimagesフォルダにまとめて置くか、画面の表示を担当するコンポーネントの下に個別の画像を置きます。画像をどこに置くとしても、画像がWebpack manifestに現れるようにするには、画像をJavaScriptにimportして最終的にapplication.jsのエントリポイントに含まれるようにする必要があります。

app/assets/imagesの下にある既存の画像をすべてfrontend/staticに素早く移動してstatic.jsエントリポイントにリンクする方法については、Gistをご覧ください。

画像の数が多すぎて、ヘルパーモジュールのバンドル項目を増やしたくない場合(Webpackのfile-loaderは、ファイルごとにパスを返す責任だけを持つモジュールを1つ生成します)、packsの下に個別のエントリポイントを作成して(static.jsなどのように)呼び出すこともできます。

そして、asset_pack_pathヘルパーimage_tagを組み合わせると、正しい<img src="">を生成できます。

画像とコンポーネントをまとめる方法は次のような感じになります。

  • フォルダ構造:
frontend/components/header
├── _header.html.erb
├── header.css
├── header.js
└── static
    └── logo.png

header.jsは次のようになります。

import "./header.css";
import "./static/logo.png"

これで次のようにERBパーシャルに書けます。

<%= image_tag asset_pack_path('./static/logo.png') %>

別の方法としては、image_tagを使うのを我慢し、代わりにCSSでurlヘルパーを用いてWebpackのcss-loaderがデフォルトでプロジェクトに含める画像を直接読み込む方法もあります。これで、次のようにCSSのbackground-プロパティとして要素に画像を割り当てることができます。

.header {
  &--logo {
    width: 100px;
    height: 100px;
    margin-bottom: 25px;
    background-image: url("./static/logo.png");
    background-size: 100%;
  }
}

この方法にする場合、JavaScriptファイルで画像をimportする必要も生じません。なお、url()はフォントにも使えます。

プロジェクトのリポジトリには、SVGアイコンをCSSから読み込む例も含まれています。インラインSVGを使いたい場合は、postcss-inline-svgモジュールを使うこともできます。

「Sprockets抜き」をやってみてわかったこと

ActionCableを使った場合とまったく同様に、RailsでSprocketを無効にすると他のいくつかの部分についてもnpmで再インストールする必要が生じます。

  • Turbolinks

プロジェクトでTurbolinksを再度有効にするには以下のようにします。

$ yarn add turbolinks
// frontend/packs/application.js
import Turbolinks from "turbolinks";
Turbolinks.start();
  • UJS

RailsにSprocketsがない場合、次のようにnpmrails-ujsを再インストールしないとUnobtrusive JavaScriptを理解できなくなります(link_tomethod: :deleteの設定など)。

$ yarn add rails-ujs
// frontend/packs/application.js
import Rails from "rails-ujs";
Rails.start();

本チュートリアルからヒントを得たプロジェクトの紹介

  • Komponentは、本記事で解説した「コンポーネントベースのアプローチ」をRailsプロジェクトに取り入れやすくするgemです。このgemに含まれるジェネレーターは、frontendフォルダの作成、Webpacker configの変更、コンポーネント作成を単一のコマンドで行なえます。また、パーシャルにふさわしいテンプレートエンジンを検出したり、コンポーネントごとの「プロパティ」やヘルパーの設定に使える.rbファイルでコンポーネントを拡張したりします。

Komponent gemの作成とメンテナンスは、フランスの開発会社OuvragesEtamin Studioが、Evil Martiansとは独立に行っています。


お読みいただきありがとうございました!

本チュートリアル3部作(全貌を理解するにはすべてお読みください)では、Webpackerを完全に採り入れてアセットパイプラインを取り除き、Reactなどのフロントエンドフレームワークについて学ばずに、できるだけRailsの組み込みツールを用いて「コンポーネント」のコンセプトに基づいてRailsのフロントエンドコードを編成する方法を学びました。本チュートリアルで作ったシンプルなチャットアプリは、Evil Martiansによって現実のプロジェクトで積極的に用いられている方法でデプロイ可能です。

本チュートリアルを進めるうえで何か問題がありましたら、お気軽にGitHubのissueを開いてお知らせください。


Part 1 | Part 2 | Part 3

スタートアップをワープ速度で成長させられる地球外エンジニアよ!Evil Martiansのフォームにて待つ。

関連記事

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

新しいRailsフロントエンド開発(2)コンポーネントベースでアプリを書く(翻訳)

Rails: モデルのクエリをカプセル化する2つの方法(翻訳)

$
0
0

概要

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

Rails: モデルのクエリをカプセル化する2つの方法(翻訳)

要点: サービスやコントローラなどのクラスからデータベースクエリのロジックを分離することは間違いなく優れた方法です。ロジックをモデルに置く場合、次の2とおりの方法が使えます。

1. クラスメソッド化する

def self.recent
  order(created_at: :desc)
end

2. ActiveRecordのスコープAPIを使う

scope :recent, -> { order(created_at: :desc) }

どちらにすればよいか

ActiveRecordのスコープはどっちみちクラスメソッドに変換されるので、どちらを選ぶかは見た目の問題に過ぎません。ただし、

スコープはいついかなるときでもチェイン可能である点がポイントです。

次のように、スコープの定義内に条件を含めた場合でもチェインできます。

scope :by_email, -> |email| { where(email: email) if email.present? }

クラスメソッドで同じことをした場合、メソッドをチェインできないことがあります。

def self.by_email(email)
  where(email: email) if email.present?
end

チェインできない理由は、self.by_emailemailがblankの場合にnilを返していることです。

ではどちらにすればよいか

チームの好みに合わせて決めればよいでしょう。その代わり、一度決めたらアプリ全体でその書き方を統一します。

関連記事

Railsのdefault_scopeは使うな、絶対(翻訳)

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

Rails: DockerでHeroku的なデプロイソリューションを構築する: 前編(翻訳)

$
0
0

概要

原著者の許諾を得て、CC BY-NC-SAライセンスに基づき翻訳・公開いたします。

記事のボリュームが大きいので前編/後編に分割しました。後編は来週公開予定です。

Rails: DockerでHeroku的なデプロイソリューションを構築する: 前編(翻訳)

Herokuライクなデプロイソリューションの構築方法を解説します。
特定のクラウドプロバイダや、Dockerと無関係なツールを必要としません。

はじめに

本チュートリアルでは、Herokuにデプロイする感覚でソフトウェアをシンプルに自動デプロイするツールの作成方法をご紹介します。デプロイごとのバージョン管理にはDockerを使い、アップグレードやロールバックをやりやすくします。また、アプリの継続的デプロイ(CD)に弊社のSemaphoreも使います。

コンテナは任意のDocker Registryにホスティングできます。アプリの実行に必要なのはDockerがインストールされているホストだけです。

チュートリアルを終えると、サンプルアプリをリモートホストにHerokuと同じようにデプロイできるシンプルなRuby CLIスクリプトが使えるようになります。直前のバージョンへのロールバック、ログの追加、実行中のアプリのバージョントラッキングを行えるコマンドもあります。

本チュートリアルはデプロイ手順を中心に据えていますので、用途に応じて環境を調整すれば任意のアプリで使えます。ここではシンプルなHello WorldをRuby on Railsでblogフォルダに構築します。Railsアプリ構築の初歩については、RailsガイドのRails をはじめようの手順1〜4をご覧ください。

必要なもの

  • Docker: ホストと、アプリをデプロイするすべてのマシンにDockerがインストールされ、動作している必要があります。
  • Docker Registryのアカウント(Docker Hubなど)
  • SSHアクセスが可能でDockerがインストールされているクラウドプロバイダ(AWS EC2など)
  • Ruby 2.3: アプリをデプロイするすべてのマシンにインストールされている必要があります。

デプロイの手順

デプロイの手順は次の5つで構成されます。

  • ビルド: いつでも変更可能なビルド手順を備えた独自のコンテナをアプリごとにビルドします。
  • アップロード: アプリのコンテナのビルドが終わったらDocker Registryに送信する必要があります。初回はコンテナ全体のアップロードが必要なので多少時間がかかりますが、次回からはDockerのレイヤシステムでサイズや帯域を節約できるので速くなります
  • 接続: Docker Registryにコンテナを送信したら、次の手順を行うためにホストに接続します。
  • ダウンロード: ホストに接続したら、コンテナをダウンロードします。
  • 再起動: 最後の手順では、アプリを停止し、続いて停止時と同じ設定(ポート、ログ、環境変数など)で新しいコンテナを起動します。

手順の概要を把握できたので、作業を開始しましょう。

1. コンテナのビルド

この例では、アプリを実行するコンテナを1つ使います(Rubyはコンパイル言語ではないのでいわゆるビルドは不要です)。この場合のDockerファイルは次のとおりです。

FROM ruby:2.3.1-slim

COPY Gemfile* /tmp/
WORKDIR /tmp

RUN gem install bundler &&\
    apt-get update &&\
    apt-get install -y build-essential libsqlite3-dev rsync nodejs &&\
    bundle install --path vendor/bundle

RUN mkdir -p /app/vendor/bundle
WORKDIR /app
RUN cp -R /tmp/vendor/bundle vendor
COPY application.tar.gz /tmp

CMD cd /tmp &&\
    tar -xzf application.tar.gz &&\
    rsync -a blog/ /app/ &&\
    cd /app &&\
    RAILS_ENV=production bundle exec rake db:migrate &&\
    RAILS_ENV=production bundle exec rails s -b 0.0.0.0 -p 3000

スクリプトを整理するために、Dockerfileはアプリの1つ上のフォルダ階層に置きます。次のような構成になります。

.
├── Dockerfile
├── blog
│   ├── app
│   ├── bin
... (アプリのファイルやフォルダ)

Dockerfileの各行について解説します。

FROM ruby:2.3.1-slim

これはコンテナのビルドに使うベースイメージです。Rubyがインストールされている必要があるので、自分で全部インストールするよりもプレインストール済みのコンテナを使う方が楽です。

COPY Gemfile* /tmp/
WORKDIR /tmp

ここでは、GemfileとGemfile.lockをコンテナの/tmpディレクトリにコピーし、次のコマンドを実行する/tmpに移動しています。

RUN gem install bundler &&\
    apt-get update &&\
    apt-get install -y build-essential libsqlite3-dev rsync nodejs &&\
    bundle install --path vendor/bundle

このRubyイメージのbundlerは古いので、warning表示を避けるためにアップデートしています。本チュートリアルで使われているのとは別のアプリで作業する場合は、他にもいくつかのパッケージ(多くはコンパイラ)が必要になるでしょう。最後にGemfileのgemをすべてインストールします。

Dockerの各コマンドは(layerなど)、コマンドの結果が同じ場合に再実行を避けるためにキャッシュされます。これで多少時間を節約できます。--pathフラグは、すべてのgemをローカルの定義済みパス(vendor/bundle)にインストールするよう指示します。

RUN mkdir -p /app/vendor/bundle
WORKDIR /app
RUN cp -R /tmp/vendor/bundle vendor
COPY application.tar.gz /tmp

ここでは、bundlerの最終的なインストールパスを作成し、インストールされたgemを前回のビルドキャッシュからすべてコピーしてから、圧縮されたアプリをコンテナ内にコピーします。

CMD cd /tmp &&\
    tar -xzf build.tar.gz &&\
    rsync -a blog/ /app/ &&\
    cd /app &&\
    RAILS_ENV=production bundle exec rake db:migrate &&\
    RAILS_ENV=production bundle exec rails s -b 0.0.0.0 -p 3000

このコマンドはdocker runコマンドのときに実行されます。コンテナ内部の圧縮されたアプリを展開し、セットアップ手順(migrate)を実行してアプリを起動します。

Dockerfileの設定どおりに動作していることを確認するには、Dockerfileのあるrootディレクトリに移動して次のコマンドを実行します。

: 以下のmydockeruserはDocker Registryの登録済みユーザー名です。これは後でコンテナのバージョン管理に用います。

注2: Railsをproduction環境で実行する場合は、config/secrets.ymlファイルでSECRET_KEY_BASEなどの環境変数が必要です。ここでは単なるサンプルアプリを使っているので、development環境やtest環境と同様に固定値で安全に上書きできます。

$ cp blog/Gemfile* .
$ tar -zcf application.tar.gz blog
$ docker build -t mydockeruser/application-container .

上を実行すると、Dockerfileの各手順のビルドが以下のように開始されます。

Sending build context to Docker daemon 4.386 MB
Step 1/9 : FROM ruby:2.3.1-slim
 ---> e523958caea8
Step 2/9 : COPY Gemfile* /tmp/
 ---> f103f7b71338
Removing intermediate container 78bc80c13a5d
Step 3/9 : WORKDIR /tmp
 ---> f268a864efbc
Removing intermediate container d0845585c84d
Step 4/9 : RUN gem install bundler &&     apt-get update &&     apt-get install -y build-essential libsqlite3-dev rsync nodejs &&     bundle install --path vendor/bundle
 ---> Running in dd634ea01c4c
Successfully installed bundler-1.14.6
1 gem installed
Get:1 http://security.debian.org jessie/updates InRelease [63.1 kB]
Get:2 http://security.debian.org jessie/updates/main amd64 Packages [453 kB]
...

すべて問題なく完了すると、以下の成功メッセージが表示されます。

Successfully built 6c11944c0ee4

このハッシュ値はDockerによってランダムに生成されるので、コンテナをビルドするたびに異なります。

キャッシュが効いていることを確認するために、同じコマンドを再実行してみましょう。今度はほぼ一瞬で完了します。

$ docker build -t mydockeruser/application-container .
Sending build context to Docker daemon 4.386 MB
Step 1/9 : FROM ruby:2.3.1-slim
 ---> e523958caea8
Step 2/9 : COPY Gemfile* /tmp/
 ---> Using cache
 ---> f103f7b71338
Step 3/9 : WORKDIR /tmp
 ---> Using cache
 ---> f268a864efbc
Step 4/9 : RUN gem install bundler &&     apt-get update &&     apt-get install -y build-essential libsqlite3-dev rsync nodejs &&     bundle install --path vendor/bundle
 ---> Using cache
 ---> 7e9c77e52f81
Step 5/9 : RUN mkdir -p /app/vendor/bundle
 ---> Using cache
 ---> 1387419ca6ba
Step 6/9 : WORKDIR /app
 ---> Using cache
 ---> 9741744560e2
Step 7/9 : RUN cp -R /tmp/vendor/bundle vendor
 ---> Using cache
 ---> 5467eeb53bd2
Step 8/9 : COPY application.tar.gz /tmp
 ---> Using cache
 ---> 08d525aa0168
Step 9/9 : CMD cd /tmp &&     tar -xzf application.tar.gz &&     rsync -a blog/ /app/ &&     cd /app &&     RAILS_ENV=production bundle exec rake db:migrate &&     RAILS_ENV=production bundle exec rails s -b 0.0.0.0 -p 3000
 ---> Using cache
 ---> ce28bd7f53b6
Successfully built ce28bd7f53b6

エラーメッセージが表示されたら、Dockerfileの構文やコンソールエラーをチェックしてやり直します。

次はすべて問題ないことを確認するために、コンテナのアプリを実行できるかどうかをテストしたいと思います。以下のコマンドを実行します。

docker run -p 3000:3000 -ti mydockeruser/application-container

これはコンテナを実行し、ホストのポート番号3000をコンテナのポート番号3000にマッピングします。問題が起きなければ次のようなRails起動メッセージが表示されます。

=> Booting Puma
=> Rails 5.0.2 application starting in production on http://0.0.0.0:3000
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.8.2 (ruby 2.3.1-p112), codename: Sassy Salamander
* Min threads: 5, max threads: 5
* Environment: production
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop

これで、localhost:3000をブラウザで開けばWelcomeメッセージが表示されます。

2. コンテナをDocker Registryにアップロードする

Docker Registryにログインするには以下の手順が必要です。

> docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don\'t have a Docker ID, head over to https://hub.docker.com to create one.
Username: mydockeruser
Password: ########
Login Succeeded

コンテナは完全に動作するので、今度はこれをDocker Registryにアップロードする必要があります。

$ docker push mydockeruser/application-container
The push refers to a repository [docker.io/mydockeruser/application-container]
9f5e7eecca3a: Pushing [==================================================>] 352.8 kB
08ee50f4f8a7: Preparing
33e5788c35de: Pushing  2.56 kB
c3d75a5c9ca1: Pushing [>                                                  ] 1.632 MB/285.2 MB
0f94183c9ed2: Pushing [==================================================>] 9.216 kB
b58339e538fb: Waiting
317a9fa46c5b: Waiting
a9bb4f79499d: Waiting
9c81988c760c: Preparing
c5ad82f84119: Waiting
fe4c16cbf7a4: Waiting

コンテナはレイヤごとにアップロードされますが、中には巨大なものもあります(100MB以上)初回に巨大なレイヤがアップロードされるのは問題ありません。今後はDockerのレイヤシステムを用いてアプリの変更分だけをアップロードし、ディスク容量や帯域を節約します。docker pushやレイヤについて詳しくお知りになりたい方は、公式ドキュメントをご覧ください。

pushが終わると成功のメッセージが表示されます。

...
9f5e7eecca3a: Pushed
08ee50f4f8a7: Pushed
33e5788c35de: Pushed
c3d75a5c9ca1: Pushed
0f94183c9ed2: Pushed
b58339e538fb: Pushed
317a9fa46c5b: Pushed
a9bb4f79499d: Pushed
9c81988c760c: Pushed
c5ad82f84119: Pushed
fe4c16cbf7a4: Pushed
latest: digest: sha256:43214016a4921bdebf12ae9de7466174bee1afd44873d6a60b846d157986d7f7 size: 2627

Docker Registryコンソールで新しいイメージを確認できます。

イメージを再度pushしてみると、すべてのレイヤが既に存在することがわかります。Dockerは再アップロードを回避するために、各レイヤのハッシュを照合してレイヤが既にあるかどうかをチェックします。

$ docker push mydockeruser/application-container
The push refers to a repository [docker.io/mydockeruser/application-container]
9f5e7eecca3a: Layer already exists
08ee50f4f8a7: Layer already exists
33e5788c35de: Layer already exists
c3d75a5c9ca1: Layer already exists
0f94183c9ed2: Layer already exists
b58339e538fb: Layer already exists
317a9fa46c5b: Layer already exists
a9bb4f79499d: Layer already exists
9c81988c760c: Layer already exists
c5ad82f84119: Layer already exists
fe4c16cbf7a4: Layer already exists
latest: digest: sha256:43214016a4921bdebf12ae9de7466174bee1afd44873d6a60b846d157986d7f7 size: 2627

3. リモート接続を開く

コンテナのアップロードが終わったので、リモートサーバーにダウンロードして実行する方法を見てみましょう。最初に、コンテナを実行するリモート環境の準備が必要です。ホストマシンで行ったときと同様に、DockerをインストールしてDocker Registryにログインしなければなりません。SSHでリモート接続するには、以下のコマンドを実行します。

ssh remoteuser@35.190.185.215
# 認証が必要な場合は以下を実行
ssh -i path/to/your/key.pem remoteuser@35.190.185.215

4. ダウンロード

リモートマシンでの設定をすべて終えた後は、ターミナルでのアクセスは不要になります。各コマンドはその環境で実行されます。コンテナをダウンロードしましょう。必要な場合はキーのフラグを指定することもお忘れなく。

$ ssh remoteuser@35.190.185.215 docker pull mydockeruser/application-container
Using default tag: latest
latest: Pulling from mydockeruser/application-container
386a066cd84a: Pulling fs layer
ec2a19adcb60: Pulling fs layer
b37dcb8e3fe1: Pulling fs layer
e635357d42cf: Pulling fs layer
382aff325dec: Pulling fs layer
f1fe764fd274: Pulling fs layer
a03a7c7d0abc: Pulling fs layer
fbbadaebd745: Pulling fs layer
63ef7f8f1d60: Pulling fs layer
3b9d4dda739b: Pulling fs layer
17e2d6aad6ec: Pulling fs layer
...
3b9d4dda739b: Pull complete
17e2d6aad6ec: Pull complete
Digest: sha256:c030e4f2b05191a4827bb7a811600e351aa7318abd3d7b1f169f2e4339a44b20
Status: Downloaded newer image for mydockeruser/application-container:latest

5. 再起動

コンテナを初めて実行したので、他のコンテナを停止する必要はありません。ローカルホストのときと同じコマンドを使って次のようにコンテナを実行できます。

$ ssh remoteuser@35.190.185.215 docker run -p 3000:3000 -d mydockeruser/application-container
f86afaa7c9cc4730e9ff55b1472c5b30b0e02055914f1673fbd4a8ceb3419e23

ここでは-tiフラグの代わりに-dフラグを与えているので、コンテナのハッシュだけが出力されます。これはコンテナをdetachedモードで動かす(出力をターミナルにアタッチしない)ことを表します。

ブラウザでリモートホストアドレス(ここでは35.190.185.21:3000を開いて、アプリが実行されているかどうかをチェックします。

(後編に続きます)

関連記事

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

Dockerでsupervisorを使う時によくハマる点まとめ

PrometheusでDockerホスト + コンテナを監視してみた

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

$
0
0

概要

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

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

Code Climate編集者メモ: 今回はゲストとしてMarko Anastasovの記事もご紹介します。Markoは開発者であると同時に、CI/デプロイサービスで知られるSemaphoreの共同設立者であり、Code ClimateのCIパートナーでもあります。


単体テストを書くという行為は、検証よりも設計という行為に近い — Bob Martin

テスト駆動開発(TDD)はテストのためのものであるという思い違いを未だによく見かけます。TDDを遵守することで開発が迷走する可能性を最小限に抑えることができ、最初にテストを書くことを義務付けることでテストの書き忘れも最小限に留められます。いつもの私は、超人であり続けなければとてもなしえないようなソリューションではなく、普通の人間のために設計されたソリューションを選びますが、ここでは少し違います。TDDは自動化テストを一種の乗り物のように用いて、私たちがコードを書く前にコードのことをいやでも考えざるを得ないように設計されています。なおこの方法は、特定の機能に接続されるすべてのコードが期待どおり動作していることを確認するのにデバッガを起動するよりもずっとよい方法です。TDDの目的はソフトウェア設計の改良であり、テストコードはその副産物のひとつです。

テストを必ず最初に書くことで、テストされるオブジェクトのインターフェイスについてじっくり考えるようになります。必要だがまだ存在しないオブジェクトについても同様です。作業は制御可能な小さな範囲で少しずつ進められます。テストが初めてパスしてもそこで作業は終わりではありません。再び実装に立ち戻ってコードをリファクタリングし、コードを美しく保ちます。コードが正しく動作していることを担保するテストスイートが存在するおかげで、自信を持ってコードを変更できます。

TDDの経験者なら誰でも、コードの設計力を問われ、そして磨かれることに気づくようになります。開発しながら常に「むー、このコードはprivateのままではまずそうだな」とか「このクラスの責務が増えすぎてしまった」という風に考えるようになるのです。

テスト駆動リファクタリング

あるコードのテストをどう書けばよいかわからなくなってくると、「red-green-refactor」というサイクルが止まってしまうこともあるでしょうし、たとえ書けたとしてもかなりつらい作業に思えることでしょう。テストを書くのがつらい部分は、しばしばコードの設計に問題があることを示します。あるいは、その部分のコードがTDDアプローチに沿って書かれていなかっただけかもしれませんが。テストコードの「匂い」は多くの場合アンチパターンと呼ぶのがふさわしく、テストとアプリコードの両方についてリファクタリングする機会であることを示します。

例として、Railsのcontroller specでの複雑なテストセットアップを見てみましょう。

describe VenuesController do

  let(:leaderboard) { mock_model(Leaderboard) }
  let(:leaderboard_decorator) { double(LeaderboardDecorator) }
  let(:venue) { mock_model(Venue) }

  describe "GET show" do

    before do
      Venue.stub_chain(:enabled, :find) { venue }
      venue.stub(:last_leaderboard) { leaderboard }
      LeaderboardDecorator.stub(:new) { leaderboard_decorator }
    end

    it "venueをidで検索して@venueに代入する" do
      get :show, :id => 1
      assigns[:venue].should eql(venue)
    end

    it "@leaderboardを初期化する" do
      get :show, :id => 1
      assigns[:leaderboard].should == leaderboard_decorator
    end

    context "userはpatronとしてログインしている" do

      include_context "patronがログインしている"

      context "patronはトップ10にいない" do

        before do
          leaderboard_decorator.stub(:include?).and_return(false)
        end

        it "leaderboardからpatronのstatsを取得" do
          patron_stats = double
          leaderboard_decorator.should_receive(:patron_stats).and_return(patron_stats)
          get :show, :id => 1
          assigns[:patron_stats].should eql(patron_stats)
        end
      end
    end

    # 簡単のため以後のテストケースは省略
  end
end

このコントローラのアクションは、技術的にはさほど長くありません。

class VenuesController < ApplicationController

  def show
    begin
      @venue = Venue.enabled.find(params[:id])
      @leaderboard = LeaderboardDecorator.new(@venue.last_leaderboard)

      if logged_in? and is_patron? and @leaderboard.present? and not @leaderboard.include?(@current_user)
        @patron_stats = @leaderboard.patron_stats(@current_user)
      end
    end
  end
end

ここでお気づきいただきたいのは、specセットアップのコードが長いと、たとえばVenue.enabled.findが呼び出されるというexpectationや、LeaderboardDecorator.newに正しい引数が渡されるというexpectationを開発者が書き忘れてしまいがちであるという点です。代入された@leaderboardの元は代入されたvenueであるかどうかがまったく明確になっていません。

MVCパラダイムに囚われてしまった開発者は(私も含めてですが)、ついコントローラにビジネスロジックを長々と書き連ねてしまい、よいspecを書くこともコードやspecのメンテも困難になってしまいます。この困難は、Railsのコントローラのたった1行のメソッドですら多くのことを行っていることが原因です。

def show
  @venue = Venue.find(params[:id])
end

上のメソッドはこれだけの作業を行っています。

  • パラメータを取り出す
  • アプリ固有のメソッドを呼び出す
  • ビューテンプレートで用いられる変数へ代入する
  • レスポンステンプレートのレンダリング

データベース内部やビジネスルールの奥深い部分に到達するコードを書き足すと、コントローラのメソッドがカオスになるだけです。

上のコントローラには、4つの条件を持つif文が隠れています。完全なspecでは、これをカバーするためだけに15とおりの組み合わせを記述しなければなりませんが、もちろんそのようなものは書かれていません。しかし、コードがコントローラの外に置かれる場合は事情が変わってきます。

改良版のcontroller specが次のようになっているとしましょう。外部から受け付けるリクエストを処理してレスポンスを準備するという作業を実行するためにはどのようなインターフェイスが望ましいでしょうか。

describe VenuesController do

  let(:venue) { mock_model(Venue) }

  describe "GET show" do

    before do
      Venue.stub(:find_enabled) { venue }
      venue.stub(:last_leaderboard)
    end

    it "有効なvenueをidで検索する" do
      Venue.should_receive(:find_enabled).with(1)
      get :show, :id => 1
    end

    it "見つかった@venueを代入する" do
      get :show, :id => 1
      assigns[:venue].should eql(venue)
    end

    it "venueのleaderboardをデコレーションする" do
      leaderboard = double
      venue.stub(:last_leaderboard) { leaderboard }
      LeaderboardDecorator.should_receive(:new).with(leaderboard)

      get :show, :id => 1
    end

    it "@leaderboardを代入する" do
      decorated_leaderboard = double
      LeaderboardDecorator.stub(:new) { decorated_leaderboard }

      get :show, :id => 1

      assigns[:leaderboard].should eql(decorated_leaderboard)
    end
  end
end

他のコードはどこに行ってしまったのでしょうか?ここではモデルを拡張して検索ロジックを単純化しています。

describe Venue do

  describe ".find_enabled" do

    before do
      @enabled_venue = create(:venue, :enabled => true)
      create(:venue, :enabled => true)
      create(:venue, :enabled => false)
    end

    it "有効なスコープ内で検索する" do
      Venue.find_enabled(@enabled_venue.id).should eql(@enabled_venue)
    end
  end
end

さまざまなif文は次のように単純化できます。

  • if logged_in?: 結果の違いはビューテンプレートで決定できる
  • if @leaderboard.present?: (古いコード)falseの場合の動作はビューで決定できる
  • その他のコードはdecoratorクラスに移動して新しいメソッドで詳しく記述できる
describe LeaderboardDecorator do

  describe "#includes_patron?" do

    context "userがpatronではない" { }

    context "userがpatronである" do
      context "userがリストにいる" { }
      context "ユーザーがリストにいない" { }
    end
  end
end

この新しいメソッドは、@leaderboard.patron_statsをレンダリングするかどうかをビューで決定できるようにします。この部分の変更は不要です。

# app/views/venues/show.html.erb
<%= render "venues/show/leaderboard" if @leaderboard.present? %>
# app/views/venues/show/_leaderboard.html.erb
<% if @leaderboard.includes_patron?(@current_user) -%>
  <%= render "venues/show/patron_stats" %>
<% end -%>

これで、コントローラのメソッドがかなりシンプルになりました。

def show
  @venue = Venue.find_enabled(params[:id])
  @leaderboard = LeaderboardDecorator.new(@venue.last_leaderboard)
end

このコードを次回使うときには、LeaderboardDecoratorに与える正しい引数とは何かをコントローラ側で把握する必要がある点がちょっと残念かもしれません。venue用の新しいdecoratorを1つ導入して、デコレーションされたleaderboardを返すようにしてもよいでしょう。この部分の実装は読者の練習用に残しておきます ;)

最後に

もっと詳しくお知りになりたい方は、SemaphoreブログでMarkoのRailsアプリのテスティングアンチパターン記事をご覧ください。

関連記事

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

Railsで重要なパターンpart 2: Query Object(翻訳)

Ruby: Proxyパターンの解説(翻訳)

Rails: belongs_to関連付けをリファクタリングしてDRYにする(翻訳)

$
0
0

概要

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

Rails: belongs_to関連付けをリファクタリングしてDRYにする(翻訳)

belongs_toリレーションはRailsアプリで最もよく使われる関連付けなので、皆さまのアプリでも多数使われていると確信しています。さて、JobCategoryという2つのモデルがあるとしましょう。1つのjobは1つのcategoryに属し、1つのcategoryには多くのjobがあるというシンプルな関連付けがなされています。

Categoryモデルにはpublished:booleanという属性があり、そのcategoryのjobを表示してよいかどうかを指定します。ここでの目的は、publishされたカテゴリに割り当てられているjobだけを返すクエリを作成することです。

普通の方法

通常は、以下のような方法を使います。

Job.joins(:category).where(categries: {published: true})

これに何かまずい点があるのでしょうか?別にありません。しかしここでは「ロジックの分離」に着目したいと思います。Jobモデルで何か操作を行う場合、publishされたcategoryだけを取り出すという条件を満たすことを気にかけるべきではありません。これはCategoryモデルに関連するロジックなので、このロジックをそちらに移動しましょう。

ロジックの分離

class Category < ActiveRecord::Base
 has_many :jobs

  def self.publishable
    where(published: true)
  end
end

ここではクラスメソッドの代わりにスコープを使うこともできます(スコープかクラスメソッドかについては別記事をご覧ください【原文リンク切れ】)。今回の場合、クラスメソッドの方が私にとって明確に思えたのでクラスメソッドを使うことにします。これによってJobモデルは次のようになります。

class Job < ActiveRecord::Base
  belongs_to :category

  def self.publishable
    joins(:category).merge(Category.publishable)
  end
end

これで、次のように呼び出せるようになりました。

Job.publishable

改善の理由

メンテナンスするコードがもっとたくさんある場合になぜ2番目のソリューションの方がよいかについて疑問をお持ちの方もいらっしゃるかもしれません。理由は次のとおりです。

  1. ロジックを分離できます。categoryに関連するものはCategoryモデルに配置され、jobに関連するものはJobに配置されています。あなたの同僚は、ロジックをチェックして正確にはどんなクエリが使われるべきかを調べなくても、Job.publishableを呼び出すだけで済みます。
  2. 最初のバージョンのクエリだと、アプリのあちこちにJob.joins(:category).where(categries: {published: true})がばらまかれてしまいます。そのcategoryがpublishされているかどうかを調べるために条件をもっと詳しくチェックしなければならないとしたらどうしますか?ばらまかれているコードをすべて見つけ出すという残念な方法を取らざるを得なくなります。しかし2番目の方法なら、メソッドに変更を加えるだけで済みます。他に何も変更する必要はありません。

  3. 人間にとってより読みやすいコードになります。これはチームで若手開発者を抱えている場合に非常に重要な点です。

  4. Categoryモデルに関連付けられているあらゆるモデルでCategory.publishedを使えるようになります。

Railsでお困りの方にお知らせ

twitter または連絡用フォームにてお知らせください。サポート方法をご連絡いたします。

関連記事

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

週刊Railsウォッチ(20180119)derailed_benchmarks gem、PostgreSQLをGraphQL API化するPostGraphile、機械学習でモック画像をHTML化ほか

$
0
0

こんにちは、hachi8833です。Nintendo Laboにいろいろ持ってかれそうで気になってます。


つっつきボイス: 「任天堂のものづくりセンス、パないなー」

それでは今週のウォッチ、いってみましょう。

Rails: 今週の改修

今回はCommit差分から見繕いました。

left_outer_joinsをunscopeできるようになった

# activerecord/lib/active_record/relation/query_methods.rb#351
     VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock,
                                      :limit, :offset, :joins, :includes, :from,
-                                     :readonly, :having])
+                                     :readonly, :having, :left_outer_joins])

つっつきボイス: 「scopeでleft_outer_joinsできるならunscopeもできないと、ってことかな」

重要度の低いダイジェストにデフォルトでSHA-1を使用するようになった

# railties/lib/rails/application/configuration.rb#103
           if respond_to?(:active_support)
             active_support.use_authenticated_message_encryption = true
+            active_support.use_sha1_digests = true
           end

ETagヘッダーなどの重要でないダイジェストにはMD5ではなくSHA-1を使う。
Rails.application.config.active_support.use_sha1_digests = true
new_framework_defaults_5_2.rb.ttより大意

pg-1.0 gemに対応

pgが0.21から1.0にメジャーバージョンアップしたそうです。

# Gemfile.lock#343
-    pg (0.19.0)
-    pg (0.19.0-x64-mingw32)
-    pg (0.19.0-x86-mingw32)
+    pg (1.0.0)
+    pg (1.0.0-x64-mingw32)
+    pg (1.0.0-x86-mingw32)

つっつきボイス: 「へー、pgはもう永遠に1.0にならないんじゃないかと思ってた」「queue_classicってメンテナ代わったのかな?↓」

# Gemfile#65
-  gem "queue_classic", github: "QueueClassic/queue_classic", branch: "master", require: false, platforms: :ruby
+  gem "queue_classic", github: "Kjarrigan/queue_classic", branch: "update-pg", require: false, platforms: :ruby

savesave!の後でオブジェクトがunfreezeされていたのを修正

破棄したオブジェクトがsave後に変更される可能性があったので修正されました。

# activerecord/lib/active_record/persistence.rb#65
     def create_or_update(*args, &block)
       _raise_readonly_record_error if readonly?
+      return false if destroyed?
       result = new_record? ? _create_record(&block) : _update_record(*args, &block)
       result != false
     end

つっつきボイス: 「これ本当ならエラーをraiseしたいところだろうな: 互換性とかの問題でfalseを返してるのかも」

MySQL: create_databasecollationが指定されている場合にデフォルトのcharsetを追加しないように修正

# activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb#250
       def create_database(name, options = {})
         if options[:collation]
-          execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')} COLLATE #{quote_table_name(options[:collation])}"
+          execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT COLLATE #{quote_table_name(options[:collation])}"
         else
           execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')}"
         end

つっつきボイス: 「そうそう、知らずに|| 'utf8'が効いちゃうとハマるんだよなー」「修正後のテストでは寿司ビール対策でおなじみのutf8mb4_bin使ってますね」

MySQLのencodingをutf8からutfmb4に変更して寿司ビール問題に対応する

リファクタリング: Browserクラスを新設

システムテストのactionpack/lib/action_dispatch/system_testing/driver.rbのオプションがBrowserクラスに引っ越しました。

# actionpack/lib/action_dispatch/system_testing/browser.rb
+module ActionDispatch
+  module SystemTesting
+    class Browser # :nodoc:
+      attr_reader :name
+
+      def initialize(name)
+        @name = name
+      end
...

DHHによる修正2件

# rails/railties/lib/rails/generators/rails/app/templates/config/initializers/content_security_policy.rb.tt#9
   policy.font_src    :self, :https, :data
   policy.img_src     :self, :https, :data
   policy.object_src  :none
-  policy.script_src  :self, :https
+  policy.script_src  :self, :https, :unsafe_inline
   policy.style_src   :self, :https, :unsafe_inline

   # Specify URI for violation reports

つっつきボイス: 「CSP=コンテンツセキュリティポリシー」「unsafe_inlineはW3Cのこれですね↓」

; Keywords:
keyword-source = “‘self'” / “‘unsafe-inline'” / “‘unsafe-eval'” / “‘strict-dynamic'” / “‘unsafe-hashed-attributes'”
CSP3より

-Rails.application.config.content_security_policy do |p|
-  p.default_src :self, :https
-  p.font_src    :self, :https, :data
-  p.img_src     :self, :https, :data
-  p.object_src  :none
-  p.script_src  :self, :https
-  p.style_src   :self, :https, :unsafe_inline
+Rails.application.config.content_security_policy do |policy|
+  policy.default_src :self, :https
+  policy.font_src    :self, :https, :data
+  policy.img_src     :self, :https, :data
+  policy.object_src  :none
+  policy.script_src  :self, :https
+  policy.style_src   :self, :https, :unsafe_inline

つっつきボイス: 「少なくともpはないなー: Kernel.#pがあるから」「そういえば1文字のローカル変数で他にも使えないものがあったような…」

その後思い出しましたが、pryではcなどをローカル変数に使うと怒られるのでした。

[1] pry(main)> c=1
=> 1
[2] pry(main)> c
Error: Cannot find local context. Did you use `binding.pry`?

参考: Pryのコンソールで使えない変数

Rails

Railsチュートリアルが5.1.4に対応


つっつきボイス: 「安川さんたちが継続的翻訳システムを構築しているおかげでRailsチュートリアルもガイドもオープンな差分翻訳ができるようになっててうれしいです: 自分はバッチで翻訳する方が好きですが」

プロセスマネージャ再び

Dogfooding Process Managerの続きだそうです。


つっつきボイス: 「自前でプロセスマネージャをこしらえた話のようなんですが、このプロセスって何だろうと思って」「ざっとしか見てないけど、Unixのプロセスのことではなさそうに見える」「ところで、何とかmanagerってネーミングはたいていアンチパターンですね」「あー確かに」

そういえば野球の世界では監督はmanagerですが、日本だとマネージャーは違う意味に横滑りしてますね。

マイグレーションをpendingしたままRailsを本番で実行しないようにする方法

短い記事です。ActiveRecord::Migration.check_pending!でやれるそうです。

# 同記事より
if ($PROGRAM_NAME.include?('puma') || $PROGRAM_NAME.include?('sidekiq')) && Rails.env.production?
  ActiveRecord::Migration.check_pending!
end

RailsのForm Objectとルーティング(RubyFlowより)

# 同記事より
class NewQuestionnaireForm
   include ActiveModel::Model

  def to_model
    Questionnaire.new(title: title, questions: questions)
  end

  def save
    to_model.save
  end
end

Railsのメモリ容量を減らしてHeroku課金を節約(Awesome Rubyより)


同記事より

以下の記事に出てきたjemallocyajl-rubyなどを動員して節約に励んでいます。

Ruby: mallocでマルチスレッドプログラムのメモリが倍増する理由(翻訳)

⭐derailed_benchmarks: Railsアプリのさまざまなベンチマークを取れるgem⭐

上の記事にも使われていたgemで、★1800超えです。ヒープダンプ/メモリリーク調査/stackprofなどさまざまな静的/動的情報を取れます。

# READMEより
$ bundle exec derailed exec perf:stackprof
==================================
  Mode: cpu(1000)
  Samples: 16067 (1.07% miss rate)
  GC: 2651 (16.50%)
==================================
     TOTAL    (pct)     SAMPLES    (pct)     FRAME
      1293   (8.0%)        1293   (8.0%)     block in ActionDispatch::Journey::Formatter#missing_keys
       872   (5.4%)         872   (5.4%)     block in ActiveSupport::Inflector#apply_inflections
       935   (5.8%)         802   (5.0%)     ActiveSupport::SafeBuffer#safe_concat
       688   (4.3%)         688   (4.3%)     Temple::Utils#escape_html
       578   (3.6%)         578   (3.6%)     ActiveRecord::Attribute#initialize
...

つっつきボイス: 「derailed_benchmarksは結構使われている印象っすね」「作者はRichard Schneemanさんでした」

ベテランRubyistがPythonコードを5倍速くした話(翻訳)

今週の⭐を進呈いたします。おめでとうございます。

Stimulus: Turbolinksと相性のよい控えめなJSフレームワーク(Ruby Weeklyより)


stimulusjs/stimulusより

// hello_controller.js
import { Controller } from "stimulus"

export default class extends Controller {
  greet() {
    console.log(`Hello, ${this.name}!`)
  }

  get name() {
    return this.targets.find("name").value
  }
}

つっつきボイス: 「Stimulusって、確かDHHのBasecampがやってるやつですよね」「Turbolinksって最近オフにすること多いけど」

Railsを順を追ってアップグレードする(Awesome Rubyより)


つっつきボイス: 「冒頭の1. Stop the world、2. Long-lived upgrade branchとかまさに定番のアプローチ」「こうやって本番を二本立てにして↓リバースプロキシで振り分けながら少しずつ移行するというのもあるある: 検証が不十分なまま切り替えるとえらいことになったりするけど」


同記事より

ActiveRecordに欲しくなるEctoの機能(Awesome Rubyより)


infinum.coより

def registration_changeset(struct, params) do
  struct
  |> cast(params, [:email, :password])
  |> validate_required([:email, :password])
  |> unique_constraint(:email)
  |> put_password_hash()
end

def update_profile_changeset(struct, params) do
  struct
  |> cast(params, [:first_name, :last_name, :age])
  |> validate_required([:first_name, :last_name])
end

|>はElixirの「パイプライン演算子」だそうです。


つっつきボイス: 「Ectoって、ElixirのPhoenixフレームワークで使うやつか」「EctoはORMではない、って書いてますね」「この種のフレームワークを業務で使う動機は今のところ見えないなー」

参考: Rails使いがElixirのEctoを勉強した時のまとめ

Railsアプリの災害復旧プラン


engineyard.comより


つっつきボイス: 「disaster recovery planはRailsアプリに限らず重要っすね」「そういえば最近Engine Yardってひと頃ほど見かけない気がしてきた」

あるRails請負開発者の一日(Hacklinesより)

Planet Argonさん(@planetargon)がシェアした投稿


つっつきボイス: 「相当昔ですが、Oracle日本支社では犬飼ってるって話を思い出しました」「今のオフィス事情だと難しそう」

その他Rails小粒記事


つっつきボイス: 「JavaScriptのテスティングフレームワークというとmocha以外にJestもあるのね」
「最後のパーシャルレンダリング記事、でかいデータで素朴にeach回したら確かに遅い↓」

<% @users.each do |user| %>
    <%= render 'erb_partials/post', user: user %>
<% end %>

「こうやってcollection使う方が確実に速いけど、油断するとつい上みたいに書いちゃうことはあるな」「社内でもたまに見ますね」

<%= render partial: 'erb_partials/post', collection: @users, as: :user %>

参考: Railsガイド 3.4 パーシャルを使用する

Ruby trunkより

提案: Hash#transform_keys!recursive: trueオプション(継続)

config = MyAwesomeFormat.load(file); config.transform_keys!(recursive: true, &:to_sym)みたいに書きたいという主旨です。

def transform_keys!(recursive: false, &block)
  # do original transform_keys! here
  values.each do |v|
    if v.respond_to?(:each)
      v.each{|i| i.transform_keys!(recursive: true, &block) if i.respond_to?(:transform_keys!) }
    else v.respond_to?(:transform_keys!)
      v.transform_keys!(recursive: true, &block)
    end
  end if recursive
end

提案: GC速度と少々引き換えにメモリを削減(継続)


#14370より

Aaron Pattersonさんからのissueです。


つっつきボイス: 「最初にレス付けてるnormalpersonさんは昨年のRubyKaigiのあちこちで名前が出てきてた、普通じゃない人」「凄い名前w」

週刊Railsウォッチ(20170922)特集: RubyKaigi 2017セッションを振り返る(1)、Rails 4.2.10.rc1リリースほか

入れ違いで修正

Ruby

Ruby 2.5の陽の当たっていない新機能(Hacklinesより)

行カバレッジやブランチカバレッジ機能などを紹介しています。

Y -> 1: def hello(number)
Y -> 2:   if number == 1
Y -> 3:    'world'
Y -> 4:   else
N -> 5:     'mars'
Y -> 6:   end
Y -> 7: end
Y -> 8:
Y -> 9: hello(1)

つっつきボイス: 「この機能がRubyMineみたいなIDEと連携したらすごくうれしい」「名前忘れたけどこういうカバレッジのgemあった: 絶対に通過しないコードをあぶり出したりとかできる」

なお2.5のカバレッジについては以下でChangelogをざっくり訳してあります。

Ruby 2.5.0リリース!NEWSを読んでみた

Rubyの継承で動的に引数を渡す(RubyFlowより)

# 同記事より
class Render
  def self.engine; end

  def self.inherited(subclass)
    puts subclass        #=> Memo::Render
    puts subclass.engine #=> nil !!!
  end
end

つっつきボイス: 「Rubyってここまでエグいコードも書けるんだなって思いますね」「何でもアタッチできちゃうとか、ここまでくるともうオブジェクト指向言語というよりオブジェクト指向スクリプトみたいw」

Ruby 2.5のFrozenErrorクラス

2.5.0 :001 > NAME = 'Atul'.freeze
 => "Atul"
2.5.0 :002 > NAME << 'Joy'
Traceback (most recent call last):
        2: from /home/atul/.rvm/rubies/ruby-2.5.0/bin/irb:11:in `<main>'
        1: from (irb):2
FrozenError (can't modify frozen String)

つっつきボイス: 「今までRuntimeErrorだったのがFrozenErrorに変わるのはありがたい」「frozen_string_literalが完了するまでの混乱を少しでも軽くするためでもあるんでしょうね」

参考: frozen_string_literalが入って気づいた、メソッド設計の原則

Kernel.method_added


つっつきボイス:Kernel.method_addedなんてのがあるのか: 特定のメソッド追加にフックかける」「実体はModuleにあった↓」

# docs.ruby-lang.org/ja/2.5.0/method/Module/i/method_added.html
class Foo
  def Foo.method_added(name)
    puts "method \"#{name}\" was added"
  end

  def foo
  end
  define_method :bar, instance_method(:foo)
end

=> method "foo" was added
   method "bar" was added

参考: Module#method_added

aruba: CLIアプリをRSpecやMiniTestでテストするgem(Awesome Rubyより)


つっつきボイス: 「あのCucumberがやってるんですね」「バッチ処理のテストをRSpecとかで書けるし、Ruby以外に任意のCLIに使えるのがいいな」


app.cucumber.pro

Rubyのシンボル話その後

#14347はちょうど前回のウォッチで取り上げました。


つっつきボイス: 「あちこちで話題になってるやつですね」「途中まで読んでた」「やっとシンボルは文字列ではないということになったと」

Rubyのシンボルをなくせるか考えてみた(翻訳)

Rubyはやっぱり死んでない(Ruby Weeklyより)

こちらもEngine Yardのブログです。

ネストしたハッシュをlambdaでリファクタリング

同記事より
pub.doc[‘programs’].each &remove_icons(‘grades’, &remove_icons(‘units’))
def remove_icons value_key=nil, &block
  lambda do |key, value|
    next if key == ‘_order’
    value.delete ‘icons’
    if value_key
      value[value_key].each(&block) if block_given?
      value[value_key].each(&remove_icons) unless block_given?
    end
  end
end

つっつきボイス: 「うんうんよくあるやつ:再帰で書きましょう!みたいな」「Hashの再帰って何かと面倒ですよね」「せいぜい3階層ぐらいしか潜らないことをわかって書いてるのに、RuboCopに怒られたりとか」

高木さん

SQL

PostgreSQLにはmeltdownパッチは不要だが少し遅くなる(Postgres Weeklyより)


つっつきボイス: 「meltdownは基本的にカーネルの問題だからアプリにパッチが必要になることはそうないかと」「パッチで遅くなるのはもうしゃーない」「AWSもいろいろ言ってるけど対策すれば遅くなるっしょ」

参考: CPU脆弱性Meltdownのパッチ適用でベンチマークスコアが25%低下した

PostGraphile: PostgreSQLをGraphQL API化するJSライブラリ(Postgres Weeklyより)


graphile.orgより

以下を実行してhttp://localhost:5000/graphiqlをブラウザで開くといきなりGraphiqlが動きました。N+1クエリも克服しているそうです。これ凄いかも。

npm install -g postgraphile
postgraphile -c postgres://user:pass@host/dbname --schema schema_name

PGLogicalがアップデート(Postgres Weeklyより)

PostgreSQLの動作を知る(Postgres Weeklyより)

Pythonのツールを使います。


つっつきボイス: 「お、Internalとか書いてるけどこれはむしろ入門向け記事ですね: 量は多いけど相当やさしい内容」

JavaScript

(a ==1 && a== 2 && a==3)trueにする知見が続々

BPS社内で盛り上がりました。

// Stackoverflowより
var aᅠ = 1;
var a = 2;
var ᅠa = 3;
if(aᅠ==1 && a== 2 &&ᅠa==3) {
    console.log("Why hello there!")
}

ifに半角のハングル文字を使うという荒業を繰り出したり、C++などでやってみたりしています。

JavaScriptの二進木、再帰、末尾呼び出し最適化

nullとundefinedとは

CSS/HTML/フロントエンド

Web Componentsの秘密

参考: MDN Web Components

フロントエンドのエラー表示を再考する


logrocket.comより

blog.jxck.ioの新着記事


つっつきボイス: 「これは読んでおきたい記事」「そういえばこれと少し似た感じの、ネコのアイコンのブログ記事が話題になってましたね」「ネコのアイコン…?」「あったこれ↓」「あこの人か: アイコンとか全然気にしてなかったw」

参考: ソフトウェアの互換性と僕らのUser-Agent文字列問題

Screenshot-to-code-in-Keras: ニューラルネットワークでモック画像から静的HTMLページを生成(GitHub Trendingより)

アニメーションGIFが巨大すぎるのでここには貼りませんでした。


つっつきボイス: 「すげっ」「Bootstrapにも対応してるみたいですね」「一回こっきりの案件とかならかなりイケそう」「HTMLコーダー界に激震走るか」

その他

YAGNIを実践する


dev.to/gonedarkより


つっつきボイス: 「社内にもYAGNIを愛して止まない人がいるから彼を観察してるとだいたいわかりますよ」

参考: Wikipedia-ja YAGNI

Slackにprivate shared channel機能が追加


つっつきボイス: 「これありがたい: shared channelは前からあるけどpublicにしかできなかったんで」

Windows CLIの改善

WSLのchmod/chownの改良とtar/curlの追加です。

なお、こんなのもありました。

DOS窓の|は大丈夫だそうです。

Docker for macで/etc/localtimeがマウントできない問題

minio: Amazon S3 API互換のオブジェクトストレージサーバー


minio.ioより


つっつきボイス: 「S3互換のこういうのは他にもありますけどね」「GCPやAzureとかいろんなクラウドで使えるのはよさそう」「今さらですがオブジェクトストレージサーバーって何でしたっけ?」「AWS S3みたいなサービスがそれです: WebDAVみたいにRESTfulにオブジェクトにアクセスできるサービス」

HighwayHash: Go言語の爆速ハッシュ生成ライブラリ


つっつきボイス: 「10G/secとか確かに超速い」「ハッシュは速度だけあってもいかんので、ちゃんと分散してるかとかも大事ですね」

データがあれば使えるCloud AutoML VisionをGoogleが発表

一般のニュースにもなってますが一応。


つっつきボイス: 「今はデータサイエンティストやAIエンジニアが明らかに不足してるからどこもカスタマイズとかチューニングに手が回らなくて、こうやってそこそこのものを公開して好きに使ってくれ、みたいな方向に向かってる感じですね」「ユーザーに丸投げですか」

参考: Googleが「Cloud AutoML Vision」を発表、独自のデータセットを使ったカスタム機械学習モデルが簡単に構築できるように

番外

暗算術

a% of b = b% of aは初めて知りました。「25の16%=16の25%」みたいに使うそうです。


つっつきボイス: 「計算すると確かにそうなってるな: 式で見ると一瞬でわかるけど言われるまで気づきにくい」「英語圏なんで単位のフィート換算とかいらなさそうなのも多いです」「ひと頃入社試験でよく出されたフェルミ推定なんかやるときは、こういうのを何となくでも知っておかないと手も足も出なかったりしますね」

参考: 暗記しておくとなにかと便利なプチ公式まとめ

これは欲しい


つっつきボイス: 「Nintendo Laboとどっちが子どもにウケるかなと思って」「今ならポプテピピックっしょw」「あれはもう子どもの反応が面白すぎますね」

学習/プログラミング不要の産業ロボット


つっつきボイス: 「荷物の積み下ろしとかまでやってます」「人間雇う方がまだまだ安いな、今のところは」

Switchエミュレータ(GitHub Trendingより)


つっつきボイス: 「ソフトはともかくハードウェアはそうもいかないか」

その後、GPL V2というライセンスの厳しさや、パチンコの当たり判定システムの話題で盛り上がりました。

ルーシーさん

自己修復コンクリート


今週は以上です。

バックナンバー(2017年後半)

週刊Railsウォッチ(20180112)update_attributeが修正、ぼっち演算子`&.`は`Object#try`より高速、今年のRubyカンファレンス情報ほか

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

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

Rails公式ニュース

Ruby Weekly

Awesome Ruby

RubyFlow

160928_1638_XvIP4h

Hacklines

Hacklines

Postgres Weekly

postgres_weekly_banner

Frontend Weekly

frontendweekly_banner_captured

JSer.info

jser.info_logo_captured

Github Trending

160928_1701_Q9dJIU

Ruby: Kernel#itselfの美学(翻訳)

$
0
0

概要

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

Ruby: Kernel#itselfの美学(翻訳)

最近私は、Rubyでよくある問題を解決しました。コレクションで渡される項目ごとの頻度をカウントするという問題です。この問題の解決方法はいくつか考えられます。最初は以下のようにEnumerable#injectまたはEnumerable#each_with_objectを用い、空のハッシュをアキュムレータの値として使いました。

collection.each_with_object({}) { |item, accum| accum[item] = accum[item].to_i + 1 }

ハッシュのデフォルト値を用いて、もう少しスマートな方法に変えました。

collection.each_with_object(Hash.new(0)) { |item, accum| accum[item] = accum[item] + 1 }

これはこれでなかなかいいのですが、これよりずっと美しい方法があります。

Rubyの美学

この問題を解決する興味深い方法のひとつは、Enumerable#group_byを使う方法です。単に要素をそれら自身でグループ化し、各項目の頻度をカウントします。以下は実装方法の1つです。

collection.group_by { |item| item }.map { |key, value| [key, value.count] }.to_h

しかしながら、特にRubyの標準から見てもう少し何とかできそうな気がします。そしてもっといい方法に気づきました。Ruby 2.4ではActiveSupportのcore extensionの非常に便利なHash#transform_valuesが採り入れられました。これを応用すれば、次のように書き換えることができます。

collection.group_by { |item| item }.transform_values(&:count)

ずいぶんよくなりましたが、group_by { |item| item }のあたりをもう少し何とかできそうです。こういう場合に便利な道具がRubyにあるでしょうか?

そう、あるのです!Ruby 2.2で導入されたKernel#itselfは、単にそれ自身を返します。これを使うというアイデアは一見奇妙に思われるかもしれませんが、これがどんぴしゃりはまるのです。

collection.group_by(&:itself).transform_values(&:count)

コードがここまで美しくなりました。

まとめ

Rubyコードは気持ちよく読めることで特に知られています。そして私はRubyとこれほど長い間付き合っていても、Kernel#itselfのようにRubyという言語にトータルな美しさを付け加えるささやかなものを発見するたびに、言い知れない喜びに浸ります。

関連記事

Rubyのシンボルをなくせるか考えてみた(翻訳)

Rubyのクラスメソッドをclass << selfで定義している理由(翻訳)


Rails: DockerでHeroku的なデプロイソリューションを構築する: 後編(翻訳)

$
0
0

前記事: Rails: DockerでHeroku的なデプロイソリューションを構築する: 前編(翻訳)

概要

原著者の許諾を得て、CC BY-NC-SAライセンスに基づき翻訳・公開いたします。

Rails: DockerでHeroku的なデプロイソリューションを構築する: 後編(翻訳)

Herokuライクなデプロイソリューションの構築方法を解説します。
特定のクラウドプロバイダや、Dockerに関連しないツールを必要としません。

ここまでのまとめ

これで自動デプロイの構築に必要な材料がすべて揃いました。以下は最終的なコードであり、deployer.rbという名前でrootフォルダに保存できます。各行の動作を見てみましょう。

# deployer.rb
class Deployer
  APPLICATION_HOST = '54.173.63.18'.freeze
  HOST_USER = 'remoteuser'.freeze
  APPLICATION_CONTAINER = 'mydockeruser/application-container'.freeze
  APPLICATION_FILE = 'application.tar.gz'.freeze
  ALLOWED_ACTIONS = %w(deploy).freeze
  APPLICATION_PATH = 'blog'.freeze

  def initialize(action)
    @action = action
    abort('Invalid action.') unless ALLOWED_ACTIONS.include? @action
  end

  def execute!
    public_send(@action)
  end

  def deploy
    check_changed_files
    copy_gemfile
    compress_application
    build_application_container
    push_container
    remote_deploy
  end

  private

  def check_changed_files
    return unless `git -C #{APPLICATION_PATH} status --short | wc -l`
                  .to_i.positive?
    abort('Files changed, please commit before deploying.')
  end

  def copy_gemfile
    system("cp #{APPLICATION_PATH}/Gemfile* .")
  end

  def compress_application
    system("tar -zcf #{APPLICATION_FILE} #{APPLICATION_PATH}")
  end

  def build_application_container
    system("docker build -t #{APPLICATION_CONTAINER}:#{current_git_rev} .")
  end

  def push_container
    system("docker push #{APPLICATION_CONTAINER}:#{current_git_rev}")
  end

  def remote_deploy
    system("#{ssh_command} docker pull "\
           "#{APPLICATION_CONTAINER}:#{current_git_rev}")
    system("#{ssh_command} 'docker stop \$(docker ps -q)'")
    system("#{ssh_command} docker run "\
             "--name #{deploy_user} "\
             "#{APPLICATION_CONTAINER}:#{current_git_rev}")
  end

  def current_git_rev
    `git -C #{APPLICATION_PATH} rev-parse --short HEAD`.strip
  end

  def ssh_command
    "ssh #{HOST_USER}@#{APPLICATION_HOST}"
  end

  def git_user
    `git config user.email`.split('@').first
  end

  def deploy_user
    user = git_user
    timestamp = Time.now.utc.strftime('%d.%m.%y_%H.%M.%S')
    "#{user}-#{timestamp}"
  end
end

if ARGV.empty?
  abort("Please inform action: \n\s- deploy")
end
application = Deployer.new(ARGV[0])

begin
  application.execute!
rescue Interrupt
  puts "\nDeploy aborted."
end

それでは1つずつ手順を追ってみましょう。

  APPLICATION_HOST = '54.173.63.18'.freeze
  HOST_USER = 'remoteuser'.freeze
  APPLICATION_CONTAINER = 'mydockeruser/application-container'.freeze
  APPLICATION_FILE = 'application.tar.gz'.freeze
  ALLOWED_ACTIONS = %w(deploy).freeze
  APPLICATION_PATH = 'blog'.freeze

ここでは値の重複を避けるためにいくつかの定数をコードで定義しています。APPLICATION_HOSTは実行するサーバーのリモートIPアドレス、HOST_USERはリモートサーバーのユーザー名、APPLICATION_CONTAINERはアプリをラップするコンテナの名前です。APPLICATION_FILEは圧縮したアプリのファイル名なので名前は自由に変えられます。ALLOWED_ACTIONSは許可する操作の配列であり、どの操作を利用可能にするかを簡単に定義できます。最後のAPPLICATION_PATHはアプリへのパスです。今回の例ではblogとしています。

  def initialize(action)
    @action = action
    abort('Invalid action.') unless ALLOWED_ACTIONS.include? @action
  end

  def execute!
    public_send(@action)
  end

上は、(ALLOWED_ACTIONSで)利用できる各メソッドのバリデーションと呼び出しを行うラッパーです。これを用いることで、コードをリファクタリングする必要なしに、呼び出し可能な新しいメソッドを簡単に追加できます。

  def deploy
    check_changed_files
    copy_gemfile
    compress_application
    build_application_container
    push_container
    remote_deploy
  end

上はデプロイ手順です。これらのメソッドは先の例とほぼ同じですが、わずかな変更があります。それぞれの手順を見てみましょう。

  def check_changed_files
    return unless `git -C #{APPLICATION_PATH} status --short | wc -l`
                  .to_i.positive?
    abort('Files changed, please commit before deploying.')
  end

アプリのデプロイにはローカルのコードを使っているので、ファイルが変更されているかどうかをチェックして、変更がある場合はデプロイを行わないようにするのがよい方法です。この手順ではファイルの作成や変更を検出するのにgit status --shortを使っています。-Cフラグはgitでチェックする対象(この例ではblog)を定義します。不要ならこの手順を取り除くこともできますが、おすすめしません。

  def copy_gemfile
    system("cp #{APPLICATION_PATH}/Gemfile* .")
  end

上は、デプロイのたびにblogのルートディレクトリにあるGemfileとGemfile.lockをコピーします。これによって、デプロイが完了する前にすべてのgemがインストールされるようになります。

  def compress_application
    system("tar -zcf #{APPLICATION_FILE} #{APPLICATION_PATH}")
  end

メソッド名からわかるとおり、この手順ではアプリ全体を圧縮して1つのファイルにします。このファイルは後でコンテナに含められます。

  def build_application_container
    system("docker build -t #{APPLICATION_CONTAINER}:#{current_git_rev} .")
  end

このメソッドは、コンテナのビルド手順を実行します。このときに依存ライブラリやgemをすべてインストールします。Gemfileが変更されるたびにDockerでそのことが検出されてインストールが行われるので、依存ライブラリの更新を気にする必要はありません。依存ライブラリが変更されるたびに多少時間がかかります。変更が何もない場合、Dockerはキャッシュを使うので手順の実行はほぼ瞬時に完了します。

  def push_container
    system("docker push #{APPLICATION_CONTAINER}:#{current_git_rev}")
  end

このメソッドは、Docker Registryに新しいコンテナをアップロードします。最新のコミットハッシュをgitで取得しているこのcurrent_git_revメソッドにご注目ください。各デプロイの識別にはこのコミットハッシュを使います。アップロードしたコンテナはすべてDockerHubコンソールで確認できます。

  def remote_deploy
    system("#{ssh_command} docker pull "\
           "#{APPLICATION_CONTAINER}:#{current_git_rev}")
    system("#{ssh_command} 'docker stop \$(docker ps -q)'")
    system("#{ssh_command} docker run "\
             "--name #{deploy_user} "\
             "#{APPLICATION_CONTAINER}:#{current_git_rev}")
  end

ここでは以下の3つを行っています。

  • docker pull: リモートサーバーにアップロードしたコンテナをpullします。ssh_commandメソッド呼び出しは、リモートコマンドの送信が必要になるたびに、コードの重複を避けるための単なるラッパーです。
  • docker stop $(docker ps -q): 新しいコンテナを実行するときにポート番号が衝突しないようにするため、実行中のコンテナをすべて停止します。
  • docker run: 正しいタグを与えて新しいコンテナを起動し、現在のgitユーザーとタイムスタンプに基づいて名前を付けます。これは、現在実行中のアプリをデプロイしたユーザーを知る必要がある場合に便利です。名前を確認するには、リモートサーバーでdocker psコマンドを入力します。
CONTAINER ID        IMAGE                                        COMMAND                  CREATED             STATUS              PORTS                    NAMES
01d777ef8d9a        mydockeruser/application-container:aa2da7a   "/bin/sh -c 'cd /t..."   10 minutes ago      Up 10 minutes       0.0.0.0:3000->3000/tcp   mygituser-29.03.17_01.09.43
if ARGV.empty?
  abort("Please inform action: \n\s- deploy")
end
application = Deployer.new(ARGV[0])

begin
  application.execute!
rescue Interrupt
  puts "\nDeploy aborted."
end

上はCLIから引数を受け取って、アプリのデプロイを実行します。Ctrl-Cでデプロイをキャンセルすると、rescueブロックでわかりやすいメッセージが表示されます。

アプリをデプロイする

この時点でのフォルダ構造は次のようになっているはずです。

.
├── blog
│   ├── app
│   ├── bin
... (application files and folders)
├── deployer.rb
├── Dockerfile

次は、アプリを実行してデプロイしましょう。

$ ruby deployer.rb deploy

コマンドが実行されるたびに出力が表示されます。すべての出力結果は、最初の例の手動実行とほぼ同じです。

Sending build context to Docker daemon 4.846 MB
Step 1/9 : FROM ruby:2.3.1-slim
 ---> e523958caea8
Step 2/9 : COPY Gemfile* /tmp/
 ---> Using cache
 ---> f103f7b71338
Step 3/9 : WORKDIR /tmp
 ---> Using cache
 ---> f268a864efbc
Step 4/9 : RUN gem install bundler &&     apt-get update &&     apt-get install -y build-essential libsqlite3-dev rsync nodejs &&     bundle install --path vendor/bundle
 ---> Using cache
 ---> 7e9c77e52f81
Step 5/9 : RUN mkdir -p /app/vendor/bundle
 ---> Using cache
 ---> 1387419ca6ba
Step 6/9 : WORKDIR /app
 ---> Using cache
 ---> 9741744560e2
Step 7/9 : RUN cp -R /tmp/vendor/bundle vendor
 ---> Using cache
 ---> 5467eeb53bd2
Step 8/9 : COPY application.tar.gz /tmp
 ---> b2d26619a73c
Removing intermediate container 9835c63b601b
Step 9/9 : CMD cd /tmp &&     tar -xzf application.tar.gz &&     rsync -a blog/ /app/ &&     cd /app &&     RAILS_ENV=production bundle exec rake db:migrate &&     RAILS_ENV=production bundle exec rails s -b 0.0.0.0 -p 3000
 ---> Running in 8fafe2f238f1
 ---> c0617746e751
Removing intermediate container 8fafe2f238f1
Successfully built c0617746e751
The push refers to a repository [docker.io/mydockeruser/application-container]
e529b1dc4234: Pushed
08ee50f4f8a7: Layer already exists
33e5788c35de: Layer already exists
c3d75a5c9ca1: Layer already exists
0f94183c9ed2: Layer already exists
b58339e538fb: Layer already exists
317a9fa46c5b: Layer already exists
a9bb4f79499d: Layer already exists
9c81988c760c: Layer already exists
c5ad82f84119: Layer already exists
fe4c16cbf7a4: Layer already exists
aa2da7a: digest: sha256:a9a8f9ebefcaa6d0e0c2aae257500eae5d681d7ea1496a556a32fc1a819f5623 size: 2627
aa2da7a: Pulling from mydockeruser/application-container
1fad42e8a0d9: Already exists
5eb735ae5425: Already exists
b37dcb8e3fe1: Already exists
50b76574ab33: Already exists
c87fdbefd3da: Already exists
f1fe764fd274: Already exists
6c419839fcb6: Already exists
4abc761a27e6: Already exists
267a4512fe4a: Already exists
18d5fb7b0056: Already exists
219eee0abfef: Pulling fs layer
219eee0abfef: Verifying Checksum
219eee0abfef: Download complete
219eee0abfef: Pull complete
Digest: sha256:a9a8f9ebefcaa6d0e0c2aae257500eae5d681d7ea1496a556a32fc1a819f5623
Status: Downloaded newer image for mydockeruser/application-container:aa2da7a
01d777ef8d9a
c3ecfc9a06701551f31641e4ece78156d4d90fcdaeb6141bf6367b3428a2c46f

出力結果は、ハッシュやDockerキャッシュの違いによって異なることがあります。最後に、上のように2つのハッシュが出力されます。

01d777ef8d9a
c3ecfc9a06701551f31641e4ece78156d4d90fcdaeb6141bf6367b3428a2c46f

1つ目の短いハッシュは、停止したコンテナのハッシュです。最後の長いハッシュは、新たに実行中のコンテナのハッシュです。

これで、リモートサーバーのIPアドレスにアクセスするとアプリが実行されていることを確認できます。

Semaphoreで継続的デリバリー(CD)する

本チュートリアルのスクリプトを使って、アプリをSemaphoreに自動デプロイできます。やり方を見てみましょう。

最初に、「Project Settings」でDocker support付きのプラットフォームを指定します。

SemaphoreのProjectページで、「Set Up Deployment」をクリックします。

「Generic Deployment」を選択します。

「Automatic」を選択します。

Gitのブランチを選択します(普通はmaster)。

ここではアプリをデプロイしたいだけなので、ローカルコンピュータで実行するときと同じ方法でデプロイスクリプトを実行します。

rbenv global 2.3.1
docker-cache restore
ruby deployer.rb deploy
docker-cache snapshot

2つのdocker-cacheコマンドにご注目ください。これらがビルドしたイメージを取り出しを行うので、ゼロからビルドする必要はありません。ローカルでの実行と同様、最初は少し時間がかかりますが、次回からは速くなります。詳しくはSemaphoreの公式ドキュメントをご覧ください。

また、rbenv global 2.3.1コマンドをメモしておきましょう。これは、スクリプトの実行に必要な現在のRubyのバージョンを設定するためのものです。別の言語を使う場合は、必要な環境を設定する必要があります。

次の手順では、リモートサーバーへのアクセスに使うSSHキーのアップロード(必要な場合)と、新しいサーバーへの名前付けを行っています。完了すると、コードをmasterブランチにpushするたびにこのスクリプトが実行され、定義済みのリモートサーバーにアプリがデプロイされます。

その他の自動化可能なコマンド

この後のセクションでは、便利な自動化コマンドをいくつかご紹介します。

現在のバージョン

現在実行中のアプリのバージョンをトラックするには、コンテナのTagに情報を記述します。

現在実行中のバージョンを取り出すには、以下のコードが必要です。

def current
  remote_revision = `#{ssh_command} docker ps | grep -v CONTAINER | awk '{print $2}' | rev | cut -d: -f1 | rev`.strip

  abort('No running application.') if remote_revision == ''

  current_rev = `git show --ignore-missing --pretty=format:'%C(yellow)%h\
%C(blue)<<%an>> %C(green)%ad%C(yellow)%d%Creset %s %Creset'\
#{running_revision} | head -1`.strip
  if current_rev.empty?
    puts 'Local revision not found, please update your master branch.'
  else
    puts current_rev
  end
  deploy_by = `#{ssh_command} docker ps --format={{.Names}}`
  puts "Deploy by: #{deploy_by}"
end

各行の動作について解説します。

remote_revision = `#{ssh_command} docker ps | grep -v CONTAINER | awk '{print $2}' | rev | cut -d: -f1 | rev`.strip

上のコマンドは以下を行います。

  • docker psでリモートコンテナのステータス出力を取得
  • grep -v CONTAINERで出力からヘッダを除去
  • awk '{print $2}'で2番目のカラム(image name:tag)を取得
  • 残りのコマンドでimage nameと:を削除し、残りの部分とコミットハッシュを返す
  • 返された文字列の最終行の改行を.stripで削除
abort('No running application.') if remote_revision == ''

コンテナが1つも実行されていない場合や、コミットが1つも見つからない場合はコマンド実行をやめます。

current_rev = `git show --ignore-missing --pretty=format:'%C(yellow)%h\
%C(blue)<<%an>> %C(green)%ad%C(yellow)%d%Creset %s %Creset'\
#{running_revision} | head -1`.strip

このコマンドは、git logにマッチするコンテナハッシュを検索して書式を整えます。

if current_rev.empty?
   puts 'Local revision not found, please update your master branch.'
else
  puts current_rev
end

このコミットが現在のgit historyにない場合、ユーザーにリポジトリの更新を促します。これは、新しいコミットがローカルコピーからまだrebaseされていない場合に発生することがあります。コミットがある場合は、ログ情報を出力します。

deploy_by = `#{ssh_command} docker ps --format={{.Names}}`

このコマンドは、現在実行中のコンテナ名を返します。コンテナ名にはユーザー名とタイムスタンプが含まれます。

puts "Deploy by: #{deploy_by}"

上のコマンドは、デプロイを行ったユーザーとタイムスタンプを出力します。

ログ

多くのアプリはログを出力するので、場合によってはログの面倒も見なければなりません。Dockerに組み込まれているログシステムを使うと、シンプルなSSH接続でアプリのログに簡単にアクセスできるようになります。

アプリからログを出力するには、以下を入力します。

def logs
  puts 'Connecting to remote host'
  system("#{ssh_command} 'docker logs -f --tail 100 \$(docker ps -q)'")
end

docker logsコマンドは、アプリで生成されたログをすべて出力します。-fフラグは、接続を保持してすべてのログをストリームとして読み出せるようにします。--tailフラグは、出力する古いログの最大行数を指定します。最後の$(docker ps -q)は、リモートホストで実行中のコンテナごとにIDを返します。今はアプリを実行しているだけなので、コンテナをすべて取り出しても問題ありません。

メモ: 本記事のサンプルアプリはすべてのログをファイルに書き込むので、Dockerにはログを一切出力しません。この振る舞いは、アプリの起動時にRAILS_LOG_TO_STDOUT=true環境変数で変更できます。

Dockerのインストールとログイン

新しいホストでは、必要なインストールや設定をsetupコマンド一発でできるようにすると便利です。

インストールとログインの2つの手順を完了させます。

def docker_setup
  puts 'Installing Docker on remote host'
  system("#{ssh_command} -t 'wget -qO- https://get.docker.com/ | sh'")

  puts 'Adding the remote user to Docker group'
  system("#{ssh_command} 'sudo usermod -aG docker #{HOST_USER}'")

  puts 'Adding the remote user to Docker group'
  system("#{ssh_command} -t 'docker login}'")
end

各コマンドの動作について解説します。

system("#{ssh_command} -t 'wget -qO- https://get.docker.com/ | sh'")

このコマンドはDockerのインストールスクリプトを実行します。リモートユーザーのパスワード入力を促すには-tフラグが必要です。パスワード入力を求められたら入力します。

system("#{ssh_command} 'sudo usermod -aG docker #{HOST_USER}'")

このコマンドは、Dockerグループにリモートユーザーを追加します。これは、sudoせずにdockerコマンドを実行する場合に必要です。

system("#{ssh_command} -t 'docker login'")

更新されたアプリをダウンロードするためにログインが必要なので、このコマンドが必要になります。-tフラグは、ログイン入力できるようにするためのものです。

ロールバック

新しいアプリの実行で何か問題が起きたら、直前のバージョンにいつでもロールバックできることが重要です。Dockerコンテナのアプローチを用いたことで、デプロイされたすべてのバージョンがホスト上に保存されているので、即座にロールバックを開始できます。

次のコードスニペットをご覧ください。

def rollback
  puts 'Fetching last revision from remote server.'
  previous_revision = `#{ssh_command} docker images | grep -v 'none\|latest\|REPOSITORY' | awk '{print $2}' | sed -n 2p`.strip
  abort('No previous revision found.') if previous_revision == ''
  puts "Previous revision found: #{previous_revision}"
  puts "Restarting application!"
  system("#{ssh_command} 'docker stop \$(docker ps -q)'")
  system("#{ssh_command} docker run --name #{deploy_user} #{APPLICATION_CONTAINER}:#{previous_revision}")
end

各手順の動作について見てみましょう。

  puts 'Fetching last revision from remote server.'
  previous_revision = `#{ssh_command} docker images | grep -v 'none\|latest\|REPOSITORY' | awk '{print $2}' | sed -n 2p`.strip
  abort('No previous revision found.') if previous_revision == ''

このコマンドは、リモートホスト上にあるすべてのDockerイメージの中から直前のコンテナtagをgrepします。このタグはgitコミットの短いハッシュになっていて、アプリのロールバックを参照するときに使われます。直前のDockerイメージがない場合は、ロールバックをやめます。

  system("#{ssh_command} 'docker stop \$(docker ps -q)'")

このコマンドは、実行中のコンテナをすべてシャットダウンして、直前のコンテナを起動できるようにします。

  system("#{ssh_command} docker run --name #{deploy_user} #{APPLICATION_CONTAINER}:#{previous_revision}")

このコマンドは、直前の手順で見つかったタグを用いてアプリを起動します。デプロイメソッド(deploy_user)で使われているのと同じ命名ルールを利用できます。

まとめ

本チュートリアルのすべての手順を行うと、ソフトウェアをデプロイする自動ツールが完全に動くようになるはずです。このツールは、アプリを簡単にデプロイできなければならないが、Herokuなどの自動化された環境にホスティングできない場合に便利です。

このツールが有用だとお思いいただけましたら、お気軽に本チュートリアルを共有してください。疑問点などがございましたら、ぜひ元記事にコメントをどうぞ。

皆さまが楽しくリリースできますように。

追伸: Dockerを用いた継続的デリバリー(CD)にご関心がおありでしたら、SemaphoreのDocker platformをぜひチェックしてください。タグ付きのDockerイメージのレイヤキャッシュを完全にサポートしています。

関連記事

Rails: DockerでHeroku的なデプロイソリューションを構築する: 前編(翻訳)

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

Railsアプリのアセットプリコンパイルを高速化するコツ(翻訳)

$
0
0

概要

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

Railsアプリのアセットプリコンパイルを高速化するコツ(翻訳)

Rails開発者なら誰でも、アセットの読み込みに非常に時間がかかることを痛感しています。アセット読み込みを高速化する便利なコツをご紹介します。

コツ

  • 1つの巨大なファイルに何もかもバンドルしないこと。CDNを用いて、必要なページでだけrequireしましょう(CDNはCKeditorなどのツールでも使われています)。
  • I18n-jsは翻訳を個別のファイルに保持します。必要な言語だけを設定するようにしましょう。さもないと、新しいライブラリを導入するたびに翻訳ファイルがあっという間に肥大化してしまいます。必要な言語だけを設定することでコンパイル済みファイルの肥大化を回避でき、ひいてはコンパイルとページ読み込みを高速化できます。
config.i18n.available_locales = %i(en)
  • therubyracerGemfileから削除しましょう。このgemはメモリを大量に消費するためです。代わりに、最新バージョンのNodeをインストールします。
  • SASSファイルやSCSSファイルでrequirerequire_treerequire_selfを使わないこと。これらはあまりに素朴であり、Sassファイルでうまく動作しません。代わりに、Sassネイティブの@importディレクティブを使いましょう。sass-railsはこのディレクティブをカスタマイズしてRailsプロジェクトの慣習に統合します。
  • SASSファイルやSCSSファイルでの@importディレクティブの使い方には注意が必要です。@import 'compass/css3/flexbox'のように個別にインポートできる状況であれば、@import 'compass';のようにパッケージアセットをまるごとインポートするのは避けましょう。
  • JavaScriptやCoffeeScriptのマニフェストでrequire_tree .を使うのは避けましょう。次のように、アプリの管理(admin)パネルが独自のアセットを持つ場合を考えてみます。
//** assets/javascripts/admin/admin.js

//= require admin/tab.js
//= ...

//** assets/javascripts/application.js

//= require 'something'
//= require_tree . // 悪手: adminのアセットまでrequireされてしまう
  • アセットをコンパイルするときのログをチェックしましょう。デフォルトのロガーをオーバーライドしてDEBUGモードでアクセスしたときの状態を確認するのは簡単です。
# /lib/tasks/assets.rake

require 'sprockets/rails/task'

Sprockets::Rails::Task.new(Rails.application) do |t|
  t.logger = Logger.new(STDOUT)
end

まとめ

上のルールはコンパイル済みアセットを高速化するのに有用です。この概念を実証するcintrzyk/sprockets-tipsをご覧いただければ、Railsのサンプルアプリでより詳細な部分を確認できます。

Rails 5.1以降、アセットの管理方法は多様になりました。私はYarnとWebpackによる管理を強くおすすめします。その有用性については、ぜひ私を信じてください。アセットの依存性管理、効率の高いコンパイルプロセス、ページを更新せずにコードを動的に再読み込みする機能、ES6サポート、PostCSSなど、多くのメリットを得られます。

関連記事

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

Ruby 3 JITの最新情報: 現状と今後(翻訳)

$
0
0

概要

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

画像は英語記事からの引用です。

Ruby 3 JITの最新情報: 現状と今後(翻訳)

Ruby 3にJITが導入されるというニュースは既にお聞きおよびかと思います。JITはどこからやってくるのでしょうか?いつ導入されるのでしょうか?それでどれだけ高速化されるのでしょうか?JITがあるとデバッグが心配な場合にオフにできるのでしょうか?

そもそもJITって何?

JITは「just-in-time」の略ですが、日常会話の「間に合ったー!」「滑り込みセーフ」の意味でなければ、特に実行時コンパイラ(Just-In-Time Compiler)を指します。現在のRubyはコードを逐次解釈して実行するインタプリタですが、JITを用いると、Rubyプログラムの一部をマシン語に変換して、Unixコマンドやexeファイルのように実行します。特に、JITは私たちが日頃読んでいるRubyコードを、プロセッサに合わせて最も自然かつ高速なコード(しばしば機械語やマシン語(”machine code” or “machine language”)などと呼ばれます)に変換します。

JITは「普通の」コンパイラといくつかの点で異なっています。最大の違いは、プログラム全体をコンパイルするわけではない点です。その代わり、最も頻繁に実行される部分だけをコンパイルし、そのプログラムでの使われ方にうまく合わせて最速で実行できるようにコンパイルします。JITは、プログラムでメソッドがどのように呼ばれているかについて推測する必要はありません。プログラムをしばらく監視して情報をメモし、それからコンパイルを開始します。

うう、JITのせいでこの言い訳が使えなくなってしまいました。今後の言い訳には「AoT設定をデバッグ中」とか「ETLスクリプトを実行中」がおすすめです。この手もまだ使えます。

「プログラマーが合法的におさぼりする言い訳No.1:『コンパイル中です』」
「おらっ!仕事中だぞ」「コンパイル中でーす」「さよか」
うう、JITのせいでこの言い訳が使えなくなってしまいました。
今後の言い訳には「AoT設定をデバッグ中」とか「ETLスクリプトを実行中」が当分おすすめです。

JITでどれだけ速くなるか

この世には「嘘」、「ひどい嘘」、そして「ベンチマーク」があります(訳注: 元ネタ)。JITによる高速化を正確な値で表すなど無理な相談です。正確な値など存在しないからです。しかし、特定のプログラムについては、完全に合理的な負荷をかけた状態で50%、150%、あるいは250%までJITで高速化できるケースが多々あります。現実的な負荷の元で、500%以上もの高速化を達成するケースすらいくつかあります。ただし、JITよりインタプリタの方が速いケースも若干あることは言うまでもありません。なぜなら、現実世界には常に最適化されているものなど存在しないからです。

現時点での無難かつシンプルなCRuby向けJIT実装では、およそ30%〜50%のパフォーマンス向上が見られ、測定方法次第では最大150%に達することもあります。30%〜50%はJITにしてはかなり控えめな値ですが、これらのブランチはまだまだシンプルですし、30%〜50%という値は過小評価のしようがありません。これは3年分から10年分の「通常」リリースに相当するスピードアップであり、わずか1〜2年のJITへの取り組みでこれほどの成果を達成したのです。そして通常のスピードアップに加えて、こうした大きなスピードアップは現在も起き続けています。さらにJITは長期に渡って改良を重ねることができます。JITは、昔ながらの「純粋なインタプリタ」のRubyでは到底不可能だった最適化の世界へと大きく扉を開いているのです。それこそが、JITを搭載したRuby実装が既に劇的な高速化の可能性を秘めている理由です。TruffleRubyなどの実装は著しいメモリオーバーヘッドを伴いますが、コードを900%あるいはそれ以上スピードアップできます。この戦略はCRubyには合いませんが、それでも高速化は確かに可能なのです。

私は、「どんだけ速くなる?」という質問にはたいていRails Ruby Benchの結果を添えて回答しています(結局私の作ったgemではありますが)。しかし現時点のMJITは、大規模な並列実行を行うRailsアプリを実行できるほどには安定していません。ご心配なく、そのときが来れば結果を公表いたします。

これは直近の値ではありません。かつMJITやYARV-MJITの値は今も激しく意味深な変動を繰り返しています。しばらくお待ちください。

これは直近の値ではありません。かつMJITやYARV-MJITの値は今も激しく意味深な変動を繰り返しています。しばらくお待ちください。

CRubyのJITはどこから来たのか

RubyのJITは、しばらく前からある形を取って導入されてきました。JRubyには何年も前からJITがありますし、RubiniusにもしばらくJITがありましたがその後取り除かれました。しかし「純粋な」CRubyにJITが組み込まれたことはかつてありませんでした。その代わりさまざまな実験用ブランチとしてJITが姿を表しましたが、Rubyリリースに取り入れられたことはなかったのです。

Shyouhei Urabeによる”deoptimization”ブランチはかなりよかったのですが、大きな成功には至りませんでした。このJITは非常に純粋かつ非常にシンプルであり、可能な最適化はごくわずかにとどまりましたが、その代わり余分なメモリをほとんど必要としないことが保証されていました。Rubyコアチームはメモリ使用量にとても気を遣っています。

その後、最近になってVladimir MakarovRuby 2.4のハッシュテーブルを再構築したあのMakarovです)が、メモリを食わない強力なJIT実装「MJIT」を作りました。MJITは既存のCコンパイラ(GCCやCLang)をRuby JIT向けに強化します。MakarovがMJITの動作を解説するキーノートスピーチのためにRubyKaigiに招かれたのも、MJITへの期待の大きさゆえです。Makarovは最初にRubyを改造して、スタックベースのVMではなくレジスタベースのVMを用いるようにし、その基礎の上にJITを構築しています。しかしMJITはまだ歴史が浅く、一般的にリリースできるほどには安定していません。どんなRubyプログラムでも動かせる、クラッシュしないRubyを作るのは困難であり、MJITはまだその段階にありません。しかし最近の結果によるとMJITのCPUベンチマークはRuby 2.0.0の230%にも達しているので、大筋では間違っていないことは確かです。

MJITの登場とちょうど同じ頃、Takashi KokubunはEvan Phoenixの初期の成果にヒントを得てLLVMベースの強力なRuby JIT実装「LLRB」を作っていました。LLRBもまた、MJITと同様、Ruby世界を支えるほどには洗練されていませんでしたが、TakashiはMJITから多くの成果を取り入れて開発を続け、YARV-MJITに結実しました。

YARV-MJITは、レジスタベースVMとなるためにMJITの変更点を取り入れました。この変更を反映すれば、Rubyはさらに高速になる代わりに、すべてを安定させるために必要なテストも増やさなければなりません。変更をやめておけば、Ruby JITの機能が少なくなる代わりにリリースを早められます。皆が望んでいるのは、できるだけ小規模な機能かつできるだけ早期のリリースであることを覚えていますか?YARV-MJITはまさにそれを実践する原則です。「JITを単に追加してみてはどうか」「さほど高速にならなくても構わないからJITを追加してみてはどうか」「JITをデフォルトでオフにして、欲しい人だけがこの新しい実験的機能を使うということにしてはどうか」という具合にです。しかしこのJITは、一部の機能をオフにしたMJITと同じものです。

JITはいつ使えるのか

言うまでもなくこれは難しい質問です。どんな問題が見つかり、どれだけ修正が容易かでリリース時期は変わってくるでしょう。

YARV-MJITのissue #1782が現在オープンになっているので、Rubyへの導入は秒読み状態なのかもしれませんが、Ruby 2.5.0のクリスマスリリースに含まれることはありません。そしてそれがベストです。

YARV-MJITもMJITも着々と改良を重ねています。VladはMJITが本当に成熟するには1年ほどかかるだろうと見込んでいます。しかしYARV-MJITによって通常のRubyリリースに取り込まれるJITが完璧な状態である必要はありません。JITを有効にするよう指定すればオンになります。

狭い意味ではいつリリースされてもおかしくありません。しかし(JITが)デフォルトでオンの状態でリリースされるにはおそらく1年、またはそれ以上はかかるでしょう。イミュータブルな文字列のときと同様、Rubyはさらに多くの新機能をオプトインとして取り込んでいます。これはFeature Toggles(Feature FlagsやFeature Flippersとも呼ばれます)に近い方法で、新機能が完全な状態でなくてもインクルードできますが、その場合は新機能同士が衝突しないことの確認が必要です。私はこのアプローチについて、Ruby 1.8から1.9へ移行したときの方法よりずっと好ましいと思います。

JITの導入をいち早く知る方法/JITをオフにする方法

YARV-MJITがいつRubyに導入されるかを知りたい方は、上のプルリク#1782を追いかけることをおすすめします。

JITで何か問題が起きるのではと心配な方は、JITは自由にオンオフできることを覚えておきましょう。RUBYOPT環境変数は、MJITやYARV-MJITを含む含まないにかかわらず任意のCRubyで使えますし、Rubyを実行するたびに(Rubyコード入力中でなくても)コマンドライン引数で渡すこともできます。

現時点では、JITはYARV-MJITでもデフォルトでオフになっています。オンにするには以下のオプションを指定します。

export RUBYOPT="-j"

YARV-MJITでは、JITパラメータを渡さないようにするだけでJITを無効にできます。つまり、-jを最初に渡さなければJITは動きません。

JITを動かすためのオプションは-jの他にもあります。たとえば、-j:wを渡すとJITのwarningがすべて出力され、-j:sを渡すとJITが作成する.cソースファイルを削除せずに/tmpディレクトリに保存します。

JITでもっと遊んでみたい方は、MJITまたはYARV-MJITが有効になっているRubyでruby --helpを実行することをおすすめします。ただしこれらのオプションは、YARV-MJITがRubyに取り込まれる前に変更される可能性があるので、ローカルの(Ruby)バージョンをチェックするべきです。

MJIT options:
  s, save-temps   Save MJIT temporary files in /tmp
  c, cc           C compiler to generate native code (gcc, clang, cl)
  w, warnings     Enable printing MJIT warnings
  d, debug        Enable MJIT debugging (very slow)
  v=num, verbose=num
                  Print MJIT logs of level num or less to stderr
  n=num, num-cache=num
                  Maximum number of JIT codes in a cache

JITを支援する方法/Rubyの次のJITについて

RubyのJITを使ってみたいのであれば、試しに動かしてみるのが簡単かつ手っ取り早いでしょう。

cloneとビルドは以下の方法で行えます。

cd ~/my_src_dir
git clone git@github.com:k0kubun/yarv-mjit.git
cd yarv-mjit
autoconf
./configure
make check

ビルドが終われば、テストやインストールをローカルで行えるようになります。私は以下のrunrubyスクリプトでローカルテストするのが好みです。

cd ~/my_src_dir/yarv-mjit
./tool/runruby.rb ~/my_src_dir/my_ruby_script.rb

rvmを使えば、ローカルでビルドしたRubyインタプリタをマウントできます。

# コンパイルの後で実行すること!
rvm mount ~/my_src_dir/yarv-mjit yarv-mjit
rvm use ext-yarv-mjit

-jでJITがオンになり、-j:wでwarningがオンになることをお忘れなく。コードをYARV-MJITで実行してみた方は、ぜひお知らせください。Twitterでお知らせいただくと助かりますが、他の方法でも構いません。

JITで何か問題が生じたら、小規模な再現手順に切り分けてから、YARV-MJITのRuby bug #14235までお知らせください。よろしくお願いします。

関連記事

Ruby: mallocでマルチスレッドプログラムのメモリが倍増する理由(翻訳)

メモリを意識したRubyプログラミング(翻訳)

Rubyのヒープをビジュアル表示する(翻訳)

Rubyのメモリ割り当て方法とcopy-on-writeの限界(翻訳)

Railsのurl_helperの速度低下を防ぐコツ(翻訳)

$
0
0

概要

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

Railsのurl_helperの速度低下を防ぐコツ(翻訳)

ある少年がナイフを一本もらいました。「これはとてもよく切れるナイフだから」という触れ込みでしたが、いざ少年が肉などを切ろうとしてみるとどうもうまく切れないので、少年は「このナイフ、言うほど大したことないな」と思ってしまいました。しかしナイフをくれた人が「ちょい待ち、ナイフの刃をもう少し傾けて。それからナイフの持ち方がよくないからこう持ってごらん」とアドバイスしたところ、今度は見事ナイフで肉を切ることができました。

オープンソースソフトウェアや人生はちょうどこのナイフのように、正しい使い方を学ばないとうまくいかないことがあります。しかも、ナイフそのものに避けがたい問題があることもあります。さらに、ネットで見つけたナイフの使い方の情報がひどいしろもので、始末の悪いことに特定の状況ではその情報が適切だったりすることもあります。人生が面倒なのは今に始まったことではありませんが、ともあれ本記事ではRails.application.routes.url_helpersというナイフについて書いてみたいと思います。

Railsコントローラのコンテキストの外でURLを生成する状況は非常に多いので、シリアライザやジョブなど、その機能が自動的には使えないような場所でこのモジュールの機能が必要になることも非常によくあります。ネットの情報では、このモジュールに直接アクセスすることを何年もの間気軽に勧めていて、しばらくの間これで何の問題も生じませんでした。

しかし運の悪いことに、最近のバージョンのRailsではこの方法で問題が生じるようになりました(問題を指摘しているGithub issue修正のPRを参照)。これによる問題を明らかにするため、簡単なテスト用Railsアプリをセットアップしてみました。関連するコードを以下に示します。

# routes.rb
Rails.application.routes.draw do
  resources :things do
    collection do
      get :faster
    end
  end
end
# app/whatever/url_helper.rb
class UrlHelper
  include Singleton
  include Rails.application.routes.url_helpers
end
# app/controllers/things_controller.rb
class ThingsController < ApplicationController
  def index
    things_json = (1..100).map do |i|
      {
        id: i,
        url: Rails.application.routes.url_helpers.thing_url(i, host: 'localhost')
      }
    end

    render json: things_json
  end

  def faster
    things_json = (1..100).map do |i|
      {
        id: i,
        url: UrlHelper.instance.thing_url(i, host: 'localhost')
      }
    end

    render json: things_json
  end
end

このindexアクションでは、StackOverflowのアドバイスどおりにコードでモジュールを直接呼び出しています。fasterアクションの方では、このモジュールをincludeするヘルパークラスを使っています。Railsではこのモジュールをincludeして使うことが推奨されているようです。

2つのアプローチの実行結果を並べて見てみましょう。abと単一のRailsインスタンスで小さなテストを実行してみました(不要な出力が大量にあったので省略してあります)。

➜ ab -n 1000 http://127.0.0.1:3000/things

Concurrency Level:      1
Time taken for tests:   18.155 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      4815000 bytes
HTML transferred:       4485000 bytes
Requests per second:    55.08 [#/sec] (mean)
Time per request:       18.155 [ms] (mean)
Time per request:       18.155 [ms] (mean, across all concurrent requests)
Transfer rate:          259.00 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.1      0       1
Processing:    15   18   2.2     17      42
Waiting:       15   18   2.2     17      42
Total:         15   18   2.2     18      42
 ➜  ab -n 1000 http://127.0.0.1:3000/things/faster

 Concurrency Level:      1
 Time taken for tests:   8.540 seconds
 Complete requests:      1000
 Failed requests:        0
 Total transferred:      4815000 bytes
 HTML transferred:       4485000 bytes
 Requests per second:    117.09 [#/sec] (mean)
 Time per request:       8.540 [ms] (mean)
 Time per request:       8.540 [ms] (mean, across all concurrent requests)
 Transfer rate:          550.58 [Kbytes/sec] received

 Connection Times (ms)
               min  mean[+/-sd] median   max
 Connect:        0    0   0.1      0       1
 Processing:     7    8   1.3      8      23
 Waiting:        7    8   1.3      8      23
 Total:          7    8   1.3      8      23

最初の例(indexメソッド)ではモジュールを直接呼び出していますが、平均して18msを要し、スループットは55リクエスト/秒です。本番で調子の良いときにはリクエストに5秒かかるのであれば「そんなに悪くないんじゃ?」とお思いかもしれません。しかしモジュールを直接呼び出すのではなく、単にincludeする方はどうでしょうか?こちら(fasterメソッドの方)は平均して8msでスループットは117リクエスト/秒と、最初のアプローチのほぼ倍速になっています。私は名シェフではありませんが、このナイフを正しく持てば適切に肉を切れるのです。

結論: Rails.application.routes.url_helpersを直接呼ばず、このモジュールをクラスにincludeすることで、コードは高速化します。

関連記事

Rails: belongs_to関連付けをリファクタリングしてDRYにする(翻訳)

Rails: モデルのクエリをカプセル化する2つの方法(翻訳)

RailsのモデルIDにUUIDを使う(翻訳)

Rails 5.1以降のシステムテストをRSpecで実行する(翻訳)

$
0
0

概要

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

@jnchitoさんの以下の記事も合わせてどうぞ。

Rails 5.1以降のシステムテストをRSpecで実行する(翻訳)

RSpec 3.7登場前のfeature spec は、実物(またはヘッドレス)のブラウザ環境でJavaScriptの絡むアプリのやり取りをフルスタックでテストする手段でした。最近リリースされたRSpec 3.7では、Railsのsystem testを元にしたsystem specが追加されました。Rails 5.1ではActionDispatch::SystemTestCaseが導入され、実際のブラウザでのテストに使えるようになりました。設定済みのCapybaraラッパーが提供され、Railsフレームワークに組み込まれている多くの機能を利用できるようになりました。設定済みのCapybaraのおかげで、従来は正しく設定するためにトリッキーになりがちだった手動設定の手間が大きく軽減されました。feature specの代わりにsystem specを使うメリットは次のとおりです。

  1. テストが終わるとデータベースの変更が自動でロールバックされるので、database_cleaner gemを用いてロールバックを手動で設定する必要がない。
  2. driven_byを使うと、specごとにブラウザを簡単に切り替えられる。
  3. テストが失敗すると即座にスクリーンショットを自動撮影し、ターミナルにもスクリーンショットをインライン表示する。この機能は事前設定済みなので、capybara-screenshot gemなどが不要になる。

上述のメリットに加えて、RSpecチームがRails 5.1以降ではfeature specよりもsystem specを推奨している点も強調しておきたいと思います。

RSpecのsystem specをヘッドレスChrome向けにセットアップする

まずはRailsプロジェクトを新規作成しましょう。いつものように--skip-testオプションを追加して、RailsデフォルトのminitestではなくRSpecを使うようにします。

rails --version
Rails 5.2.0.beta2
rails new rspec-system-specs --skip-test --skip-active-storage

セットアップでGemfilerspec-railsを追加する作業以外で最も面倒なのは、ヘッドレスChromeブラウザのテストに必要なgemを見極めることでしょう。必要なgemのリストを以下に示します。

group :development, :test do
  # Capybara system testingとselenium driverのサポートを追加
  gem 'capybara', '~> 2.16.1'
  gem 'selenium-webdriver', '~> 3.8.0'
  # Chromeでのシステムテスト実行に使うchromedriverを簡易インストール
  gem 'chromedriver-helper', '~> 1.1.0'
  gem 'rspec-rails', '~> 3.7.2'
end

chromedriverがインストールされていることを確認します(訳注: 以下はMacでhomebrewを使う場合です)。

brew install chromedriver
chromedriver --version
#> ChromeDriver 2.34.522932 (4140ab217e1ca1bec0c4b4d1b148f3361eb3a03e)

spec/test_helper.rbspec/rails_helper.rbを生成します。

rails g rspec:install

system specを書く

ここでは単純なhome#indexアクションがroutes.rbのrootとして設定されていて、app/views/home/index.html.erbで以下をレンダリングするとします。

<h1 id="title">Hello World</h1>

最初のsystem specを次のようにspec/system/home_spec.rbに実装できます。

require 'rails_helper'

describe 'Homepage' do
  before do
    driven_by :selenium_chrome_headless
  end

  it 'shows greeting' do
    visit root_url
    expect(page).to have_content 'Hello World'
  end
end

ここではヘッドレスChromeを使いたいので、:selenium_chrome_headlessドライバを設定しています。Capybaraでこの他に提供されている登録済みドライバは、:rack_test:selenium:selenium_chromeです。ブラウザの解像度などの高度な設定オプションについてのドキュメントは、driven_byをご覧ください。

ちゃんと動くかどうか確認します。

$ rspec
Puma starting in single mode...
* Version 3.11.0 (ruby 2.5.0-p0), codename: Love Song
* Min threads: 0, max threads: 4
* Environment: test
* Listening on tcp://127.0.0.1:50713
Use Ctrl-C to stop
.

Finished in 1.41 seconds (files took 2.04 seconds to load)
1 example, 0 failures

ドライバをspecごとに設定したくない場合は、spec_helper.rbでデフォルトのグローバル設定を以下のように行なえます。

RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :selenium_chrome_headless
  end
end

後は必要に応じて個別のspecファイルでデフォルトのドライバ設定を上書きします。

テスト失敗時のブラウザスクリーンショット

system specで特筆したい点は、specが失敗したときにブラウザのスクリーンショットを自動で撮影し、ターミナルにインライン出力してくれる便利な機能があることです。

spec失敗時

この機能はRailsのシステムテストに組み込まれているので、feature specのようにcapybara-screenshotなどによるサポートを手動で設定する必要がありません。

JavaScriptのテスト

ここまではサーバー側でのコンテンツレンダリングのテストだけなので、今度はクライアント側のJavaScriptを追加して、JavaScriptが動くブラウザ(ここではヘッドレスChrome)がsystem specで使えることを示してみましょう。

$ ->
  $('#title').text('Hello Universe')

specのアサーションをexpect(page).to have_content 'Hello Universe'に変更すると、クライアント側でのJavaScript変更のspecテストはこれまで同様パスします。

$ rspec
Finished in 1.99 seconds (files took 2.44 seconds to load)
1 example, 0 failures

データベースの自動ロールバック

上述したように、system specでのデータベース変更は自動的にロールバックされます。ページにデータベース出力を少し追加してテストしてみましょう。

<h1 id="title">Hello World</h1>
<p>
  Planet count: <%= Planet.count %>
</p>

specを変更してレコードをseedし、出力されたレコードのcountのアサーションを行うspec exampleを新たに追加します。

describe 'Homepage' do
  before do
    driven_by :selenium_chrome_headless
    puts 'creating a planet'
    Planet.create!(name: 'Mars')
  end

  it 'shows greeting' do
    visit root_url
    expect(page).to have_content 'Hello Universe'
  end

  it 'shows planet count' do
    visit root_url
    expect(page).to have_content 'Planet count: 1'
  end
end

rspecの結果は次のようになります。

$ rspec --format d
creating a planet
  shows greeting
# spec example追加後のデータベース自動ロールバック
creating a planet
  shows planet count

Finished in 1.52 seconds (files took 1.03 seconds to load)
2 examples, 0 failures

できました!feature specではspec example実行後にデータベースの状態をクリーンアップするためにdatabase_cleaner gemの設定が必要でしたが、これで不要になりました。

本記事のデモに用いた例のソースコードはGitHubに置いてあります。

関連記事

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

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

Rails: ActiveRecord関連付けのpreload/eager-loadをテストする2つの方法(翻訳)

Viewing all 1093 articles
Browse latest View live