JQ Blog

Rack Application

Rack起動

Rack gemをインストール

1
$ gem install rack

config.ru

1
2
3
4
5
6
7
8
9
10
11
class ShowEnv
  def call(env)
    [
      200,
      { 'Content-Type' => 'text/plain' },
      env.keys.sort.map { |k| "#{k} = #{env[k]}\n" }
    ]
  end
end

run ShowEnv.new

起動

1
$ rackup

起動確認

  • ブラウザでlocalhost:9292で接続
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
GATEWAY_INTERFACE = CGI/1.2
HTTP_ACCEPT = text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
HTTP_ACCEPT_ENCODING = gzip, deflate, br
HTTP_ACCEPT_LANGUAGE = ko,ja;q=0.9,en-US;q=0.8,en;q=0.7
HTTP_CACHE_CONTROL = max-age=0
HTTP_CONNECTION = keep-alive
HTTP_HOST = localhost:9292
HTTP_SAVE_DATA = on
HTTP_UPGRADE_INSECURE_REQUESTS = 1
HTTP_USER_AGENT = Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.45 Safari/537.36
HTTP_VERSION = HTTP/1.1
PATH_INFO = /
QUERY_STRING = 
REMOTE_ADDR = ::1
REQUEST_METHOD = GET
REQUEST_PATH = /
REQUEST_URI = /
SCRIPT_NAME = 
SERVER_NAME = localhost
SERVER_PORT = 9292
SERVER_PROTOCOL = HTTP/1.1
SERVER_SOFTWARE = puma 3.11.2 Love Song
puma.config = #<Puma::Configuration:0x007ff652a60680>
puma.socket = #<TCPSocket:0x007ff652a09cb8>
rack.after_reply = []
rack.errors = #<Rack::Lint::ErrorWrapper:0x007ff651039450>
rack.hijack = #<Proc:0x007ff6510396f8@/Users/jo/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/rack-2.0.4/lib/rack/lint.rb:525>
rack.hijack? = true
rack.input = #<Rack::Lint::InputWrapper:0x007ff651039478>
rack.multiprocess = false
rack.multithread = true
rack.run_once = false
rack.tempfiles = []
rack.url_scheme = http
rack.version = [1, 3]

Rack Applicationの仕様

Rackアプリケーションの形式

  • callメソッドに応答するオブジェクト
  • callの引数envはCGI環境変数(ハッシュ)
  • callの戻り値は[HTTPステータス番号, レスポンスヘッダ, ボディ]の形式
    • ステータス番号は3桁の整数
      • より正確にはto_iに応答してステータスを返すオブジェクト
    • レスポンスヘッダはハッシュ
      • より正確にはeachイテレータに応答しkey, valueの組をyieldするオブジェクト
    • ボディは次の3つのバリエーションのどれかひとつ
      • 文字列が要素の配列([文字列, 文字列, … ])(上の例ではこれを使用)
      • to_pathメソッドを持つオブジェクト(ファイルを返す)
      • ストリーム(File-like)オブジェクト(ストリームから読んで返す)

オブジェクト生成方法の種類

  • インスタンスメソッドとして定義する場合
  • 特異メソッドとして定義する場合
  • lambdaを用いる場合

インスタンスメソッドとして定義する場合

1
2
3
4
5
6
7
class Foo
  def call(env)
    [200, {'Content-Type' => 'text/plain'}, ['Hello']]
  end
end

run Foo.new

特異メソッドとして定義する場合

1
2
3
4
5
6
7
class Foo       # module Foo も可
  def self.call(env)
    [200, {'Content-Type' => 'text/plain'}, ['Hello']]
  end
end

run Foo

lambdaを用いる場合

1
2
run lambda {|env| [200, {'Content-Type' => 'text/plain'}, ['Hello']] }
# run proc {|env| ... } でも同じ

lambda(またはproc)の実行メソッドがProc#callで、名前がRack仕様のcallと同じため実行できるという仕組みになっている。

Rack Applicationの構成

一般的には複数のRackアプリケーションを連結した構成を取る。典型的なRackアプリケーションの模式図は下記のようになる。

1
endpoint <-> middleware[s] ... <-> (Rack handler) <-> (server)
  • エンドポイント
  • ミドルウエア
  • Rackハンドラ

エンドポイント

エンドポイントは(サーバから一番離れた)末端のRackアプリケーション。典型的にはWebアプリケーションそのものになる。RailsアプリケーションもRackエンドポイントである。 下記はRailsアプリケーションのconfig.ru

1
2
3
・・・

run Rails.application

Rackに含まれているエンドポイントアプリケーションをいくつか示す。

  • Rack::File - 静的ファイルサーバ(ディレクトリリスティングなし)
  • Rack::Directory - 静的ファイルサーバ(ディレクトリリスティング付き)
  • Rack::Lobster - 動作確認用サンプル

ミドルウエア

ミドルウエアはエンドポイントとサーバの中間に位置するRackアプリケーション。典型的なミドルウエアとは次のようなものになる。

  • callはクラスのインスタンスメソッドとして記述する
  • コンストラクタ引数に上流(Webアプリケーション側)Rackアプリケーションを取る

ミドルウエアは中間の各種フィルタ処理を担当する。Rack gemに含まれるRackアプリケーションの大部分はこのミドルウエアである。これも主なものからいくつか示す。

  • Rack::Static - 静的ファイルサーバ(各種フィルタ機能付き)
  • Rack::Deflater - 圧縮エンコーディング対応
  • Rack::ETag - HTTP ETagを処理
  • Rack::ConditionalGet - If-None-Match及びIf-Modified-Sinceの対応
  • Rack::MockRequest - テスト用モック(実際のHTTPを使わない)
  • Rack::Lint - プロトコルの実行時チェック(開発時は自動的に挿入される)
  • Rack::ShowExceptions - 例外時に詳細情報を表示(開発時は自動的に挿入される)
  • Rack::ContentLength - Content-Lengthをセットする
  • Rack::Cascade - 複数アプリケーションの分岐

なお「ミドルウエア」という用語はRackアプリケーション全般という意味でも使われている(広義のミドルウエア)。

Rackハンドラ

環境や用途の違いによりthin/puma/webrick/mongrelなど様々のサーバの選択がある。この環境の違いを吸収する部分がRackハンドラである。Rubyベースの環境で用いられるサーバにはRackハンドラが用意されており、それぞれ仕様の異なるサーバに対してRackの共通仕様を提供する。
開発環境でrackupを起動する時、rackup -s webrickのように立ち上げるサーバ種類を選択することもできる。
config.ruにサーバの種類やポート番号などを直接設定することも可能。

1
Rack::Handler::WEBrick.run Foo.new, Port: 9292

Rack DSL

Rackアプリケーションの構成はconfig.ruに記述するけど、この際用いられるのがRack DSLと呼ばれる記法である。これはRack::Builderモジュールで実装されている。 ここで次のような構成を持っているアプリケーションを作ってみる。

1
Rack::Directory <-> Rack::Deflater <-> Rack::ETag <-> (Rack handler/server)

Rack::Directoryは指定するディレクトリを静的ファイルサーバとして立ち上げる。
Rack::Deflaterは圧縮エンコーディングに対応する。
Rack::ETagでHTTPヘッダにETagフィールドをセットする。

config.ruを修正する。

1
2
3
use Rack::ETag
use Rack::Deflater
run Rack::Directory.new 'public'

ここでミドルウエアとエンドポイントの違いが重要になる。

  • ミドルウエアは接続順にuseで記述する
  • エンドポイントは最後にrunで記述する

useを使わずにrunだけで記述することも可能だが、下記のようにコードが増える。この程度だとまだ十分だけど、Ruby on Railsのように多数のミドルウエアを直列接続することを考えたらuseを使った方が良いであろう。

1
2
3
4
5
6
# useをrunに書き換えた場合
run Rack::ETag.new(
  Rack::Deflater.new(
    Rack::Directory.new 'public'
  )
)

上記のアプリケーションはpublickディレクトリを静的ファイルサーバとして立ち上げる。
directory

Rack::Staticを用いたらディレクトリ(URLの最後が'/‘)に対するアクセスに対してもディレクトリリスティングではなく、静的ページに対応できる。

1
use Rack::Static, urls: [''], root: 'public', index: 'test1.txt'

Rack::Staticは実質的にエンドポイントの機能を持っているが、コンストラクタの第一引数がappなのでエンドポイントにはならない。そこで空のエンドポイントを作って対応する。

1
2
3
4
5
6
7
8
9
class Static

    def initialize(app, options={})
      @app = app
      @urls = options[:urls] || ["/favicon.ico"]

  ・・・

end

rack/static.rb at master · rack/rack · GitHub

1
2
3
4
5
6
7
8
9
10
# 空のエンドポイント
class NullEndPoint
  def call(env)
  end
end

use Rack::ETag
use Rack::Deflater
use Rack::Static, urls: [''], root: 'public', index: 'test1.txt'
run NullEndPoint.new

指定したtest1.txtが表示される。
Alt text

参考

Rack解説 - Rackの構造とRack DSL - Qiita

ActiveRecordの中身

動的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
require 'activerecord'
ActiveRecord::Base.establish_connection :adapter => "sqlite3",
                                        :database => "dbfile"

ActiveRecord::Base.connection.create_table :tasks do |t|
  t.string   :description
  t.boolean  :completed
end

class Task < ActiveRecord::Base; end

task = Task.new
task.description = 'ブログの記事作成'
task.completed = true
task.save
task.description # => "ブログの記事作成"
task.completed?  #=> true

上記のコードに属性にアクセスする4つのメソッドを呼び出している。(description=()、completed=()、description()、completed?())
しかし、これらの「属性アクセサ」はTaskクラスの定義にはない。
では、このアクセサはどこからきたものなんだろうか。

ActiveRecord::Base#method_missing()

実際にactive_record/attribute_methods.rbではActiveModel::AttributeMethodsincludeしてmethod_missing()を使っている。method_missing()が呼び出されたら該当メソッドがあるかどうかを判断し、ゴーストメソッドを生成する。そのゴーストメソッドはゴーストアクセサになり、動的に属性にアクセスすることができる。

動的ファインダ

1
2
3
4
5
6
7
8
9
10
task = Task.find(:first, :conditions => {:completed => true})
task.description # => "ブログの記事作成"

task = Task.find_by_description('ブログの記事作成')
task.id # => 1

Task.find_all_by_completed(true)
Task.find_by_description_and_completed('ブログの記事作成', true)
Task.find_or_create_by_description('ランチターイム')
Task.find_by_description!('帰宅')

ActiveRecord::Base.findfind_by_<attr>のように属性名を含むメソッドでの検索もできる。
and, or での連結などもできる。
これも動的属性と同じく「ゴーストメソッド」で実装している。
ここでもやはり最初にmethod_missingでメソッドを定義して次からは直接呼び出せるようにしている。

参照

rails/activerecord at master · rails/rails · GitHub

クラス拡張ミックスインのサンプル作り

課題

  • 学校の先生、学生のリスト出力
    • 全員のリスト
    • データを指定

先生、学生のリスト出力

  • クラスメソッドを使って取り出す

指定データ出力

  • インスタンスメソッドを使って取り出す

前提

  • 先生、学生のデータはそれぞれmixin_samples/students_data.rbmixin_samples/teachers_data.rbを使う
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module MixinSamples
  module StudentsData
    def self.included(base)
      base.extend self
    end

    def data
      [
        { age: 18, name: 'Suzuki', japanese: 70, math: 80, science: 90, english: 70 },
        { age: 17, name: 'Sato', japanese: 60, math: 70, science: 50, english: 50 },
        { age: 18, name: 'Takahashi', japanese: 90, math: 90, science: 100, english: 70 },
        { age: 16, name: 'Tanaka', japanese: 80, math: 60, science: 70, english: 90 }
      ]
    end
  end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module MixinSamples
  module TeachersData
    def self.included(base)
      base.extend self
    end

    def data
      [
        { age: 46, name: 'Ito', teaching_subject: '国語', teaching_target: '1年生' },
        { age: 38, name: 'Watanabe', teaching_subject: '数学', teaching_target: '2年生' },
        { age: 25, name: 'Yamamoto', teaching_subject: '科学', teaching_target: '3年生' },
        { age: 32, name: 'Nakamura', teaching_subject: '英語', teaching_target: '3年生' }
      ]
    end
  end
end

Sample code

出力のためのTotalPrintPersonalPrintモジュールを定義する

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module MixinSamples
  module TotalPrint

    def print_data
      puts convert_data
    end

    def print_names
      puts names
    end

    private

    def convert_data
      data.map { |val| val.map { |k, v| "#{k.capitalize}: #{v}" }.join(' | ') }
    end

    def names
      data.map { |val| val.values_at(:name) }.flatten.join("\n")
    end
  end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
module MixinSamples
  module PersonalPrint

    def to_print
      puts person_info
    end

    private

    def person_info
      args.map { |k, v| "#{k.capitalize}: #{v}" }.join(' | ')
    end
  end
end

データ指定のためのSearchモジュールを定義する

1
2
3
4
5
6
7
8
module MixinSamples
  module Search
    def search(name)
      objs = data.map { |hash| hash.values_at(:name).include?(name) ? hash : nil }.compact
      objs.map { |obj| new(obj) }
    end
  end
end

モジュールをMixinするスーパークラスを定義する

1
2
3
4
5
6
7
8
9
10
11
12
13
module MixinSamples
  class SchoolMember
    attr_accessor :args

    include PersonalPrint
    extend TotalPrint
    extend Search

    def initialize(args)
      @args = args
    end
  end
end

実際の本体である先生、学生のクラスを定義する

1
2
3
4
5
module MixinSamples
  class Teacher < SchoolMember
    include TeachersData
  end
end
1
2
3
4
5
module MixinSamples
  class Student < SchoolMember
    include StudentsData
  end
end

結果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
MixinSamples::Teacher.print_data
=>
Age: 46 | Name: Ito | Teaching_subject: 国語 | Teaching_target: 1年生
Age: 38 | Name: Watanabe | Teaching_subject: 数学 | Teaching_target: 2年生
Age: 25 | Name: Yamamoto | Teaching_subject: 科学 | Teaching_target: 3年生
Age: 32 | Name: Nakamura | Teaching_subject: 英語 | Teaching_target: 3年生

MixinSamples::Student.print_data
=>
Age: 18 | Name: Suzuki | Japanese: 70 | Math: 80 | Science: 90 | English: 70
Age: 17 | Name: Sato | Japanese: 60 | Math: 70 | Science: 50 | English: 50
Age: 18 | Name: Takahashi | Japanese: 90 | Math: 90 | Science: 100 | English: 70
Age: 16 | Name: Tanaka | Japanese: 80 | Math: 60 | Science: 70 | English: 90

teachers = MixinSamples::Teacher.search 'Ito'
=> [<MixinSamples::Teacher:0x007fd7b1682910 @age=46, @name="Ito", @teaching_subject="国語", @teaching_target="1年生">]

teachers.map { |teacher| teacher.to_print }
=>
Age: 46 | Name: Ito | Teaching_subject: 国語 | Teaching_target: 1年生

students = MixinSamples::Student.search 'Suzuki'
=> [<MixinSamples::Student:0x007fd7b1506500 @age=18, @args={:age=>18, :name=>"Suzuki", :japanese=>70, :math=>80, :science=>90, :english=>70}, @english=70, @japanese=70, @math=80, @name="Suzuki", @science=90>]

students.map { |student| student.to_print }
=>
Age: 18 | Name: Suzuki | Japanese: 70 | Math: 80 | Science: 90 | English: 70

ActiveRecordの設計

ActiveRecordの設計

ActiveRecordはRailsの一部であり、Rubyのオブジェクトをデータベースのテーブルにマッピングするものである。この機能をオブジェクトリレーショナルマッピングと呼ぶ。
ActiveRecordはRailsアプリケーションでは、モデル(MVCの「M」)として使われる。通常のRubyのオブジェクトでビジネスロジックを管理し、ActiveRecordでオブジェクトをデータベースに永続化することになる。

ActiveRecordの簡単な例

1
2
3
class Duck < ActiveRecord:Base
  validates_length_of :name, maximum: 6
end

上記の例では、6文字より長いnameをデータベースに保存しようとすると、例外を発生するか(save!の場合)、何も言わずに失敗する(saveの場合)。
ActiveRecordは規約に基づいて、自動的にDuckクラスをducksテーブルにマッピングしてくれる。データベーススキーマからducksテーブルにnameが含まれることを認識し、属性にアクセスするゴーストメソッドを定義する。

1
2
3
4
5
6
7
8
9
10
my_duck = Duck.new
my_duck.name = "Donald"
my_duck.valid? # => true
my_duck.save!


some_duck = Duck.first
some_duck.id   # => 1
some_duck.name # => "Donald"
some_duck.delete

のように使われる。

ActiveRecord::Base

1
2
3
4
5
6
7
8
9
10
# gems/activerecord-x.x.x/lib/active_record.rb
module ActiveRecord
  autoload :Base, 'active_record/base'
  autoload :Batches, 'active_record/batches'
  autoload :Calculations, 'active_record/calculations'
  autoload :Callbacks, 'active_record/callbacks'

  ・・・

end

ActiveRecordautoload()を使って各モジュールを読み込んでいる。Kernel#autoload()はモジュール名とファイル名を受け取り、モジュールを最初に参照した時にファイルを自動的に読み込む。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module ActiveRecord
  class Base
    class << self
      def find(*args) # ...
      def first(*args) # ...
      def last(*args) # ...
      # ...
    end

    public
      def id # ...
      def save # ...
      def save! # ...
      def delete # ...
      # ...
  end
end

ActiveRecord::Baseには上記のコードのようにクラスメソッド、インスタンスメソッドを定義している。

ActiveRecord::Validations

先ほど使ってたvalid?()validates_length_of()などのメソッドはActiveRecord::Validationsモジュールで定義されている。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
module ActiveRecord
  module Validations
    def self.included(base)
      base.extend ClassMethods
      base.class_eval do
        alias_method_chain :save, :validation
        alias_method_chain :save!, :validation
      end
      base.send :include, ActiveSupport::Callbacks
      # ...
    end

    module ClassMethods
      def validate_each(*attrs) # ...
      def validates_confirmation_of(*attr_names) # ...
      def validates_length_of(*attrs) # ...
      # ...
    end

    def save_with_validation(perform_validation = true) # ...
    def save_with_validation! # ...
    def valid? # ...
    # ...
  end
end

ActiveRecord::BaseActiveRecord::Validationsをインクルードしていて、さらにフックメソッドを通してクラスメソッドも手に入れている。これはクラス拡張ミックスインである。ActiveRecord::Baseはモジュールのインクルードを繰り返して、メソッドを積み上げていく。

alias_method_chain()

1
2
3
4
5
6
7
class MyClass
  def greet
    puts "Hello!"
  end
end

MyClass.new.greet # => Hello!

例えば、このgreetメソッドの周りにロギング機能をつけたいとする場合、アラウンドエイリアスを使えば良い。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyClass
  def greet_with_log
    puts "メソッド呼び出し開始.."
    greet_without_log
    puts "...メソッド呼び出し完了"
  end

  alias_method :greet_without_log, :greet
  alias_method :greet, :greet_with_log
end

MyClass.new.greet
# =>
"メソッド呼び出し開始.."
"Hello!"
"...メソッド呼び出し完了""

このような仕組みをRailsではModule#alias_method_chain()を使って使用できる。これはActiveSupportライブラリで提供されている。

1
2
3
4
5
6
7
8
9
10
11
12
13
module ActiveRecord
  module Validations
    def self.included(base)
      base.extend ClassMethods
      base.class_eval do
        alias_method_chain :save, :validation
        alias_method_chain :save!, :validation
      end

  ・・・

  end
end

上記のコードではActiveRecord::Baseクラスを再オープンして、save()save!()に検証機能をつけている。これでオブジェクトをデータベースに保存した時に自動的に検証を行ってくれる。

ActiveRecordのまとめ

ActiveRecord::BaseActiveRecordのメインとなるクラスだ。インスタンスメソッドとクラスメソッドが定義されているが、それ以外にもActiveRecord::Validationsのような追加モジュールをインクルードしている。モジュールをインクルードする際、クラス拡張ミックスインをして、さらに別のモジュールをインクルードする。こうしたモジュールはalias_method_chain()を使ってさらにいろんなメソッドを生み出す。このように次々にメソッドが定義されて、ActiveRecord::Baseは巨大なクラスになっていく。

コードを記述するコード

eval()

eval()を使うとコード文字列を使って処理できる。例えば、

1
2
3
array = [10, 20]
element = 30
eval("array << element") # => [10, 20, 30]

evalの問題点

evalが評価するコード文字列内でもローカル変数にアクセスできる。 そういう意味ではブロックと似ている。

1
2
v1 = 1
eval("v1") # => 1

evalとブロックどちらを使うのが良いのか。
答えは「ブロック」らしい。コード文字列(evalを用いた処理記述)は問題点がある。

コードインジェクション

外部からコード文字列を読み込んだ際に、悪意のあるコードを読み込んでしまい、
プライベート情報漏洩やハードディスクの情報を消されたりしてしまう。

1
2
3
4
5
6
7
8
9
def my_method(method)
  eval("'a'.#{method}")
end

# 悪意のあるコード プライベートな情報が表示されてしまう。
my_method(object_id; Dir.glob("*")

'a'.object_id
Dir.glob("*")

オブジェクトの汚染とセーフレベル

Rubyは安全でないオブジェクトに汚染マークを付ける。
webフォーム、コマンドライン、システム変数の読み込み文字列などを含んでいた場合、安全でないと判断する。

1
2
3
4
5
6
user_input = "User input: #{gets()}"
# 汚染されているかどうかをチェック
puts user_input.tainted?

x = 1
true

汚染マークを検知して、例外を発生させるセーフレベルという仕組みがある。
潜在的に危険な操作に対して制限をかけられる。

1
2
3
4
5
6
7
# セーフレベルを1に設定
user_input = "User input: #{gets()}"
eval user_input

x = 1
# セーフレベル1以上の場合、汚染した文字列を評価できない
SecurityError: Insecure operatiion - eval

フックメソッド

さまざまなイベントを契機に処理を行うことができる。(継承、拡張など)

1
2
3
4
5
6
7
8
9
10
11
12
13
module MyModule
  # includedがフックメソッド 
  def self.included(othermod)
    # MyModuleが拡張されると下記が処理される
    puts "MyModuleは #{othermod}にmixinされた"
  end
end

class C
  include MyModule
end

# => MymoduleはCにmixinされた

その他のフックメソッド

  • Class#inherited()
  • Module#extend_object()
  • Module#method_added()
  • Module#method_removed()
  • Module#method_undefined()

などをオーバーライドすれば、そのイベントに関連したメソッドを実行できる。

参考

http://tamata78.hatenablog.com/entry/2015/09/19/172049

クラス定義②

特異クラス

特異メソッドはどのクラスに所属しているのか。
特異メソッドの定義クラスにおけるインスタンスメソッドだと全インスタンスに引き継がれてしまう。
Objectクラスだと全クラスに引き継がれてしまう。ではどこに所属しているのか。 特異クラスという特殊なクラスに所属している。

特異クラスのオープン

1
2
3
class << an_object
 #処理を記述
end

特異クラスの参照を取得

1
2
3
4
5
eigenclass = class << obj
  self
end

eigenclass.class # => Class

特異クラスの特別なところ

  • インスタンスを1つしか持てない(だからシングルトンクラスとも呼ばれる)
  • 継承ができない
  • 特異クラスはオブジェクトの特異メソッドが住んでいる場所

クラスメソッドの構文

この前の記事でクラスメソッドを定義する方法の2つを書いてたけど、もう1つがある。

1
2
3
4
5
6
7
8
class MyClass
  class << self
    def my_method; puts "Hello!" end
  end
end

MyClass.my_method # => "Hello!"
end

クラスの属性を定義する

1
2
3
4
5
6
7
8
9
class MyClass
  # これだとインスタンスの属性定義になってしまう
  attr_accessor :a
end

MyClass.a # => NoMethodError: undefined method `a' for MyClass:Class
obj = MyClass.new
obj.a = 1
obj.a # => 1
1
2
3
4
5
6
7
8
class MyClass
  class << self
    attr_accessor :c
  end
end

MyClass.c = 'It works!'
MyClass.c # => "It works!"

クラス拡張

moduleのインスタンスメソッドをクラスメソッドとして定義させるとき
クラス拡張という方法で実現できる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module MyModule
  def my_method
    puts 1
  end
end

class MyClass
  class << self
    # 特異クラス内でインスタンスメソッドを拡張させるとクラスメソッドとして定義可能
    include MyModule
  end
end

MyClass.my_method # => 1

Object#extendを用いれば、もっとシンプルに定義できる。

1
2
3
4
5
class MyClass
  extend MyModule
end

Myclass.my_method # => 1

エイリアス

1
alias :新しい名前 :古い名前
  • aliasはキーワードであり、メソッドではいので二つの間にカンマはいらない
  • 参照を向けるのではなく、コピーする。だから、aliasコマンドの後で元となるメソッドに変更が加えられても、aliasによって作成されたメソッドには影響がない
1
2
3
4
5
6
7
8
class MyClass
  def my_method; "my_method()"; end
  alias :my_method2 :my_method
end

m = MyClass.new()
m.my_method # =>  "my_method()"
m.my_method2 # =>  "my_method()"

のように使える。

aliasの用途

元のメソッドを修正したいが、外部ライブラリなどで修正できない場合がある。
その場合は、元メソッドを別名定義し、元メソッド名でメソッドを再定義する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyClass
  def my_method
    puts "original"
  end

  alias :my_method2 :my_method

  def my_method
    puts "changed"
  end
end

mc = MyClass.new
mc.my_method # => changed
mc.my_method2 # => original

参照

http://tamata78.hatenablog.com/entry/2015/09/16/205224

クラス定義①

知っておくべきこと

Rubyにおけるクラスとは

Rubyにおけるクラス定義は「コードを実行する」ということだ。JavaやC#ではクラスを定義してもそのクラスのオブジェクトを生成して、メソッドを呼び出すまで何も起きない。しかし、Rubyでクラスを定義するということはオブジェクトの動作を規定することではなく、コードを実行するということなのだ。

クラスインスタンス変数 & クラス変数

クラスインスタンス変数

1
2
3
class MyClass
  @my_var = 1
end

こういうふうにクラスにもインスタンス変数が定義できる。ここの@my_varはクラス定義の中にあるのでクラスに属している。
だからクラスのオブジェクトのインスタンス変数とは別物ということだ。

1
2
3
4
5
6
7
8
9
10
11
12
class MyClass
  @my_var = 1

  def self.read; @my_var; end
  def write; @my_var = 2; end
  def read; @my_var; end
end

obj = MyClass.new
obj.write
obj.read     # => 2
MyClass.read # => 1

上記のコードには2つのインスタンス変数を定義している。どちらも@my_varという名前だ。しかし、それぞれ異なるスコープに定義されており、別々のオブジェクトに属している。上記のコードのMyClassのオブジェクトのobjMyClassクラスのインスタンス変数の@my_varは呼べない。同じくobjの外側からもobjのインスタンス変数の@my_varが呼べないのだ。
こういうふうにクラスがselfとなる場所に定義されているインスタンス変数をクラスインスタンス変数と呼ぶ。

クラス変数

クラス変数は@@をつけて定義できる。

1
2
3
class C
  @@v = 1
end

これはクラスインスタンス変数とは違う。サブクラスや通常のインスタンスメソッドからもアクセスできる。

1
2
3
4
5
class D < C
  def my_method; @@v; end
end

D.new.my_method # => 1

しかし、クラス変数の使いには少し気をつけないといけないところがある。

1
2
3
4
5
6
7
@@v = 1

class MyClass
  @@v = 2
end

@@v # => 2

@@vの値が変わるのはこのクラス変数がクラスではなく、クラス階層に属しているからだ。@@vmainのコンテキストに定義されているので、mainのクラスであるObjectの全ての子系にも属しているということになり、全部同じクラス変数を共有しているからだ。

特異メソッド

特異メソッド

Rubyでは、特定のオブジェクトにメソッドを追加できる。

1
2
3
4
5
6
7
8
9
str = "just a regular string" # 普通の文字列

def str.title?
  self.upcase == self
end

str.title?                 # => false
str.methods.grep(/title?/) # => ["title?"]
str.singleton_methods      # => ["title?"]

上記のコードはtitle?メソッドを文字列strに追加している。Stringクラスの他のオブジェクトには影響はない。
こうしたあるオブジェクトに特化したメソッドのことを特異メソッドと呼ぶ。

クラスメソッドの真実

クラスメソッドはクラスの特異メソッドである。

1
2
3
4
5
6
7
# 特異メソッドの定義
def MyClass.class_method; end

# クラスメソッドの定義
class MyClass
  def self.class_method; end
end

上記の2つの定義は同じものなのだ。

1
2
3
def object.method
  # メソッドの中身
end

上記のobjectの部分にはオブジェクトの参照、クラス名の定数、selfのいずれかが使える。つまり、クラスメソッドはselfを使った特異メソッドである。

クラスマクロ

Module#attr_*()族のメソッドを使えば、一気にオブジェクトに対するアクセサを生成できる。
Module#attr_readerは読み取り用、Module#attr_writerは書き込み用、Module#attr_accessorは読み書き両用だ。
コードで見てみると

1
2
3
4
5
class Person
end

person = Person.new
person.name # => NoMethodError: undefined method `name' for #<Person:0x007f88d1cbbb58>

NoMethodErrorが出てしまう。

1
2
3
4
5
6
7
8
9
class Person
  def name
    @name
  end
end

person = Person.new
person.name        # => nil
person.name = 'Jo' # => NoMethodError: undefined method `name=' for #<Person:0x007f88d1ede5e8>

nameのメソッドは読み込めるが、アクセスはできない。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person
  def name
    @name
  end

  def name=(str)
    @name = str
  end
end

person = Person.new
person.name        # => nil
person.name = 'Jo' # => "Jo"
person.name        # => "Jo"

これでインスタンス変数の@nameにアクセスできる。
これはこうもかける。

1
2
3
4
class Person
  attr_reader :name
  attr_writer :name
end

そして、

1
2
3
4
5
6
7
8
9
10
11
class Person
  attr_accessor :name

  def greeting
    "Hello, #{@name}"
  end
end

person = Person.new
person.name = 'Jo' # => "Jo"
person.greeting    # => "Hello, Jo"

こう使える。
このようなメソッドをクラスマクロと呼ぶ。
クラスマクロはキーワードのように見えるが、クラス定義の中で使えるクラスメソッドである。

&Check

&Checkでのクラスメソッド

formクラスでよくクラスメソッドを使っている。

1
2
3
4
5
6
7
8
9
10
11
class CandidatePanelCsvForm

    ・・・

  def self.model_name
    ActiveModel::Name.new(self, nil, "CandidatePanelCsvForm")
  end

    ・・・

end

&Checkでのクラスマクロ

form, service, usecaseなどで幅広く使われている。(attr_accessor, attr_reader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
module Csv
  class ResponseSetsService
    attr_accessor :filer

    def initialize(resource_owner, survey, **args)
      @resource_owner = resource_owner
      @survey         = survey
      @survey_group   = survey.group
      @args           = args
      @filer          = nil
    end

    ・・・

  end
end

module Missions
  class RecipientService
    attr_reader :burden

    delegate :commenter, to: :burden

    def initialize(burden)
      @burden = burden
    end

    ・・・

end

全然関係ないけど気になるやつ

delegate

上記のコードの中でdelegateというのがあったけど、delegateは何物か気になったから調べておく。
delegatehas_one, throughと似たようなもので、toで指名しているオブジェクトからの関係あるモデルを呼び出せるらしい。
上記ではto:burdenを指名しているから、:burdenの関係モデルの中のcommenterを持ってこれる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Qualitative::SurveyGroupComment < ApplicationRecord

  ・・・

  private

  def notify_related_actors
    recipient_service = Missions::RecipientService.new(self)

    ・・・

  end

  ・・・

end

上記のコードでわかるように渡されるburdenQualitative::SurveyGroupCommentのオブジェクトである。
だから、delegateを使ってQualitative::SurveyGroupCommentオブジェクトの関係モデルのcommenterオブジェクトを呼び出すことができるということだ。

1
Missions::RecipientService.new(self).commenter

のように使える。

次の記事

第4章 クラス定義の残った分量の

  • 特異クラス
  • アラウンドエイリアス

あと、

  • 第5章「コードを記述するコード」

ブロック②

instance_eval

instance_evalとは

フラットスコープよりもっと簡単にコードと束縛を好きなように組み合わせる方法がある。instance_evalメソッドだ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class MyClass
  def initialize
    @v = 1
  end

  def my_method
    private_method
  end

  private

  def private_method
    puts @v
  end
end

obj = MyClass.new
obj.my_method # => 1

v = 2
obj.instance_eval do
  @v = v
  def private_method
  puts @v + 1
end

obj.my_method # => 3

ブロックはレシーバをselfとしたコンテキストで評価されるので、ブロックからレシーバのプライベートメソッドや@vなどのインスタンス変数にアクセスできる。
instance_eval()に渡したブロックをコンテキスト探索機と呼ぶ。

カプセル化の破壊

このコンテキスト探索機を使うとカプセル化を破壊できてしまう。でもこのコンテキスト探索機が必要になる時がある。例えば、irbからオブジェクトの中身をすぐにみたいとかの場合もある。こういう場合、そのオブジェクトに入っていくにはinstance_evalが最も近道になる。
テスト環境で何かのオブジェクトをテストする時も、データベースにアクセスするメソッドをシンプルな定数値と入れ替えることができる。

呼び出し可能オブジェクト

呼び出し可能オブジェクトに対しては前記事を書いたので、簡単にまとめてみる。
前の記事
ブロックの使用は2つに分けられる。まず、コードを保管する。それから、そのブロックを呼び出して実行する。でも、「コードを保管して、あとで呼び出す。」方式はブロックに限らない。Rubyではコードを保管できる場所が他に3つがある。

  • Procの中。(ブロックがオブジェクトになったもの)
  • lambdaの中。(Procの変形)
  • メソッドの中。

Procオブジェクト

  • ブロックはオブジェクトじゃない
  • ブロックを保管して、あとで呼び出すためにはブロックがオブジェクトになる必要がある。そのために、Rubyの標準ライブラリProcがある
  • ブロックを評価するには、Proc#call()で呼び出す
  • ブロックを保管して、あとで呼び出す技術を遅延評価と呼ぶ
  • ブロックをProcに変換する方法は4つがある
    • Proc.new
    • lambda()
    • proc()
    • &修飾

&Checkでのブロック

&Checkにはいくつかのところでブロックを使用している。まず、モデルクラスでのscopeにlambdaを多く使っている。
あと、SurveyGroupモデルのvalidationに使っていて、そのコードは

1
2
3
4
5
6
with_options on: [:update] do |v|
  v.validate :validate_relation_participants
  v.validates :group_id, presence: true, unless: Proc.new { owner.is_a?(Conceptor) } # ここ
  v.validates :not_send_from, presence: true
  v.validates :not_send_to, presence: true
end

ここはProcを使わずowner.is_a?(Conceptor)だけ書くとエラーになるため、Procオブジェクトを使って値を渡している。
あとはVisualizeLogicServiceで使っている。

1
2
3
4
5
6
# 遷移パターンを表示する
def visualize
  trees = []
  @tree.root_node.print_tree(0, nil, lambda { |node, prefix| trees << "#{prefix} #{node.name}" }) # ここ
  trees
end

上記のコードの中のprint_treerubytree gemからrequireされて使っていて、深くは知らないけど、そのメソッドの引数を渡すためにlambdaを使っている。

ブロック①

スコープ

スコープとは

スコープは範囲、またはエリアなどで定義できる。つまり、プログラミングでのスコープはどこまで参照できるかを決める範囲を指してスコープと呼ぶ。

スコープの変更

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
v1 = 1

class MyClass
  v2 = 2
  local_variables # => [:v2]

  def my_method
    v3 = 3
    local_variables
  end

  local_variables # => [:v2]
end

obj = MyClass.new
obj.my_method # => [:v3]
obj.my_method # => [:v3]
local_variables # => [:v1, :obj]

このスコープの移動を追っていると、
まず、v1が定義されたトップレベルのスコープだ。次に、MyClassが定義されたスコープだ。
Rubyの「内部スコープ」から「外部スコープ」を参照する「入れ子構造」が存在しないため、スコープはきちんと区別されている。新しいスコープに入ると、束縛も新しい束縛と交換される。なので、my_methodが呼び出されるとさらに新しいスコープがオープンされて終わったらスコープは削除され、トップレベルのスコープに戻る。そこでまたmy_methodを呼び出すと新しいスコープをオープンして、新しいv3を定義する。(ここで定義されたv3は先のv3とは関係ない)

スコープゲート

プログラムがスコープを切り替えて、新しいスコープをオープンする場所は3つある。

  • クラス定義(class)
  • モジュール定義(module)
  • メソッド呼び出し(def)

スコープのフラット化

1
2
3
4
5
6
7
8
9
my_var = "Success"

class MyClass
  # my_varを表示したい。。

  def my_method
    # my_varを表示したい!
  end
end

このコードでクラスとメソッドでトップレベルのロカール変数のmy_varを使いたい場合、どうすればいいのか。今までの説明だったらスコープがそれぞれ違くて使えないけど。 さっき言った通り、スコープゲートは3つがある。
つまり、スコープゲート以外にはスコープ関係なくトップレベルのローカル変数を使えるということになるのだ。
classdefの代わりにClass.newdefine_methodを使えばクロージャでmy_varを取得できる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
my_var = "Success"

MyClass = Class.new do
  puts "#{my_var} はクラスの中で表示できる!"

  define_method :my_method do
    puts "#{my_var} はメドッドの中で表示できる!"
  end
end

MyClass.new.my_method

# =>
Success はクラスの中で表示できる!
Success はメドッドの中で表示できる!

こういうふうに書くと、無事出力される。

気になる点

そうしたらdefine_methodにブロックを渡したらどうなるだろう。

1
2
3
4
5
6
define_method :my_method do
  yield
end

my_method { puts "hello!" }
# => LocalJumpError: no block given (yield)

エラーになる。

1
2
3
4
5
6
7
def my_method
  yield
end

my_method { puts "hello!" }
# =>
hello!

defでメソッドを定義する時は問題なく動く。
define_methodでブロックが使える方法とは何があるのかな。

1
2
3
4
5
my_lambda = lambda{ puts "hello!" }

define_method :my_method, my_lambda
# =>
hello!
1
2
3
4
5
my_proc = proc{ puts "hello!" }

define_method :my_method, my_proc
# =>
hello!

lambdaprocを使うとdefine_methodにブロックを渡せる。

まとめ

  • どこまで参照できるかを決める範囲をスコープと呼ぶ
  • スコープはきちんと区別されている。新しいスコープに入ると、束縛も新しい束縛と交換される
  • スコープゲートは3つがある
    • クラス定義(class)
    • モジュール定義(module)
    • メソッド呼び出し(def)
  • classdefの代わりにClass.newdefine_methodを使えばスコープのフラット化ができる
  • define_methodにもブロックが渡せる