書籍「エクストリームプログラミング」の読みかた

勤務先で話題になっていた書籍『エクストリームプログラミング』について、メンバーに話したことを書き残しておきます。

TL;DR としては、書籍『エクストリームプログラミング』は初版の内容を踏まえた方が第二版を読み進めやすいかもしれないといったものです。

現代で書籍『エクストリームプログラミング』というと kdmsnr さんが訳した第二版新訳がピックアップされたりするのですが、Kent Beck が記した『Extreme Programming Explained: Embrace Change』には初版と第二版があり、第二版は Kent Beck が初版からの5年間の人生でリライトされた (2nd Edition の付加以外) 同じタイトルでの別物です。

初版と第二版の原書邦訳を交えてタイムラインにすると次のとおりです。

例えるなら新約聖書旧約聖書を背景にしているような感じで、旧約 (初版) を背景として知っておくと旧約 (初版) から新約 (第二版) への繋がりを理解できると思います。

エクストリームプログラミング』の初版は Kent Beck が Martin Fowler や Ron Jeffries らと取り組んだ C3 プロジェクトでのソフトウェア開発の実践での知見が色濃く出ており、第二版はその裏にある思想などが掘り下げてられています。第二版だけ読んでも抽象的でとっつきづらさがある場合は、初版を読んだ具象的な知識を元に第二版を読む方が Kent Beck の 5 年間の流れとともに理解が進むかもしれません。

また『エクストリームプログラミング』の初版には用語集がついており、エクストリームプログラミングにおけるプログラマーの定義も「分析、設計、テスト、プログラミング、結合を行うチーム内の役割」と記されています。これはエクストリームプログラミングを理解するのに大事なことだと思うので特筆しておきます。

最後に引用した書籍の Amazon へのリンクをつけておきます。

『Extreme Programming Explained: Embrace Change』 (1999年)

www.amazon.co.jp

『XPエクストリーム・プログラミング入門―ソフトウェア開発の究極の手法』 (2000年)

[asin:489471275X:detail]

『Extreme Programming Explained: Embrace Change, 2nd Edition』 (2004年)

『XPエクストリーム・プログラミング入門―変化を受け入れる』 (2005年)

エクストリームプログラミング』 (2015年)

Code Climate Test ReporterとSimpleCov 0.18で起きるエラーを回避する

昨日あたりから RuboCop の master で CI が落ちていて、見てみたら Code Climate の cc-test-reporter でエラーが起きていることが原因だった。

$ #!/bin/bash -eo pipefail
./tmp/cc-test-reporter before-build
COVERAGE=true bundle exec rake spec
./tmp/cc-test-reporter format-coverage --output tmp/codeclimate.$CIRCLE_JOB.json
Starting test-queue master (/tmp/test_queue_157_5140.sock)

==> Summary (2 workers in 36.1115s)

    [ 1]                         8753 examples, 0 failures, 9 pending       254 suites in 36.1034s      (pid 160 exit 0 )
    [ 2]                         6772 examples, 0 failures, 2 pending       239 suites in 36.1054s      (pid 161 exit 0 )

Coverage report generated for RSpec, rspec-1, rspec-2, rspec-fork-163, rspec-fork-164, rspec-fork-165 to /home/circleci/project/coverage. 21266 / 21459 LOC (99.1%) covered.
Error: json: cannot unmarshal object into Go struct field input.coverage of type []formatters.NullInt
Usage:
  cc-test-reporter format-coverage [coverage file] [flags]

Flags:
      --add-prefix string   add this prefix to file paths
  -t, --input-type string   type of input source to use [clover, cobertura, coverage.py, excoveralls, gcov, gocov, jacoco, lcov, simplecov, xccov]
  -o, --output string       output path (default "coverage/codeclimate.json")
  -p, --prefix string       the root directory where the coverage analysis was performed (default "/home/circleci/project")

Global Flags:
  -d, --debug   run in debug mode


Exited with code exit status 255

https://circleci.com/gh/rubocop-hq/rubocop/83619

調べたところ SimpleCov 0.18 のリリースがリリースされており、cc-test-reporter がうまく組み合っていないというイシューが開かれていた。

github.com

PR がパスしているかどうかが分からないのは不便なので、対応までのワークアラウンドとして Gemfile に SimpleCov 0.18 未満を指定して乗り切ることにした。

-gem 'simplecov', '~> 0.10'
+# Workaround for cc-test-reporter with SimpleCov 0.18.
+# Stop upgrading SimpleCov until the following issue will be resolved.
+# https://github.com/codeclimate/test-reporter/issues/418
+gem 'simplecov', '~> 0.10', '< 0.18'

github.com

キーワード引数の分離への対応にRuby 2.8.0-devを使う

先日のパッチ会で kamipo さんにもらったアドバイスを書き残しておく。

TL;DR としては表題そのまま。キーワード引数の分離への対応にRuby 2.8.0-devを使うというもの。

Ruby 2.7.0 を使ってキーワード引数の分離への警告のみでそれを抑制しようとする場合は、スーパーハードモードルビーとパッチ会で呼ばれた変更箇所の特定が難しいケースになる場合がある。

スーパーハードモード (Ruby 2.7.0)

Ruby 3.0 に向けてキーワード引数の分離が必要になる場合は、Ruby 2.7.0 を使うと以下のような警告が表示される。

% ruby -v
ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-linux]

% bundle exec rake
(略)

/home/vagrant/.rvm/gems/ruby-2.7.0/bundler/gems/rails-80e72c5eb7d4/activerecord/lib/active_record/migration.rb:907: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
/home/vagrant/src/oracle-enhanced/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb:199: warning: The called method `create_table' is defined here

まず warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call となっている Active Record のコードの場所を示す。

    def method_missing(method, *arguments, &block)
      arg_list = arguments.map(&:inspect) * ", "

      say_with_time "#{method}(#{arg_list})" do
        unless connection.respond_to? :revert
          unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method)
            arguments[0] = proper_table_name(arguments.first, table_name_options)
            if [:rename_table, :add_foreign_key].include?(method) ||
              (method == :remove_foreign_key && !arguments.second.is_a?(Hash))
              arguments[1] = proper_table_name(arguments.second, table_name_options)
            end
          end
        end
        return super unless connection.respond_to?(method)
->      connection.send(method, *arguments, &block) # ここが `activerecord/lib/active_record/migration.rb:907`
      end
    end
    ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)

次に warning: The called methodcreate_table' is defined here` となっている Oracle enhanced adapter のコードの場所を示す。

->      def create_table(table_name, id: :primary_key, primary_key: nil, force: nil, **options) # ここが `oracle_enhanced/schema_statements.rb:199`
          create_sequence = id != false
          td = create_table_definition(
            table_name, **options.extract!(:temporary, :options, :as, :comment, :tablespace, :organization)
          )

結論としてはどちらもキーワード引数の分離への変更対象ではない。メソッドの定義側も呼び出し側いずれのコードも期待から外れたものではなかった。

これが kamipo さんの "warningみてもどこ直せばいいかまったくといっていいほどわからんからな" の一例である。

次に Ruby 2.8.0-dev を使うと警告ではなくエラーになることでバックトレースが見えるイージーモードに切り替えてみよう。

イージーモード (Ruby 2.8.0-dev)

Ruby 2.8.0-dev を使った結果は以下となる。

Failures:
  1) OracleEnhancedAdapter schema dump tables should not include ignored
  table names in schema dump
     Failure/Error:
               def create_table(table_name, id: :primary_key, primary_key: nil, force: nil, **options)
                 create_sequence = id != false
                 td = create_table_definition(
                   table_name, **options.extract!(:temporary, :options, :as, :comment, :tablespace, :organization)
                 )

                 if id && !td.as
                   pk = primary_key || Base.get_primary_key(table_name.to_s.singularize)

                   if pk.is_a?(Array)

     ArgumentError:
            wrong number of arguments (given 2, expected 1)
     # ./lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb:199:in `create_table'
     # /home/travis/.rvm/gems/ruby-head/bundler/gems/rails-d80714e0834f/activerecord/lib/active_record/migration.rb:907:in `block in method_missing'
     # /home/travis/.rvm/gems/ruby-head/bundler/gems/rails-d80714e0834f/activerecord/lib/active_record/migration.rb:875:in `block in say_with_time'
     # /home/travis/.rvm/rubies/ruby-head/lib/ruby/2.8.0/benchmark.rb:293:in `measure'
     # /home/travis/.rvm/gems/ruby-head/bundler/gems/rails-d80714e0834f/activerecord/lib/active_record/migration.rb:875:in `say_with_time'
     # /home/travis/.rvm/gems/ruby-head/bundler/gems/rails-d80714e0834f/activerecord/lib/active_record/migration.rb:896:in `method_mi
     # ./spec/active_record/connection_adapters/oracle_enhanced/schema_dumper_spec.rb:24:in `block in create_test_posts_table'
     # ./spec/spec_helper.rb:121:in `instance_eval'
     # ./spec/spec_helper.rb:121:in `block (2 levels) in schema_define'
     # /home/travis/.rvm/gems/ruby-head/bundler/gems/rails-d80714e0834f/activerecord/lib/active_record/migration.rb:884:in `suppress_messages'
     # ./spec/spec_helper.rb:120:in `block in schema_define'
     # /home/travis/.rvm/gems/ruby-head/bundler/gems/rails-d80714e0834f/activerecord/lib/active_record/schema.rb:50:in `instance_eval'
     # /home/travis/.rvm/gems/ruby-head/bundler/gems/rails-d80714e0834f/activerecord/lib/active_record/schema.rb:50:in `define'
     # /home/travis/.rvm/gems/ruby-head/bundler/gems/rails-d80714e0834f/activerecord/lib/active_record/schema.rb:46:in `define'
     # ./spec/spec_helper.rb:119:in `schema_define'
     (以下略)

Ruby 2.7.0 で表示されている警告は ArgumentError からのバックトレースの以下2行となる。そして、Ruby 2.8.0-dev では警告ではなくエラーになることでその先もバックトレースとして表示されている。

     ArgumentError:
            wrong number of arguments (given 2, expected 1)
     # ./lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb:199:in `create_table'
     # /home/travis/.rvm/gems/ruby-head/bundler/gems/rails-d80714e0834f/activerecord/lib/active_record/migration.rb:907:in `block in method_missing'

これが示すことはバックトレースで問題の箇所を探索することができるようになるということ。この例だとバックトレース中に示されている spec/active_record/connection_adapters/oracle_enhanced/schema_dumper_spec.rb:24 が修正対象となるものだった。

これはテストコード中の以下を示している。

  def create_test_posts_table(options = {})
    options[:force] = true
    schema_define do
->    create_table :test_posts, options do |t| # ここが `oracle_enhanced/schema_dumper_spec.rb:24`
        t.string :title
        t.timestamps null: true
      end
      add_index :test_posts, :title
    end
  end

変更するべきはこのテストコードで次のようになる。

diff --git a/spec/active_record/connection_adapters/oracle_enhanced/schema_dumper_spec.rb b/spec/active_record/connec
index f3b1df0..8330a95 100644
--- a/spec/active_record/connection_adapters/oracle_enhanced/schema_dumper_spec.rb
+++ b/spec/active_record/connection_adapters/oracle_enhanced/schema_dumper_spec.rb
@@ -21,7 +21,7 @@ describe "OracleEnhancedAdapter schema dump" do
   def create_test_posts_table(options = {})
     options[:force] = true
     schema_define do
-      create_table :test_posts, options do |t|
+      create_table :test_posts, **options do |t|
         t.string :title
         t.timestamps null: true
       end

これによって解決している PR が以下です。

github.com

このように、キーワード引数の分離への対応には Ruby 2.8.0-dev を使った方がよりヒントを得られるケースがあるので、困った時は rbenv install 2.8.0-dev などで master の Ruby を使ってみることを考慮してみると良いでしょう。

この問題に2ヶ月間取り組んだ kamipo さんからの知見に感謝します。


次回のパッチ会は 2020年2月27日(木)です。

blog.agile.esm.co.jp

Ginza.rb 第79回

Ginza.rb 第79回 Rails6の新しい定数自動読み込みZeitwerkのコードを読もう!に参加した。会場はメドピアさん。

ginzarb.doorkeeper.jp

表題のとおり今回は Zeitwerk がテーマ。余談だが自分は発音できないのでカタカナ読みでツァイトヴェルクと読んでいたが、それでも読みづらいので Rails コミュニティで略称として使われている zloader (ゼットローダー) の方で読んでいたりする。

オートローダーとは Railsrequire を書かなくてもクラスが使える機構を支えているものだが、Ruby 標準の autoload を Rails が拡張した autoload が Rails 5.2 以前に択一だったもので、Rails 6 で追加されたオートローダーが Zeitwerk となる。従来のオートローダーは classic というもので Active Support が提供している。なお、Rails では Active Support が Zeitwerk に依存してる

autoload はクラスが見つからなかったときにファイルをロードするが、Zeitwerk はその逆でファイル名をもとにクラスをロードする。なのでファイル名とクラス名のマッピングが不確かだと NameError が起きるのでそのあたりはひとつの注意点としましょうとなっている。例としては html_parser.rb は HtmlParserマッピングされるため HTMLParser としたい場合は infrection を使ったマッピングを行う。

github.com

また Zeitwerk はスレッドセーフな実装になっているので、autoload を使っていたときにレースコンディションでエラーになるようなケースが解消されるなどのメリットがあるとのこと。

実装として面白かったのはクラスの reload だが、自分だったら愚直に load メソッド (require と異なり同じファイルを読み直す) を使うのが第一感となるが、require 済みのファイルパスを保持する $LOADED_FEATURES から対象のパスを reject! で破壊的に除いて 再び setup を実行するとかマジかと思って面白かった。このコードは面白かった。印象的なことだったので2度言った。

あと TracePoint が実装として使われていたりもするライブラリなので、TracePoint ファンの人は読んでみると面白いと思う。

今回 willnet さんの主導で README を読んで実装のコード解説に入っていたけれど、時間内にしゅっと本筋のコードを眺めてどういったものかを解説する様はまさに匠の技だった。Zeitwerk への理解が深まりました。ありがとうございました。

Gem 開発における gemspec と Gemfile への開発 Gem の指定について

昨日の Asakusa.rb 第547回で、hsbt さんに聞いて amatsuda さんたちを交えて話していた表題について書き残しておく。先に記しておくと決定的な結論はない。

Gem を開発する際に、開発時のみに使う Gem を指定する先として gemspec を使った spec.add_development_dependency での指定と Gemfile を使った gem での指定がある。補足しておくとそれぞれ前者は RubyGem での指定で、後者は Bundler での指定となる。このあたりどちらが推奨されているとかあれば聞いてみようというのが質問の発端だった。

RubyGems 3.1.2 と Bundler 2.1.4 時点で、それぞれでできることは以下となる。

RubyGems

メタデータとして rubygems.org に公開できる。ただしそれを使ったアナリティクスなどをとっているわけではないみたい。

Gemfile

Gemfile に gem 指定する際にできることとして path オプションや github オプションに加え、group 指定などできる。加えて gemfiles に複数バージョンそれぞれ用の Gemfile を配置することで、例えば複数バージョンの Rails についてバージョンごとに Gemfile を切り替えたテストができる。Appraisals を使った複数の Gemfile 切り替えなんかもこれがベースになる。これらは gemspec ではできないこと。

github.com

結局のところ?

基本的にできることが多い Gemfile に書けば良いというスタイルもある一方で amatsuda さんのスタイルは面白くて、複数環境の Gemfile に共通するものは gemspec に指定して、環境差分がある分についてはそれぞれの Gemfile に指定することで開発依存 gem の指定を DRY にするというもの。好きな言葉を地で行っていて流石だと思った。

結論という結論は特になく、現状だと Gemfile でしか指定できないことは Gemfile をもちいて行うしかなく、あとは考え方次第のスタイルという感じではないだろうかと理解している。

ちなみに GitHubUsed by はそれぞれが参照されていそうという感じだった。

Asakusa.rb 第547回

Asakusa.rb 第547回だった。

ちょうど RubyKaigi 2020 のプロポーザルの提出期限日に Asakusa.rb というタイミングだったので、yahonda さんや joker1007 さんにフィードバックをもらいつつ、締め切り3時間ちょっと前くらいに提出していた。提出が終わったらだいたい燃え尽きていた (レビューありがとうございました!) 。

あとは RubyGems と Bundler のメンテナーでもある hsbt さんに、gem 開発リポジトリにおける gemspec への add_development_dependency 指定と Gemfile への指定の違いについて疑問に思っていたことを聞いたりしていた。有用な話を聞けたので、別エントリとして書くと思う。

Asakusa.rb べんり。