Skip to content

grow-rb/9_anonymous_class_and_reimplementing_rspec

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 

Repository files navigation

無名クラス

Rubyでクラスを定義するには2通りの方法があります。classキーワードとClass.newです。

class

classキーワードはクラス定義しつつそのクラスの中にコンテキストを移します。

このクラス定義の仕方は一般的ですが、一方でいくつかの制約があります。例えば、

  • 名前を動的に指定できない
  • classキーワードの外部にある変数にアクセスできない

これらの制約は通常あまり問題になりませんが、問題になるケースでは以下のClass.newを使います。

Class.new

Rubyの世界ではクラスはオブジェクトですので、newメソッドで作ることができます。この方法で作られたクラスは定数に代入されるまで無名なので「無名クラス」と言われることもあります。

無名クラスでは動的に名前付けができますし、名前をつける必要がないときは無名のままにできます。また、変数のスコープを作らないため、define_methodと組み合わせることで現在のスコープにある変数をクラス定義の内部で使うことができます。

例を見てみましょう。

lvar = "foo"
klass = Class.new do
  define_method :foo do
    lvar
  end
end

Object.const_set("Class_#{1+1}", klass)
puts Class_2.new.foo
# => foo

DSLの作り方をおさらい

DSLを構築する際に多く使われるのはinstance_evalです。また、アプリケーションクラスと呼ばれるDSL評価用のクラスを作って各DSLをそこへのポインタ的に使う手法もあります(Rakeなど)。しかし、これらの方法には制約があります。スコープが区切られないため、メソッドを定義すると定義がmainオブジェクトに漏れ出してしまうのです。

試しに、以下のコードをrake foo:barで実行してみましょう。

# 適当なディレクトリに配置
# Rakefile

namespace :foo do
  desc 'Sample task'
  task :bar do
    bar
  end

  def bar
    puts 'bar'
  end
end

bar # これはエラーになってほしい

するとbarが2回出力されます。これは一番下のbarがエラーになっていないことによるものです。これは一見直観に反しますが、namespaceメソッドがクラス定義をしているわけではない、ということを理解すると結局def barmainオブジェクトに対してのメソッド定義になっていることがわかるかと思います。

DSLから無名クラスを作る

では、RSpecのDSLではどうでしょうか。以下のコードをrspec foo_spec.rbなどで実行してみましょう。

# foo_spec.rb
require 'rspec'

RSpec.describe 'This is the description' do
  describe 'This is inner description' do
    def some_method
      puts self.class
    end
    it 'works' do
      some_method
    end
  end
end

some_method

今度はエラーになったかと思います。ここで、最下部のsome_method呼び出しを削除して再実行してみましょう。すると、RSpec::ExampleGroups::ThisIsTheDescription::ThisIsInnerDescriptionのような出力が得られます。これはputs self.classの結果ですので、def some_methodはこの(定義した覚えのない)クラスに対して定義されていることがわかります。

ここで使われているのが無名クラスです。無名クラスには名前を後から与えることができるので、上のようにdescribeメソッドに与えた文字列を元にクラス名を決定することもできるわけですね。

無名クラスのメソッド

Class.newで定義したクラスに対してメソッドを定義するにはClass.newにブロックを与え、その中で通常通りメソッドを定義します。

klass = Class.new do
  def foo
    puts 'foo'
  end
end

klass.new.foo # => foo

無名クラスの親クラス

Class.newに引数を与えるとそのクラスは親クラスになります。

class Parent
  def foo
    puts 'foo'
  end
end

Child = Class.new(Parent)
Child.new.foo # => foo

Releases

No releases published

Packages

No packages published

Languages