Rubyのモジュールでnewを使う方法

タイトルは半分くらい釣りです。役に立たないRuby雑学です。

コード例

モジュールで self.new を定義すると、モジュールに対してnewが呼べる。

class A
end

module M
  def self.new
    A.new
  end
end

p M.new.class
#=> A

別のクラスのインスタンスを返すクラス

同じようにクラスで self.new を定義しておけば、別のクラスのインスタンスを返すことも可能になる。

class A
end

class B
  def self.new
    A.new
  end
end

p B.new.class
#=> A

読みづらいので、こんなコードは書いたらダメです。絶対にダメですよ!

RuboCopの新しいルールを追加する方法(Custom Copの作り方)

何度もレビューで指摘することはRuboCopのルールとして追加して、自動化しておくと便利です。

この記事では、RuboCopで新しいルールを作る方法を紹介します。

参考にしたページ

これがとても参考になった。

github.com

Copの書き方は独特なので、RuboCopリポジトリlib/rubocop/cop ディレクトリにある他Copをいくつか読むと参考になります。

github.com

Custom Cop の作り方

ここでは例としてMatzの名前を "Hiroyuki Matsumoto" と間違って書いたとき、検知できるCopを作ってみます。

まずは .rubocop.ymlrequire の行を追加します。

require: './lib/custom_cops/matz_name'

AllCops:
  # 以下略...

次に、 lib/custom_cops/matz_name.rb に新しいルールを書きます。

# frozen_string_literal: true

module CustomCops
  # @example
  #   # bad
  #   MATZ_NAME = "Hiroyuki Matsumoto"
  #
  #   # good
  #   MATZ_NAME = "Yukihiro Matsumoto"
  class MatzName < RuboCop::Cop::Cop
    BAD_NAME = "Hiroyuki Matsumoto"
    GOOD_NAME = "Yukihiro Matsumoto"
    MSG = "Use '#{GOOD_NAME}' instead of '#{BAD_NAME}'."

    def on_str(node)
      add_offense(node) if node.source.include?(BAD_NAME)
    end

    def autocorrect(node)
      ->(corrector) {
        new_code = node.source.gsub(BAD_NAME, GOOD_NAME)
        corrector.replace(node.source_range, new_code)
      }
    end
  end
end

いくつか独特なルールがあるので、簡単に説明します。

on_xxx メソッドはRubyのコード内で該当するコードが見つかった時に呼ばれるメソッドです。 たくさんあるので興味ある人はRuboCopリポジトリlib/rubocop/ast/traversal.rbを読むと良いです。 基本的には「それっぽいメソッドを定義して、テスト書いてみて試す」とか「他Copの実装をパクる」で頑張ります。

add_offenseはルール違反を検知した時に呼び出します。 引数に渡している node に対して、MSG 定数の文字列をメッセージとして表示します。 メッセージを動的に変えたい場合は message オプションで変える事もできます。

add_offense(node, message: "foo")

autocorrect メソッドは実装すると --auto-correct に対応することができます。 コード例のように proc を返すようにする必要があります。

テストの書き方

spec/rails_helper.rb で下記のようにCustom Copを読み込むコード、RSpecのヘルパーメソッドの読み込みを行います。

require "rubocop"
Dir[Rails.root.join("lib/custom_cops/**/*.rb")].each { |f| require f }
require "rubocop/rspec/support"

RSpec.configure do |config|
  config.include(RuboCop::RSpec::ExpectOffense)
end

RuboCopリポジトリのテストコードを参考にしながら、コードを書きます。

# frozen_string_literal: true

require "rails_helper"

RSpec.describe CustomCops::MatzName do
  subject(:cop) { described_class.new }

  it "registers an offense" do
    expect_offense(<<-RUBY.strip_indent)
      MATZ_NAME = "Hiroyuki Matsumoto"
                  ^^^^^^^^^^^^^^^^^^^^ Use 'Yukihiro Matsumoto' instead of 'Hiroyuki Matsumoto'.
    RUBY
  end

  it "autocorrects" do
    new_source = autocorrect_source('MATZ_NAME = "Hiroyuki Matsumoto"')
    expect(new_source).to eq 'MATZ_NAME = "Yukihiro Matsumoto"'
  end
end

できればRuboCopにプルリクを投げよう

もし有用なCopを実装できたら、RuboCopにプルリクを投げると良いです。

  • RuboCopのメンバーがコードレビューしてくれる
  • 世界中のRuboCopユーザーがIssue挙げたり、メンテしてくれる

などのメリットがあります。

普段からRuboCopを便利に使わせて頂いているので、たまには恩返しとしてプルリクを送りたいですね。

おわり

「人間が何度もレビューで指摘する」は真面目で良いことだとは思いますが、怠惰力が足りないです。

機械的にレビューできる箇所は機械に任せ、我々は人間にしかできない「自動化の難しいところのレビュー」や「ビールを飲みながらアニメを観る」などを頑張りたい。

ActiveRecord の where.not とド・モルガンの法則

where.not を使っていて遭遇した問題についてrailsdmの懇親会で話していたら、@kamipo さんから「5.2では少し直っている」という情報を頂いた。

この話をしているときに思ったけど、今の where.not の問題点について知らない人も多そうなので、せっかくなのでブログにまとめてみた。

where.notの問題

where.not のドキュメントにいくつか例がある。

問題は複数の条件を引数で指定した場合。

User.where.not(name: "Jon", role: "admin")
# SELECT * FROM users WHERE name != 'Jon' AND role != 'admin'

これはド・モルガンの法則でORになりそうなのに、なっていない。

# ド・モルガンの法則
# NOT(A AND B) = NOT(A) OR NOT(B)

# つまり、これは
!(name == "Jon" && role == "admin")

# こうなる
name != "Jon" || role != "admin"

実際に遭遇したケース

ポリモーフィック関連を使っていて、 where.not と組み合わせた時に遭遇した。

# $ rails g model Picture imageable:references{polymorphic}
# $ rails g model Employee
# $ rails db:migrate:reset
# $ rails c
# Loading development environment (Rails 5.1.5)

e = Employee.create
#=> #<Employee id: 1, created_at: "2018-03-26 09:50:28", updated_at: "2018-03-26 09:50:28">
Picture.where.not(imageable: e).to_sql
#=> SELECT "pictures".* FROM "pictures" WHERE ("pictures"."imageable_type" != 'Employee') AND ("pictures"."imageable_id" != 1)

5.2.0.rc2 の挙動

5.2.0.rc2 ではポリモーフィック関連の挙動が直っている。

# $ rails g model Picture imageable:references{polymorphic}
# $ rails g model Employee
# $ rails db:migrate:reset
# $ rails c
# Loading development environment (Rails 5.2.0.rc2)

e = Employee.create
#=> #<Employee id: 1, created_at: "2018-03-26 09:56:18", updated_at: "2018-03-26 09:56:18">
Picture.where.not(imageable: e).to_sql
#=> SELECT "pictures".* FROM "pictures" WHERE NOT ("pictures"."imageable_type" = 'Employee' AND "pictures"."imageable_id" = 1)

そして、これが @kamipo さんの最高のコミット。

github.com

213796f のコミットでポリモーフィック関連の挙動が直った後、すかさずテストを追加している。*1

ポリモーフィック関連以外のケース

Issue は作られていて、議論されている。

github.com

個人的には 6.0 で複数条件のケースも直っていると嬉しい。

まとめ

where.not で ポリモーフィック関連複数の条件 を渡すコードがあったら注意しましょう。

*1:テストのある機能は基本的に維持される

Railsアプリの育て方という発表をしました #railsdm

Rails Developers Meetup 2018 Day 1で「Railsアプリの育て方」という発表をしました。

railsdm.github.io

発表資料

余談

本当は4月から放送されるシュタインズゲートゼロみたいな流れにしたかったけど、ちょっと上手く話の流れを作れなかったので、今回は普通に作りました。

資料を作るのは難しい。

Step-to-Rails-Expert.rb#17 に参加した #StepRailsEx

ToDo アプリを題材にして、参加者で相互レビューする勉強会。

step-to-rails-expert-rb.connpass.com

コード持ち込んだ

今までサボっていたので、今回は課題を解いて持ち込んでみた。

github.com

Ruby/Rails のアップグレードなどが含まれている関係で、変更量が多くなってしまった。

実際の機能に関わるコミットは後半のやつ。

  • Bootstrap で少しデザイン調整
  • タスクを完了する機能を実装
  • サインアップ後に確認メールを送信する機能
  • 本番環境で実際にメールが届くように設定

機能に関する diff は sinsoku/expert-todo@c57cff2...9af64b3 になる。

レビューの練習できる会

仕事以外でレビューする機会/される機会は少ないので、 StepRailsEx は良い機会になるなーと思って、できるだけ参加してる。

あと、ToDoアプリって大した機能は無いのに各参加者で実装が違っていて、その違いについて話すのも楽しい。

Rails を勉強してる人は参考になるかも?

Rails 5.2.0 rc1 + System Test の実例ってあまり多くないだろうし、何かしら参考になるかもしれない。

あと、仕事で GitHub を使っていない人にとっては、プルリクでのレビューの雰囲気とか参考になるかも。

次回の日程

たぶん、4月後半の月曜になると思う。

主催の @ebihara99999 さんと @two_sann さんをフォローしていれば、情報が流れてくるはず 👀

AWS EC2 上に Rails の開発環境を構築する

Rails アプリの開発環境じゃなくて、 rails/rails にプルリクを送るための開発環境を作る方法です。

rails-dev-box について

Rails には Contributing to Ruby on Rails guide というドキュメントがあり、 誰でも簡単に開発環境を作ることができます。

5.1.1 The Easy Way

The easiest and recommended way to get a development environment ready to hack is to use the rails-dev-box.

「The Easy Way」の名前通り、 vagrant up するだけで簡単に開発環境が構築できてしまいます。

問題点

私の使っている PC は MacBook なので、ちょっと VagrantRails を動かすのはスペック的にツラいです。そこで、AWS EC2 上に開発環境を構築してみました。

Vagrant + Ansible

開発環境は Vagrant(+vagrantup-aws) と Ansible を使いました。

$ vagrant up --provider=aws
$ vagrant ssh
ubuntu@xxx: ~$ cd ~/src/rails
ubuntu@xxx: ~$ bundle exec rake
...

のような感じで AWS EC2 にお金さえ払えば、誰でも簡単に Rails の開発環境が使えるようになります。

Vagrantfile と playbook.yml は以下の通りです。*1

Vagrantfile

# frozen_string_literal: true

Vagrant.configure("2") do |config|
  config.vm.box = "dummy"

  config.vm.provider :aws do |aws, override|
    aws.access_key_id = ENV["AWS_ACCESS_KEY_ID"]
    aws.secret_access_key = ENV["AWS_SECRET_ACCESS_KEY"]
    aws.region = ENV["AWS_DEFAULT_REGION"]

    # Ubuntu Server 16.04 LTS (HVM), SSD Volume Type
    aws.ami = "ami-48630c2e"

    # m3.medium  $0.096 / hour
    # m3.large   $0.193 / hour
    # m3.xlarge  $0.385 / hour
    # m3.2xlarge $0.77  / hour
    aws.instance_type = "m3.large"

    aws.keypair_name = ENV["AWS_KEYPAIR_NAME"]
    aws.security_groups = ["rails-dev"]
    aws.tags = { Name: "rails-dev" }

    override.ssh.username = "ubuntu"
    override.ssh.private_key_path = "~/.ssh/#{ENV["AWS_KEYPAIR_NAME"]}.pem"
  end

  config.vm.provision "ansible" do |ansible|
    ansible.playbook = "playbook.yml"
  end
end

playbook.yml

- name: rails
  hosts: all
  gather_facts: no

  vars:
    bootstrap_path: /tmp/bootstrap.sh
    rails_path: ~/src/rails

  pre_tasks:
    - name: install python-simplejson
      raw: sudo apt-get -y install python-simplejson
      register: output
      changed_when: output.stdout.find("1 newly installed") != -1

  tasks:
    - name: Download bootstrap.sh
      get_url:
        url: https://raw.githubusercontent.com/rails/rails-dev-box/master/bootstrap.sh
        dest: "{{ bootstrap_path }}"

    - name: Replace user from vagrant to ubuntu
      replace:
        path: "{{ bootstrap_path }}"
        regexp: "vagrant"
        replace: "ubuntu"

    - name: Install packages
      become: yes
      apt:
        name: "{{ item }}"
      with_items:
        - libreadline-dev

    - command: which ruby
      register: which_ruby
      failed_when: which_ruby.rc not in [0, 1]
      changed_when: no

    - name: Bootstrap for Rails
      become: yes
      command: "bash {{ bootstrap_path }}"
      when: which_ruby.rc == 1

    - name: git clone rails/rails
      git:
        repo: "https://github.com/rails/rails.git"
        dest: "{{ rails_path }}"

    - name: bundle install
      bundler:
        chdir: "{{ rails_path }}"
        extra_args: "--jobs=4"

注意点

  • Ubuntu 16.04 だと Python 3 とかの問題で pre_tasks 無いと動かない。よく分かっていない
  • aws.security_groups は group ID じゃなくて、 Name を使う必要がある。

  • ActiveStorage のテストは一部テストが落ちる。

ActiveStorage のテスト

このブログ読んで、ImageMagick に詳しい人いたらコメントやプルリクを頂けると凄く助かります。

ubuntu: ~/src/rails/activestorage $ bundle exec rake
...
(中略)
Error:
ActiveStorage::Previewer::PDFPreviewerTest#test_previewing_a_PDF_document:
MiniMagick::Invalid: `identify /tmp/mini_magick20180306-5674-pg12ad` failed with error:
identify: no decode delegate for this image format `' @ error/constitute.c/ReadImage/501.

みたいなのでテスト落ちて、よく分からなかった。

まとめ

AWS EC2 を使うことで、Rails の全件テストを 80 円/1 時間 で実行できるようになりました。

あとは Rails に送るパッチを完成させて、英語のコミットログを用意するだけです!*2

*1:実際のファイルは sinsoku/dotfiles に置いてあります

*2:Ruby より英語の方が圧倒的に大変

面接で現年収(前職の年収)を聞かれるのが嫌い

転職活動で面接に行くと、だいたいどの会社も現年収を聞いてくる。

この文化が本当に嫌いなので、その気持ちをブログにまとめてみた。

最近の Twitter のツイートまとめ

たぶん人事が考えていること

生活レベルを維持できる金額を提示したい

生活する上で、最低限の必要な年収はあると思います。 特に結婚していて奥さんや子供がいると、気になるところです。

これを人事が気にして現年収を聞きたくなる気持ちも分かるのですが、それは 希望年収 を聞けば事足ります。現年収を聞く必要はありません。

現年収より高い金額を提示したい

できれば現年収より高い金額を提示したいというケース。

そんな「お気持ち」で年収が簡単に上がる評価基準の会社は微妙。

他社の評価を参考にしたい

他社で評価が高い人であれば、おそらく仕事もできるし、高い年収を提示しても良いはずというケース。

自分で相手の技術力を測ろうとせず、他社の評価を無条件で信じるその姿勢が 嫌い です。 短い面接で相手を正確に評価することが難しいと重々承知していますが、それでも自分で評価しようとする姿勢は持っていて欲しいと思っています。

そもそも、業種や会社規模の異なる他社の評価を参考にするのもどうかと思います。

採用したい

採用するために、現年収より高い金額を提示しておきたいケース。

そんなに採用したい人であれば、現年収に関係なく上限の金額を提示すれば良い。 中途半端に「現年収 + 50万」みたいなの提示して、他社のオファー金額に競り負けたら後悔する。

少しでも安い金額で採用したい

現年収と同じ、もしくは微増の提示年収なら承諾される可能性が高いと考えて聞くケース。

この気持ちは少し分かるけど、エンジニアを安く採用したいと思っている企業は 嫌い です。

まとめ

現年収を聞く理由は下記の2つくらいしか思い浮かびません。

  • エンジニアを安く採用したい
  • 自社で評価せず、他社の評価を使って楽したい

給与テーブルや評価基準を明確にしている企業様は現年収を聞かないようにして頂けると、「この企業は評価基準が明確なのか」と分かるので聞かないようにして頂ければ幸いです。

この記事が人事の目に止まって、現年収を聞く文化が消えて無くなって欲しい。