Goで始めるMiddleware
そもそもMiddlewareとは何かと言うと、私の認識ではアプリケーションの処理の前後で何らかの処理を行ったりするもの。例えば、ログを追加したりリクエストのAPIキーを認証したりする。以下の図がわかりやすい。
Laravel 5.0 - Middleware (Filter-style) | MattStauffer.com より引用
とはいえ、上記だけでは具体的にどのように実装したらイメージがつかなかったんだけど、Goの公式のwikiでいろいろ紹介していてわかりやすかったのでこれを元に簡単なサンプルを実装した。
LearnServerProgramming · golang/go Wiki · GitHub
Middlewares in Go: Best practices and examples – Nicolas Merouze
package main import ( "fmt" "net/http" "time" ) // indexHandler ... func indexHandler(w http.ResponseWriter, r *http.Request) { fmt.Println("Hello, middleware!") } // aboutHandler ... func aboutHandler(w http.ResponseWriter, r *http.Request) { fmt.Println("This is midlleware test!!") } // loggingHandler ... func loggingHandler(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { t1 := time.Now() next.ServeHTTP(w, r) t2 := time.Now() fmt.Printf("[%s] %q %v\n", r.Method, r.URL.String(), t2.Sub(t1)) } } func main() { http.HandleFunc("/", loggingHandler(indexHandler)) http.HandleFunc("/about", loggingHandler(aboutHandler)) http.ListenAndServe(":8888", nil) }
ポイントは、
http.HandlerFunc
を返すラッパーを作ること- ラッパーの中で引数の
http.HandlerFunc
を呼び出すこと
だと個人的に思った。前述の図との対応としては、処理のイメージとしてはこんな感じだろうか。Middlewareで前処理を行って、アプリケーションの処理( next.ServeHTTP(w, r)
)を行って、Middlewareで後処理を行う。
// sampleHandler ... func sampleHandler(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // 前処理 next.ServeHTTP(w, r) // 後処理 } }
まとめ
Middlewareは怖くない
CentOSでユーザ追加
DockerでGoの環境構築する際にCentOSでユーザ追加したのでメモ。
$ useradd -m -d /home/docker -s /bin/bash docker $ echo "docker:docker" | chpasswd $ echo "docker ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
useradd
:新規ユーザの追加chpasswd
でパスワードの更新
それぞれ詳細はman
コマンドで確認できる。
追加されたユーザは/etc/passwd
ファイルで確認できる。なお、passwd
ファイルの詳細はman 5 passwd
で確認できる。
/etc/sudoers
ファイルでユーザのsudo権限を管理している。dockerで環境構築した際には上記のコマンドで設定できたが、通常ではrootでさえ編集不可のファイルとなっている。こちらもman
コマンドで詳細を確認できる。
感想
普段の作業では設定しないところだったので忘れないようにメモした。そしてman
コマンドってこんなに便利なんだってことがわかった。
Real World HTTP を読んだ
Real World HTTPを読んだ。前に読んだやつ。
HTTPについてそれが作成されてきた歴史と併せて説明されておりわかりやすかった。RFCのリファレンスにもなっており、これを通して初めてRFCの内容を読んだ。ハイパフォーマンスブラウザネットワーキングではパフォーマンスの観点からの説明だけであったのに対して、Real World HTTPでは一般的に使用される機能に関して説明してあり、HTTPの入門書としてもいいんじゃないかと思った。
- HTTP HEADERの説明が一通りあって、今までスルーしてた部分をフォローできた。
- Cache-Controlについてもう少し詳しく調べた
リクエスト時の Cache-Control、max-age=0 と no-cache の違い | tech - 氾濫原
RFC 7234 - Hypertext Transfer Protocol (HTTP/1.1): Caching
ちなみにGOによる実装の部分はスルーしたが、最近になってGoを書くようになったので改めて読み直したい。
ハイパフォーマンスブラウザネットワーキングを読んだ
TCP, TLS, HTTPといったアプリケーションよりも低いレイヤーについて基本的な構造を説明し、パフォーマンスの観点からエンジニアはどう関わるべきかが書かれている。文章は読みやすかった。読み飛ばした部分もあるが10日間ほどで読み終わった。380ページ。
以下、自分用メモ。
1章 レイテンシ・帯域幅入門
- ネットワークのパフォーマンスを決める2つの要素がある
- レイテンシ
- 帯域幅
- レイテンシは要するにパケットを送信してから受信するまでの時間。光ファイバーを通る時間やルータの処理時間などの計。
- 帯域幅は要するに速度(bps)。
- 光ファイバーを通る時間(propagation delay)は光の速さが29km/sであり、限界が決まっている。
- 感覚的に数百ミリ秒遅れると人間は遅れを認識する。
- ラストワンマイルのレイテンシ。要するにパケットが太平洋を渡っても、最後に複数のプロバイダを経由することでレイテンシが発生してしまうこと。
- ほとんどの場合、帯域幅ではなくレイテンシがパフォーマンスのボトルネック。
2章 TCPの構成要素
- TCP接続で3ウェイハンドシェイクから始まる。ここでもクライアントがデータを送信し始めるまでに1往復分のレイテンシが発生している。
- フロー制御。データの送信サイズがウィンドウ値で制限されてしまう。データ受信側が処理しきれないデータ量を送信側が送信してしまうのを防ぐための仕組み。
- スロースタート。データの送信サイズは小さい値から始まって、1往復ごとに送信サイズが大きくなっていく。ネットワーク自体の許容量を超えないようにするための仕組み。
- 上記のようにデータサイズが制限されることにより発生するレイテンシ(データを余計に送信するためデータの往復が発生するなど)がボトルネックであり、帯域幅はそんなに関係ない。
- ウィンドウ値を大きくしたり、スロースタートリスタートを無効にしたりするとパフォーマンスにはいいらしい。
- そもそもとして、CDNを利用してデータを近くでやり取りするようにするといい
3章 UDPの構成要素
- 受信側に確実に届けるための仕組みがない。このため信頼性は低い。
- その分、余計なデータの往復などが発生しないため、レイテンシの発生が少ない。
- NATの話が出てきた。ここでも処理時間(レイテンシ)が発生する
- 疲れてたので読み飛ばしながら進んだ。
4章 TLS
- HTTPSのSのやつ。暗号化、認証、データ整合性を提供する。要するに、認証局を利用して、クライアントから送信されたデータが本当にクライアントから送られてきたものかどうかを保証する。逆も同じ。
- TLSハンドシェイクから通信を始める。最大2往復。
- 1往復目で使用するTLSのバージョンや多分だけど利用できる認証局をクライアントが送信して、サーバがバージョンや認証局はこれを使います、っていうレスポンスを返す。
- 2往復目で暗号化のために共通鍵のやり取りを行う。
- TCPハンドシェイクと合わせると、最大で3往復分のレイテンシが発生する。暗号化の計算によるレイテンシは大したことないらしいので、ここがボトルネックとなる。
- パケットはすでに光速で近い速度なためここは大きなレイテンシ改善は見込めないが、CDNを利用することで距離を近くすることができる。つまり、CDNにTCP・TLSハンドシェイクを行わせることでレイテンシ改善が見込める。
- これが動的コンテンツのやり取りにおいて、キャッシュ機構は使用しないのにCDNを利用する理由。
5章 ワイヤレスネットワーク入門
- パフォーマンスは周波数帯域幅とシグナル強度に依存する
6章 WiFi
7章 モバイルネットワーク
8章 モバイルネットワークの最適化
- 読み飛ばした
9章 HTTPの歴史
- 0.9 => 1.0 => 1.1 => 2.0
- 0.9のリクエストはGETメソッドとパスの1行で構成される。めちゃシンプル
- 1.0でキャッシュ(Expires)がレスポンスで返るようになった
- 1.1でKeep-Alive接続が可能になった
- 2.0はパフォーマンス向上が主なテーマ
10章 Webパフォーマンス入門
- JavaScriptの実行はCSSにブロックされる可能性がある。
- パフォーマンスを計測できるツールを使う、そして計測する。
- ウォーターフォールチャートがパフォーマンスを計測する上で見やすい
- ブラウザがやっているパフォーマンス最適化
11章 HTTP 1.x
- キープアライブ接続がとにかく重要!
- クライアントは一度に一つのリクエストしか送れない、サーバは一度に一つのリクエストしか処理できない、といったように並列処理ができないという課題がある。HTTPのHoLブロッキング(TCPのHoLブロッキングとは別)。
- ここを解決できればパフォーマンス改善に繋がる。これをやりたいのがHTTP2.0。
12章 HTTP 2.0
- パフォーマンス向上に主眼が置かれている
- バイナリフレーミングレイヤー(?)。これでクライアントから複数のリクエストを一度に送信できるようになるらしい
- サーバからプッシュで画像などを配信できるようになるらしい
13章 アプリケーション配信最適化
- 定番のパフォーマンスベストプラクティス
14章 ブラウザネットワーク入門
- 印象に残っていない
15章 XMLHttpRequest
- ajaxで使うやつ
- CORSを気にしている
16章 Sever-Sent Events
- 読み飛ばした
- サーバ => ブラウザ
17章 WebSocket
- 読み飛ばした
- サーバ <=> ブラウザ
18章 WebRTC
- 読み飛ばした
- リアルタイム通信で使用する(Web会議など)
Webエンジニアに転職して半年が経った
Webエンジニアに転職して半年が経った。
仕事にも慣れてきたのでこれまでとこれからについて整理しようと思う。
転職前
企業向けのパッケージ開発をしていた。Web経由で使用してもらうものではなく、お客さんのとこに作ったサーバにパッケージをインストールして使ってもらう形態だった。
いわゆるオレオレフレームワークのJavaを使って、ビジネスロジックをひたすら書いてた。ちなみにDBはOracle。
人間関係とか仕事の出来とかは問題なかったけど、やはりオレオレフレームワークのJavaでは何か物足りなかった。オレオレフレームワークの上だと、クラス設計すら何も考えなくていいのでただif文書いてメソッド呼び出して、みたいなことしか書かなくてよかった。
そんな中、インターネットを覗くと、Webエンジニアたちが自分の知らない言葉でしゃべってた。gitとは、デザインパターンとは、HTTPとは、他にもたくさん。まるで話についていけない。自分自身とインターネット上のWebエンジニアを比較して、技術的なことが身についていないことが不安で仕方がなかった。もちろん、そもそも非WebとWebを単純に比較してどうなのとは今は思うが。
というわけで、将来的にエンジニアとしての技術力が足りなくて会社の外では食っていけないと思った。一番大事なのは社内ではなく社外(市場)での評価だと考えていたので、転職することを決意した。
ちなみに、技術的には物足りなかったが、技術以前に問題解決に対する考え方に関しては徹底的に教えてもらっていて、これは本当によかったと今感じている。直属だった先輩、上司にはかなりよくしてもらっていたのでありがたかった。
転職後〜今
新しい会社ではRubyでRailsを使ってサイトを作っている。同じフレームワークでもRailsだとググったら情報がたくさん出てくるし、ソースも読めるし、勉強しがいがあって楽しく過ごせている。
自分たちで運用しているので、アプリケーションを動かすサーバや下のレイヤーであるHTTP, TCP/IPなどについても知っておく必要があり、正直いくら勉強してもなかなか追いつけないでいる。それでもやはり勉強しがいがあるというのは楽しいものであると思った。
入社したての時はWebど素人であり、HTMLも若干怪しいくらいだったが、やっていけば案外なんとかなった。前職で学んだ仕事のやり方のおかげ。頭の中で考えているリスクはほとんどが必要以上にリスクとして認識されている、って話はよく聞くが本当にそうだった。そういえば、前職の先輩に「具体的にここがヤバい、って言えなければそれは大体ヤバくない。必要以上に頭がリスクとして認識しているだけ。」と言われたことがあったのを思い出した。
スキルセット
今現在、自分がわかる範囲はこんな感じだろうか。どれも雰囲気使えるレベルではあるが。
- Ruby
- Ruby on Rails
- nginx
- MySQL
こう書くとなかなか不安になるからたまに列挙するのはいいかも。
今後
次の半年だが、大きく2つのテーマで勉強する。キーワードはRubyと低レイヤー。
基本的な文法はわかったので手を動かしてとにかく書く。正直、仕事で使っているからRubyなのだが、一つの言語で大体のことできるようになると他の言語の習得も容易になるという話をよく聞くので、最初はあえてこだわってRubyを書いていく。
もう一つは低レイヤー。HTTP、TCP/IPなどなどネットワークまわりを中心に。
流行り廃りのない知識がないと将来的に死ぬ気がする。今後死にたくないので勉強する。
md2keyを使った
社内LTでスライド作るのにmd2keyを使った。便利。
太字や取り消し線はまだ使えないらしい
Rich texts · Issue #11 · k0kubun/md2key · GitHub
とのこと。
調べてみた
実装できないかなと思って、applescriptで太字や取り消し線を実現できないか調べてみた。keynote見る限り、文字のスタイルにボールドはあるから案外すぐ見つかるんじゃないかと思った。
結果としては見つけられなかった。リッチテキストのプロパティがcolor, font, size
しかなかったのでそもそもできないんじゃないか(style
みたいなのがあると期待してたがなかった)。ちなみに色やフォントはうまく指定できた。
rich text n, pl rich text : This provides the base rich text class for all iWork applications. elements contains characters, paragraphs, words. properties color (color) : The color of the font. Expressed as an RGB value consisting of a list of three color values from 0 to 65535. ex: Blue = {0, 0, 65535}. font (text) : The name of the font. Can be the PostScript name, such as: “TimesNewRomanPS-ItalicMT”, or display name: “Times New Roman Italic”. TIP: Use the Font Book application get the information about a typeface. size (integer) : The size of the font.
applescriptをあんまり深追いしたくないのでこの辺でやめる。
Working With Unix Processes を読んだ
Working With Unix Processes (English Edition)
- 作者: Jesse Storimer
- 発売日: 2011/12/20
- メディア: Kindle版
- クリック: 1回
- この商品を含むブログ (1件) を見る
この前読んだ本と同じ作者が書いている。
Working with TCP Sockets を読んだ - tnakata's blog
こちらもサンプルソースはRubyで書かれている。Ruby書いててプロセスのことまだ何もわかってない(言語化できてない)という人にはいいかもしれない。
読もうと思った理由
- プロセスの概念を言語化したいから。
読んでて勉強になった部分
- プロセスとはUnix上での処理単位。どんなプログラムもプロセスの中で実行される。
- 例えば、irbを実行するとプロセスが一つ立ち上がる。
ps
コマンドを実行することで立ち上がったプロセスの存在を確認できる。
- 例えば、irbを実行するとプロセスが一つ立ち上がる。
- Unixでは全てはFileとして扱われる。
- プロセスはforkでコピーできる(子プロセスが作られる)。並列処理に関係するところ。
- 子プロセスが終了したのに親プロセスが
Process.wait
を実行しないと、子プロセスのメモリは解放されないままになる。Zombie Processという。 - シグナルという概念がある。プロセス間、もしくはキーボードからプロセスへシグナルを送り、プロセスを終わらせることができる。
Ctrl+C
によるプロセスの強制終了やProcess.kill
- ちょっと自信がないが、ソケットを利用したやり取りとは異なる概念。
- プロセス間の通信で代表的なものはパイプとソケット。どちらもデータの流れはSTREAM(データの始まりと終わりの目印がない)。
- STREAMの反対はMESSAGE。UNIXソケットで利用される。
- Daemon Processesとはバックグラウンドで走っているプロセスのこと。
- 全てのプロセスの親としてinitプロセスがある。
- execrc(2)で現在のプロセスを他のプロセスに変えることができる。Rubyのexecはexecrc(2)のラッパー。なおプロセスを変えた後に元のプロセスに自動で戻ることはない。
- Ruby のexecは元のプロセスのfile descriptorsを全て閉じる。メモリリークを防ぐため。
- PreforkingはCopy on Writeによってメモリを効率的に使用できる。
- CoWはコピーしたプロセスを使う(たしかacceptした)タイミングでメモリがコピーされるので効率的とのこと。
読んでてよくわからなかったところは以下の通り。
- Zombie ProcessやDaemon Processが実際にどのようなときに使われるのか
- 仕事でやってることとどう結びつければよいか
Working with TCP Sockets を読んだ
Working With TCP Sockets (English Edition)
- 作者: Jesse Storimer
- 発売日: 2012/10/24
- メディア: Kindle版
- この商品を含むブログを見る
一通り読んだけど結構勉強になった。サンプルソースはRubyで書かれているから、Rubyを使っていてWebサーバとかソケットとかのことについて基礎から勉強したいって人にはいいと思う。
読もうと思った動機
以下に書いたわからないことを言語化したいから
- ネットワークプログラミングについて基礎的なことをまるで知らない。
- Railsを使っているとWebrickとかPumaとかnginxとかいろんなWebサーバが出てくるけど違いがわからない。
- そもそもWebサーバの仕組みを知らない。
勉強になった部分
- serverやclientがどのようなlifecycleを送るのかについて
- server : create, bind, listen, accept, close
- client : create, bind, connect, close
- RubyのSocketライブラリはsystem callを呼んでいること
- Befferがいい感じにデータ量を調整してくれること
- forkやthreadを利用することで並列接続を実現していること
- WebrickやPumaやnginxのような様々なサーバが存在するけど、違いは並列接続を実現させる方法だということ(ざっくり)
逆に理解できなかったところ
- non-blocking IO が理解できた気でいるけど自信ない
- Preforkingがどうして効率的なのか
- Eventedは何が何だか、Hybridも
'rails -v'コマンドは何をしているのか
‘rails -v'コマンドは何をやっているのか
rails -v
コマンドがどのような仕組みになっているか気になったので調べてみる。RailsのバージョンはRails 5.0.1
。
which rails
コマンドで実行ファイルの場所を取得する。
$ which rails ~/.rbenv/shims/rails
rbenv
を使用している場合には上記が実行ファイルとなるが、上記のファイルはただのラッパーのようである。ここでは詳細は追わないが、以下のリンクが後で理解するときに参考になりそう。
実際の実行ファイルをrbenv which rails
コマンドで取得する。
$ rbenv which rails ~/.rbenv/versions/2.3.1/bin/rails
~/.rbenv/versions/2.3.1/bin/rails
の中身を見ると、
(省略) gem 'railties', version load Gem.bin_path('railties', 'rails', version) #[1] pry(main)> Gem.bin_path('railties', 'rails') #=> "~/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/railties-5.0.1/exe/rails"
となっており、railties
というgemのrails
という実行ファイルを実行している。
/exe/rails
を見てみる。
#!/usr/bin/env ruby git_path = File.expand_path('../../../.git', __FILE__) if File.exist?(git_path) railties_path = File.expand_path('../../lib', __FILE__) $:.unshift(railties_path) end require "rails/cli"
上位のフォルダに.git
があれば、自分自身のlib
が探索されるように探索パス($:
)にパスを追加している。
そしてrequire "rails/cli"
でcli.rb
を実行している。/lib/rails/cli.rb
が読み込まれるはずだが、念のためにrequire "rails/cli"
の上の行にputs $:
を差し込んで~/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/railties-5.0.1/lib
が含まれているかどうか確認した。ちゃんとあったので多分これが読み込まれているはず。
/lib/rails/cli.rb
を見てみる。
require 'rails/app_loader' # If we are inside a Rails application this method performs an exec and thus # the rest of this script is not run. Rails::AppLoader.exec_app require 'rails/ruby_version_check' Signal.trap("INT") { puts; exit(1) } if ARGV.first == 'plugin' ARGV.shift require 'rails/commands/plugin' else require 'rails/commands/application' end
Rails::AppLoader.exec_app
を詳しく見るため、/lib/rails/app_loader.rb
を見る。
require 'pathname' require 'rails/version' module Rails module AppLoader # :nodoc: extend self RUBY = Gem.ruby EXECUTABLES = ['bin/rails', 'script/rails'] (省略) def exec_app original_cwd = Dir.pwd loop do if exe = find_executable contents = File.read(exe) if contents =~ /(APP|ENGINE)_PATH/ exec RUBY, exe, *ARGV break # non reachable, hack to be able to stub exec in the test suite elsif exe.end_with?('bin/rails') && contents.include?('This file was generated by Bundler') $stderr.puts(BUNDLER_WARNING) Object.const_set(:APP_PATH, File.expand_path('config/application', Dir.pwd)) require File.expand_path('../boot', APP_PATH) require 'rails/commands' break end end # If we exhaust the search there is no executable, this could be a # call to generate a new application, so restore the original cwd. Dir.chdir(original_cwd) and return if Pathname.new(Dir.pwd).root? # Otherwise keep moving upwards in search of an executable. Dir.chdir('..') end end def find_executable EXECUTABLES.find { |exe| File.file?(exe) } end (省略)
if exe = find_executable
は現在自分がRails::Root
にいるならtrue
、そうでないならfalse
を返す。
カレントディレクトリがRails::Root
以外
ここでは、一旦homeディレクトリにいるとして続きを見ていく。
cli.rb
に戻ると、今回のコマンドの引数は-v
なのでrequire 'rails/commands/application'
が実行されることがわかる。
/lib/rails/commands/application.rb
では、args = Rails::Generators::ARGVScrubber.new(ARGV).prepare!
が実行されている。
require 'rails/generators' require 'rails/generators/rails/app/app_generator' module Rails module Generators class AppGenerator # :nodoc: # We want to exit on failure to be kind to other libraries # This is only when accessing via CLI def self.exit_on_failure? true end end end end args = Rails::Generators::ARGVScrubber.new(ARGV).prepare! Rails::Generators::AppGenerator.start args
Rails::Generators::ARGVScrubber.new(ARGV).prepare!
を詳しく見るために、/lib/rails/generators/rails/app/app_generator.rb
を見る。
# This class handles preparation of the arguments before the AppGenerator is # called. The class provides version or help information if they were # requested, and also constructs the railsrc file (used for extra configuration # options). # # This class should be called before the AppGenerator is required and started # since it configures and mutates ARGV correctly. class ARGVScrubber # :nodoc: def initialize(argv = ARGV) @argv = argv end def prepare! handle_version_request!(@argv.first) handle_invalid_command!(@argv.first, @argv) do handle_rails_rc!(@argv.drop(1)) end end (中略) private def handle_version_request!(argument) if ['--version', '-v'].include?(argument) require 'rails/version' puts "Rails #{Rails::VERSION::STRING}" exit(0) end end
prepare!
メソッド内で呼び出しているhandle_version_request!
でRailsのバージョンを表示していることがわかった。
これでRails::Root
以外の場所からrails -v
コマンドを打った時の流れがわかった。
カレントディレクトリがRails::Root
次にRails::Root
にいる場合を見ていきます。Rails::AppLoader.exec_app
を再び見ていくと、bin/rails
をreadしている。
#!/usr/bin/env ruby APP_PATH = File.expand_path('../../config/application', __FILE__) require_relative '../config/boot' require 'rails/commands'
APP_PATH
という文字列が含まれているので、Rails::AppLoader.exec_app
の
if contents =~ /(APP|ENGINE)_PATH/ exec RUBY, exe, *ARGV
により、bin/rails
はRubyのスクリプトとして実行される。bin/rails
は../config/boot
とrails/commands
を実行する。
# ../config/boot ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) require 'bundler/setup' # Set up gems listed in the Gemfile.
詳細までは追いませんが、gemを使用可能にできるようロードパスを更新してるみたい。
Bundler: The best way to manage a Ruby application's gems
# rails/commands ARGV << '--help' if ARGV.empty? aliases = { "g" => "generate", "d" => "destroy", "c" => "console", "s" => "server", "db" => "dbconsole", "r" => "runner", "t" => "test" } command = ARGV.shift command = aliases[command] || command require 'rails/commands/commands_tasks' Rails::CommandsTasks.new(ARGV).run_command!(command)
rails
コマンドに続けて入力した引数をここで変数として取り扱っている。rails s
といったエイリアスもここで定義されている。そして、rails/commands/commands_tasks
をrequireしている。rails/commands/commands_tasks
では、
require 'rails/commands/rake_proxy' module Rails # This is a class which takes in a rails command and initiates the appropriate # initiation sequence. # # Warning: This class mutates ARGV because some commands require manipulating # it before they are run. class CommandsTasks # :nodoc: include Rails::RakeProxy attr_reader :argv (中略) def initialize(argv) @argv = argv end def run_command!(command) command = parse_command(command) if COMMAND_WHITELIST.include?(command) send(command) else run_rake_task(command) end end (中略) def version argv.unshift '--version' require_command!("application") end (中略) private (中略) def require_command!(command) require "rails/commands/#{command}" end (中略) def parse_command(command) case command when '--version', '-v' 'version' when '--help', '-h' 'help' else command end end end end
となっており、Rails::CommandsTasks
を定義している。
Rails::CommandsTasks.new(ARGV).run_command!(command)
でRails::CommandsTasks
のインスタンスを作り、run_command!
メソッドを実行している。run_command!
では、まずparse_command
でエイリアスを取り扱い、version
コマンドとしてまとめて扱うようにしている。そして、version
メソッド→require_command!
メソッドの順に実行され、最終的にrails/commands/application
を実行する。
ここから先は、Rails::Root
以外から実行した場合と同じになる。
Rubyではなぜmix-inでクラスメソッドを引き継げないのか
includeと継承の違い
include
メソッドによりモジュールをinclude(Mix-in)してそのモジュールのインスタンスメソッドを使えるようになります。クラスメソッドは使えません。
instance method Module#include (Ruby 2.4.0)
一方、継承では親となるクラスのインスタンスメソッド・クラスメソッドの両方を使えるようになります。
module Foo def foo_instance_method "foo_instance_method" end def self.foo_class_method "foo_class_method" end end class SuperBoo def boo_instance_method "boo_instance_method" end def self.boo_class_method "boo_class_method" end end class Boo < SuperBoo include Foo end boo = Boo.new boo.boo_instance_method #=> "boo_instance_method" boo.foo_instance_method #=> "foo_instance_method" Boo.boo_class_method #=> "boo_class_method" Boo.foo_class_method #=> NoMethodError: undefined method `foo_class_method' for Boo:Class
上記でBoo
クラスがFoo
モジュールをincludeしたことでBoo
クラスの継承チェーンにFoo
モジュールが組み込まれます。
Boo.ancestors #=> [Boo, Foo, SuperBoo, Object, PP::ObjectMixin, Kernel, BasicObject]
ここだけ見ると、includeは継承の一種かなと思ってしまいます。違いが出るのは特異クラスの継承チェーンを見たときです。
Boo.singleton_class.ancestors #=> [#<Class:Boo>, #<Class:SuperBoo>, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, PP::ObjectMixin, Kernel, BasicObject]
上記で確認できるように、includeしたモジュールは特異クラスの継承チェーンには出てきません。このため、includeしたモジュールのクラスメソッドを使用できません。つまり、includeと継承は一見似ているようですが全く異なる処理となります。
includeしたモジュールのクラスメソッドも使いたい
上記の通り、includeしたモジュールからはクラスメソッドを引き継げません。このため、ある責務を持ったモジュールのクラスメソッドを複数のクラスへ引き継ぎたいという場合にどうするかという問題が出てきます。この場合はincludedを使った実装をするのが一般的です。
module Foo def foo_instance_method "foo_instance_method" end def self.included(base) base.extend(ClassMethods) end module ClassMethods def foo_class_method "foo_class_method" end end end class SuperBoo def boo_instance_method "boo_instance_method" end def self.boo_class_method "boo_class_method" end end class Boo < SuperBoo include Foo end Boo.foo_class_method #=> "foo_class_method"
上記の実装はRailsでも使用されています。
なぜincludeでクラスメソッドを継承しないのか
そもそもなぜincludedを使用するようなめんどくさいことをしないとincludeしたモジュールのクラスメソッドを引き継げないのでしょうか。
上記で述べたようにincludeでクラスメソッドを引き継ぐ、という要望は普通にあるみたいです。であれば、Rubyの仕様としてincludeしたモジュールのクラスメソッドも引き継ぐというのはありだったんじゃないか、ということを考えました。その方がincludedを使用せずともクラスメソッドが使えるようになるから楽ですよね。
多重継承と同じになってしまうから、というのも考えましたがまあこれは違うかと。複数のモジュールをincludeしても継承しても、継承チェーンの中での順番は確定するからです。
とはいえ、includeでクラスメソッドを継承しないという選択をしているからには何かしらのメリットがあるということです。このメリットが何か私にはわからなかったので調べてみることにしました。
includeが使用されるそもそもの想定とは
Ruby forumの過去のスレッドで同じようなことを疑問に思った人がスレッドを立てていました。
Why the lack of mixing-in support for Class methods? - Ruby Forum
疑問に対するmatzさんの回答もありました。
https://www.ruby-forum.com/topic/68638
引用します。
Mix-in is used for several purposes and some of them can be hindered by inheriting class methods, for example, I don’t want to spill internal methods when including the Math module. I am not against for some kind of inclusion that inherits class methods as well, but it should be separated from the current #include behavior.
以下、matzさんの考えへの私の解釈です。
Mix-inが使用される想定(例えば、後述するMathモジュールみたいな使い方)がいくつかあるんだけど、クラスメソッドを継承したらそれがうまくいかない。 例えば、Mathモジュールを継承するときにクラスメソッドまで継承したら、includeしたクラスでメソッド名(名前空間)がかなり侵されてしまうのを避けたい。Mathモジュールの使い方を見ると、インスタンスメソッドとクラスメソッドにおそらく同名のメソッドを持っています。MathモジュールをincludeすればMathモジュールのクラスメソッドは不要になりますが、この時クラスメソッドも継承していると不要になったクラスメソッドの分だけincludeしたクラスでメソッド名の衝突が起きてしまう恐れがあります。