マイペースなRailsおじさん

Ruby、Ruby on Rails、オブジェクト指向設計を主なテーマとして扱います。だんだん大きくなっていくRuby on Rails製プロダクトのメンテナンス性を損なわない方法を考えたり考えなかったりしている人のブログです。

gemの概要を雑に把握するシリーズ③ rack/rack

rackgithub.com

概要

WebサーバーとWebフレームワークの組み合わせを自由に選択できるように、統一的なインターフェースを提供するライブラリ。 Middlewareを選択することで、双方向の通信の間に特定の処理を割り込ませることができる。 各ソフトウェアで必要になるであろう、MimeタイプやRequestなど、HTTPサーバーに基本的な構成要素も提供している。

気になる実装

ポートとか、基本的なWebサーバーの設定は全部rackで受け持っていた。pumaとかにrackからバイパスしている。 OptParserの呼び出し方がかっこよく見える。参考にしよう。 'X-Sendfile', 'X-Lighttpd-Send-File'とか、知らないヘッダがありそうなので、Applicationから使いたくなりそうなHTTPの勉強にはかなり良いかもしれない

bin/rackup xxx.ru とすると、Rack::Builderの中で.ruに書かれたスクリプトを実行する。 Appのインターフェースを実装したものをrunにわたすと@appとして設定される。

Middlewareという名称でロガーやサーバーの立ち上げのためのフックを提供していて、これをカスタマイズしてつかうこともできる。

Refactoring Kataを使ってRubyのリファクタリングを練習する

Refactoring Kata

CodeKataというサイトがありまして、これはプログラミングの練習をしたい人のための問題集です。この様な練習問題を総称して「Code Kata」あるいは「Coding Kata」と呼んでいます。Emily Bacheさんは、リファクタリングに焦点を絞ったCode Kataを公開していて、これらをRefactoring Kataと読んでいます。

Refactoring Kataは、非常に可読性の悪いコードに対して、新しい機能を追加するという課題になっていて、とても練習になります。もともと、こういう課題が無いかなぁと探していたのですが、とてもいいものが見つかりました。手始めに、Gilded RoseというKataに挑戦してみました。

Gilded Rose

こちらのKataのリファクタリングに挑戦します。

github.com

ファンタジーの世界の宿屋Gilded Roseで、商品の管理を行うプログラムに機能追加せよ、という課題です。仕様書の日本語版がついています。ありがとうございます!

GildedRose-Refactoring-Kata/GildedRoseRequirements_jp.md at master · emilybache/GildedRose-Refactoring-Kata · GitHub

コードを眺める

Rubyでやりたいので、'ruby/gilded_rose.rb'を眺めます。

GildedRose-Refactoring-Kata/gilded_rose.rb at master · emilybache/GildedRose-Refactoring-Kata · GitHub

ワーオ、これ、よく作れたなっていうレベルで難しいプログラムになってます。「リファクタリング」に照らし合わせて、どこにどのリファクタリングを適用すべきかを考えていくのも面白いかもしれません。

テストを書く

プログラムが非常に可読性が低いことがわかりましたが、以外にもちゃんと動きます。ということでテストを黙々と書いていきます。

リファクタリングする

テストが書けたらリファクタリングに取り組みます。

いろいろやりましたが、とにかく分岐を無くすこと、ネストを浅くすることを心がけました。 Qualityの更新に関するルールがわかるようになったのではないでしょうか。

gist.github.com

OOPを駆使する

Itemクラスの変更が禁止されていたので、OOPはやらなくて良いかなーと思ったんですが、Adapterクラスを入れればできないこともないしなーとおもい、思いっきりOOPしてみました。

結果としては、個々のメソッドを単純にできたのですが、全体の構造は複雑になり、見通しが悪くなってしまった気がします。 Qualityをクラスにしたのはやりすぎだったように思います。

gist.github.com

練習あるのみ

リファクタリングって、かなり時間のかかる行為だと思っています。実際、今回はトータル3時間くらいコードを触っていました。ただ、それも慣れの問題が大きいと感じていて、こういった教材を使って経験を積んでいくことにより、短時間でリファクタリングすることは十分可能なはずです。

Refactoring Kataを使って日々鍛錬をしていきましょう。

Active RecordとNull Objectパターン

Active RecordでNull Objectパターンを使うことはできるのか考えてみました。

RubyでNull Objectパターン

RubyでもNull Objectパターンを使えます。下記の記事を御覧ください。

ytnk531.hatenablog.com

Active RecordとNull Objectパターン

どんなときに使いたいか

アソシエーションを張っているレコードがない場合の処理を書きたいときに、Null Objectパターンを使えたら便利そうです。

実装

class Customer < ApllicationRecord
  has_one :actual_license, class_name: 'License'

  def license
    actual_license || NullLicense.new
  end
end

class License < ApplicationRecord
  belongs_to :customer
end

class NullLicense
end

このようにすれば、アソシエーションしているLicenseレコードが無い場合はNullLILicenseを渡すことができます。実際のlicenseとの関連はCustomer#actual_licenseを呼び出すことになります。

実装のポイント - has_oneのclass_nameオプションを使ってLicenseを取得するメソッドの名前を変更する - licenseメソッドのゲッタを作ることでNullLicenseのインスタンスを返せるようにする

困りそうなポイント

NullLicenseのほうに値の無い処理が書けるようになりましたが、その代わり、構造上のややこしさを生み出しました。

  • ライセンスのない場合の処理を外部に書きたいとき、Customer#license.nil?のような判定をすると意図していない結果になる(NullLicenseが取得できるため)
  • licenseかlicense_actualかを呼び出し側で考慮しないと行けない部分が出てくるかもしれない

といったところです。2点目の回避策としては、NullLicenseの方にLicenseインスタンスが外から呼び出されるメソッドを実装してしまうという方法があります。例えばLicense#destroyが呼ばれる場合、このようにしてダミーのメソッドを作ります。

class NullLicense
  def destroy
  # 何もしない
  end
end

この方法は、NullLicenseの方に値がないときの処理がまとまる利点がありますが、構造の把握をややこしくしてしまうという欠点もあります。

Active RecordでNull Objectパターンはややこしいコードを生んでしまうかも

Null Objectパターンを使うためにアソシエーションの張り方を歪めているので、新たな問題を生んでしまう可能性があります。

このあたりのデメリットを許容した上で、値がないときのややこしい処理がビューやコントローラーに散ってしまうことの対策として使うのがよさそうです。

RubyのNull Objectパターン

Rubyの場合はNil Objectパターンと呼ぶことになるんでしょうか?

Null Objectパターン

Null Objectパターンは、値が存在しない場合の処理を共通化したい場合に使えるデザインパターンです。

こちらの解説がわかりやすいです

Nullオブジェクトパターンの紹介と正体 - ベインのブログ

NullObjectパターン - Qiita

Rubyで実装

CutomerクラスとLicenseクラスを使って、CustomerのLicenseのname要素を表示することを考えます。CustomerがLicenseを持っているとします。Licenseは空でも良いです。

class Customer
  attr_reader :license

  def initialize(license=nil)
    @license = license
  end
end

class License
  attr_reader :name

  def initialize(name)
    @name = name
  end
end

Coustomerの持っているLicenseの名前を表示したいときは、次のようなコードを書くことになります。

driving_license = License.new('運転免許')
customer = Customer.new(driving_license)
puts customer.license.name # => 運転免許

ここで、免許が無いときは、「免許なし」と表示したいとします。 おそらく最も単純な方法は、表示時に分岐を作ることです。

driving_license = License.new('運転免許')
customer1 = Customer.new(driving_license)
customer2 = Customer.new
puts customer1.license? ? customer1.license.name : '免許なし' # => 運転免許
puts customer2.license? ? customer2.license.name : '免許なし' # => 免許なし

このコードをいろいろな場所から使おうと思うとき、下記の懸念点があります。

  • Customerを利用する場所では、lisenceが設定されていなかった場合の分岐を書かなくては行けない
  • lisenceが設定されていなかった場合にどのような処理が走るのかは、利用する側のコードを見ないとわからない

こういう問題を解消したいとき、Null Objectパターンが有効に働くことがあります。 Nullオブジェクトパターンの紹介と正体 - ベインのブログ

存在しないことを表現するLicenseクラス、NullLicenseを作ります。Customerでは、licenseが参照された際にlicenseがない場合は、NullLicenseのインスタンスを返すようにします。

class NullLicense
  def name
    '免許なし'
  end
end

class Customer
  def initialize(license=nil)
    @license = license
  end

  def license
    @license || NullLicense.new
  end
end

これで、licenseを参照する側で分岐を書く必要がなくなりました。

driving_license = License.new('運転免許')
customer1 = Customer.new(driving_license)
customer2 = Customer.new
puts customer1.license.name # => 運転免許
puts customer2.license.name # => 免許なし

毎回インスタンスを生成しなくて良くする

Customer#licenseは、コールされるたびにNullLicense.newを生成することになるので、無駄があります。NullLicenseクラスの定数として持たせることでこの無駄な処理を行わないようにできます。

class License
  Nothing = NullLicense.new
end

class NullLicense
  def name
    '免許なし'
  end
end

class Customer
  def initialize(license=nil)
    @license = license
  end

  def license
    @license || License::Nothing
  end
end

Null Objectパターンのうれしさ

Null Objectパターンを使う利点は次の通りです

  • 値が無い場合のあるオブジェクトを利用する側で、値がない場合のことを考慮した処理を書かなくて良くなる
  • 値がない場合の処理がNull Objectにまとまる。

デメリットとして考慮する必要があるのは、次のような点です。

  • Null Objectを返す処理が追加されるので、単純なプロパティの返却ではなくなる。単にプロパティを返してほしいだけだったのにインスタンスが生成されていて、パフォーマンスのボトルネックになる、ということがあるかもしれない。
  • インターフェースを共有することになるので、Null Objectを作る対象のインターフェースが追加されたらNull Objectも追従する必要がある

Null Objectパターンは、非常に便利なのですが、オブジェクトの生成のところで少し工夫が必要になります。いろいろなところに分岐が散ってしまいそうなとき有効なパターンです。

n+1クエリの自動テストが書けるgem、n_plus_one_control

N+1問題

N+1問題は、ループ内の処理を通過するたびにSQLが発行されるというパフォーマンス上の問題です。Railsにおいては、includesメソッドを使って、preloadあるいはeager loadを使って回避できます。

こちらの記事がわかりやすいです

【Ruby on Rails】N+1問題ってなんだ? - Qiita

検出方法

N+1問題の検出には、bulletがよく使われます。このgemは、N+1なクエリが発行されるとメッセージを出力してくれます。

n_plus_one_controlを使って自動テストで検出する

n_plus_one_controlというgemは、自動テストで検出することができるようにしてくれます。

Squash N+1 queries early with n_plus_one_control test matchers for Ruby and Rails — Martian Chronicles, Evil Martians’ team blog github.com

だいだいの仕組みは、複数レコードを出力する処理を複数回実行し、発行クエリの差分を見ることで無駄なクエリが発行されていないかをチェックするものです。 bulletでは検出できないパターンも検出できるようです。

n_plus_one_controlを使ってみる

対象のコード

モデル

ユーザーが部署のレコードを持つモデルを用意します(普通逆だけど)

class User < ApplicationRecord
  has_one :department
end
class Department < ApplicationRecord
  belongs_to :user
end

コントローラー

とくにひねりはなく普通に全レコード取得します。

class UsersController < ApplicationController
  def index
    @users = User.all
  end
end

ビュー

user.departmentが複数回走るので、ここで毎回クエリが発行されます。

<h1>Users</h1>

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Department</th>
    </tr>
  </thead>

  <tbody>
    <% @users.each do |user| %>
      <tr>
        <td><%= user.name %></td>
        <td><%= user.department.name %></td>
      </tr>
    <% end %>
  </tbody>
</table>

テストする

n_plus_one_controlは、複数のレコードを参照する処理を複数回実行するので、任意の数のレコードを生成する方法を知っている必要があります。populateに、任意の数nを受け取って、n個のユーザーと部署レコードを生成するコードを書いておきます。

あとは、テスト対象の処理が走るページを参照する処理をassert_perform_constant_number_of_queries(populate: populate)で囲めば準備完了です。

require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest
  setup do
    @user = users(:one)
  end

  test 'test_no_n_plus_one_error' do
    populate = lambda do |n|
      n.times do
        User.create(
          name: 'テスト',
          department: Department.new(name: 'テスト部署')
        )
      end
    end

    assert_perform_constant_number_of_queries(populate: populate) do
      get users_url
    end
  end
end

実行結果

$ rails test
# Running:

F

Failure:
UsersControllerTest#test_test_no_n_plus_one_error [/home/tanaka/n_plus_one_app/test/controllers/users_controller_test.rb:18]:
Expected to make the same number of queries, but got:
  5 for N=2
  6 for N=3
Unmatched query numbers by tables:
  departments (SELECT): 4 != 5

いいかんじに失敗してくれました。n=2のときに、5回クエリが発行されているので、n=3でも5回クエリが発行されることを期待したが、6回発行されたという旨が表示されました。すごい!

なおす

includesを使って解消します。

class UsersController < ApplicationController
  def index
    @users = User.all.includes(:department)
  end
end

これで再度テストします

$ rails test
Running via Spring preloader in process 7072
Run options: --seed 18633

# Running:

.

Finished in 1.235619s, 0.8093 runs/s, 0.8093 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

テストが通るようになりました。

大事なところで使うといいかも

n+1クエリって、改修の中でいつの間にか生まれていくという印象が強くあります。大きな機能追加のときは気をつけるんですが、細かい改修だと見落としてしまったり…。なので、この手の自動テストが書けるというのは非常に役立ちそうです。

n+1をどれだけ徹底して潰すかにもよりますが、パフォーマンスに気を使う処理の場合は、このようなテストは大変有効に働くのでは無いでしょうか。逆に、あまりどこでも使ってしまうと「早すぎる最適化」に陥ってしまうかもしれません(「早すぎる最適化は諸悪の根源」について - Qiita)。

用法用量に気をつけて、少しずつ使う機会を探っていこうと思います。

Railsの設計について思うこと

Railsと設計

Railsは、Active Recordを中心に据えてコードを書いていきます。そうなると、自然とテーブル設計に強く依存したアプリケーションになって行くので、アプリケーションが複雑になっていくとツライ!となることがあります。こういった、Railsアーキテクチャそのものに対する議論はたびたび話題に上がります。

RailsとDDD

RailsでDDDした人の話

最近、ミノ駆動さんのDDD(ドメイン駆動設計)に関する発表が界隈で話題になりました。

ドメイン駆動設計は、アプリケーションの中心にDBなどのデータストアではなく、ドメインを持ってくるという設計方法です。ヘキサゴナルアーキテクチャを使って実装されることが多いようです。長期開発を主眼においた設計手法で、大規模プロジェクトではよく使われる設計手法です。

ミノ駆動さんは、Railsの300KLOCを超える大規模なモノリシックなアプリケーションをDDDでリファクタリングするというとてつもない問題に取り組まれています。尊敬します。実際どんなアーキテクチャにしているかについても記事を書かれているので気になる方はぜひ。

ドメイン駆動設計の比類なきパワーでRailsレガシーコードなど大爆殺したるわあああ!!! - Qiita

ミノ駆動さんの主張のうち、私がRailsの設計を考える上で重要だと感じるのは次の2点です。

  • 初めからRailsでDDDをやるのはおすすめしない
  • コアドメインに対して部分的にDDDを導入すると良いのではないか

hanami

では、rubyでDDDをやりたい人はどうすればいいのでしょうか? hanamiというrubyフレームワークがあります。railsからこちらに乗り換えるべきでしょうか?

hanamirb.org

ここで、一度立ち止まって考えたいことは、いきなりDDDにゆくべきか?という点です。

偉い人はなんていっているか

エンタープライズアプリケーションの大家Martin Fowlor先生は、Railsのようなアクティブレコードを使った(所謂)犠牲的アーキテクチャと、Springのようなデータマッパーを使ったヘキサゴナルアーキテクチャはどっちがいいんだ?という議論を2014年に公開しています。時間が経ってはいますが、議論されているの対象となるRailsの性質は変わっていないため、現在でも十分聞く価値があります。

www.martinfowler.com

ものすごくざっくり要約すると下記のようなことを言っています。

  • データマッパーとアクティブレコードの間にはトレードオフがある。データマッパーは複雑だがDBとの依存を低くでき、アクティブレコードは単純だがDBに強く依存する。
  • ドメインが複雑で、アプリケーションとDBを分離する必要があるならヘキサゴナルアーキテクチャを使うべき
  • ドメインが単純ならばActive Recordが有効に働く
  • 判断基準となるのはドメインの複雑さであって、コードの量やテーブルの数ではない。ThoughtWorksで1000テーブル以上を扱うRailsアプリケーションが開発され、うまくいっていた。

Railsは、主に初期開発時のスピードをつけるために大変有効に働きます。設計が複雑になるからと言ってその利点をすんなり諦めてしまうのはもったいないように感じます。

スケールする設計

成功したプロダクトでは、長期的に見るとソフトウェアはこのような扱いを受けます。

  1. 立ち上げる
  2. 拡張する
  3. リプレイスする

Railsは、1. のフェーズにおいてはとても有効に機能します。 私がRailsのプロジェクトに関わっていて感じるのは、2. のフェーズをうまく過ごすことができれば、いきなりDDDを導入してアーキテクチャをごっそり入れ替えるみたいな大規模な回収を入れずに済むのではないか?ということです。

DDDにすれば解決する?

ヘキサゴナルアーキテクチャやDDDが良い、とされているのはアプリケーションの複雑さをアーキテクチャで吸収することでコードが単純になるから、と私は考えています。大げさに言うと、コードが単純になる代わりに、クラス設計や依存するライブラリの方に複雑さを寄せているに過ぎない、という考えです。

であれば、最初から複雑でないアプリケーションを構築するために、冗長なコードを書く必要があるのでしょうか?もちろん、最初から大規模だったり複雑になることがわかっていて、お金と人員が豊富なことがわかっているプロジェクトであればDDDを導入すべきだと思います。

管理が辛いのはRailsのせい?

Railsは、アーキテクチャに関する選択はプログラマーに委ねられている部分が少なからずあります。コントローラーとモデルの間に、どれたけクラスを配置しようが自由です。究極的には、Active RecordをDDDでいうRepositoryの実装として扱えば、Active Record、ひいてはDBへの依存を取り除き、ドメインをDBから隔離することもできます。

アプリケーションが複雑になってきたら、部分的に冗長なクラス構成に拡張していく、というようにスケールしてやればRailsでも大規模かつ複雑なアプリケーションを十分構築可能なのではないか、ということです。

最強のRailsの設計とは、自分達のアプリケーションにとっての最適を目指すこと

Railsは、普通に使っていくと柔軟性が低い。でも、最初のうちはそれでいい。アプリケーションが複雑になってきたら、その複雑さを吸収できるようなアーキテクチャに少しずつ変えていけばよいのでは、というお話でした。

Ruby on Railsがわからないときに見る場所

RailsSQLからJavaScriptまで広い範囲を扱えるため、詰まりポイントが非常に多いです。私がRailsに慣れなかったころに確認していたことを振り返ってまとめてみます。

どこがつまりポイントになるか

Railsが扱う要素技術は非常に多いので、初心者にとってはそれら一つ一つがつまりポイントになります。よくよく調べてみると、意外とRails自体で詰まることは少ないかもしれません。Railsでは、主に下記の要素技術を扱います。

たぶんもっとありますが、これら一つ一つがそれぞれ奥深い技術です。うまく動かなくてもめげずに根気よく向き合って行きましょう。

Railsの使い方がわからないとき

あまりひねったことはせず、公式のガイドとリファレンスガイド、ソースコードを見るのがわかりやすいです。Qiitaや技術系ブログの記事もわかりやすくて良いのですが、私の場合は変なつまり方をしてしまうことがあったので、できるだけ確実な一次情報を当たるようにしていました。

慣れていない機能を使うときは、まずガイドで大まかな使い方を把握します。やりたいことができそうなことが確認できたら、使いたいメソッドについてリファレンスガイドで詳しい説明を確認します。引数の渡し方や、ガイドには書いていない便利な機能、あるいは制約が書いてあることがあるので、注意して確認します。

ソースコードの方は正直かなり巨大なので、細かく読むことはめったにありませんでした。RailsActionView、ActiveModel、ActiveRecordといったモジュールに分かれていて、それらをRailtieというモジュールでつなぎ合わせています。ソースコードでは、各モジュールのREADME.mdを読んで、モジュールの担当範囲を把握したり、主要なクラスの継承関係、依存関係を把握します。この構成がわかっているとリファレンスガイドから知りたい情報にたどり着くのが圧倒的に早くなります。

Rubyの使い方がわからないとき

Rubyの方も、同じような方法で調べます。ただし、体系立てて学べる資料がインターネット上で見つけられなかったので、私は書籍から入っていきました。

英語版のリファレンスマニュアルと、日本語版のリファレンスマニュアル(通称るりま)は微妙に書いてあることが違ったりするので、両方確認していました。

Railsが思うように動かないとき

Railsのコードが思うように動かないとき、Railsがわかっていないというよりは、Railsを構成する要素技術の理解が足りないためにうまくいかないというのがほとんどです。

何が悪いのか検討もつかない場合は、フロントエンドからサーバーサイドに向かって、順を追ってどこでうまく行っていないのか確認すると良いでしょう。

DOM要素を確認する

ブラウザの開発者ツールを立ち上げて、HTMLとCSSが意図した状態になっているか確認します。JavaScriptを使っている場合は、コンソールにエラーが出ていないかも確認します。

HTML、CSSJavaScriptについて、下記のことがはっきりわかっていないときは、MDNなどで知識を補いながら確認します。

  • 期待する動作をさせるためには、HTMLはどのようになってればよいか
  • CSSが意図した要素に対して有効か
  • CSSJavaScriptソースコードがページに読み込めているか

f:id:ytnk531:20201028001803p:plain

HTTPメッセージを確認する

こちらも開発者ツールでのネットワークタブから、HTTPメッセージを確認します。

下記、足りない知識があれば補いながら確認します。HTTPについてはMDNの解説がちょうどいい粒度です。

  • HTTPレスポンスボディ、HTTPリクエストボディに意図したデータが書き込まれているか
  • HTTPレスポンスヘッダ、HTTPリクエストボディに見慣れないヘッダが付与されていないか

f:id:ytnk531:20201028002136p:plain

Railsデバッグログを確認する

rails serverを実行したターミナルに、デバッグ用のログが出力されるので、ここから下記のようなことを確認します。

  • 意図したページにリクエストが投げられたか
  • 意図したコントローラーで処理が実行されたか
  • 意図したSQL分が実行されているか

SQLについては、書籍も学習サイトも豊富にあるかと思いますので、お好きなもので学習してください。

Started GET "/" for ::1 at 2020-10-27 07:42:03 +0900
Processing by TopController#index as HTML
  Rendering top/index.html.erb within layouts/application
  Board Load (0.1ms)  SELECT "boards".* FROM "boards"
  ↳ app/views/top/index.html.erb:25
  Rendered top/index.html.erb within layouts/application (Duration: 3.7ms | Allocations: 870)
[Webpacker] Everything's up-to-date. Nothing to do
Completed 200 OK in 15ms (Views: 13.5ms | ActiveRecord: 0.1ms | Allocations: 4993)

Railsのコントローラーを確認する

デバッガーを使って、Railsの動きを止めて、変数に意図した値が入っているか確認いしてみましょう。 デバッガーの使い方はこちらが詳しいです。

Rails アプリケーションのデバッグ - Railsガイド

以下を確認します

  • 変数に意図した値が入っているか
  • request, paramsに意図した値が入っているか

終わりに

Railsは覚えることが非常に多く、エディターの補完も効く範囲が限られているので、気を抜くとすぐに迷子になってしまします。 詰まったらまずは、「自分が理解できていないことは何か」を理解することを心がけましょう。