LoginSignup
1
2

【Ruby】クラス・モジュールについて理解を深める

Last updated at Posted at 2024-04-28

クラス

クラスの定義

class Foo
  # 初期化時に実行されるメソッド
  def initialize(x)
    # インスタンス変数に値を保持
    @x = x
  end

  # インスタンスメソッド
  def method1
    @x
  end
end

クラス名は定数であるため、大文字で始めないと構文エラーとなる。
┗ 定数の中にクラスを格納している。

クラス変数

class Foo
  def initialize
    @@i = 1
  end

  def return_i
    @@i
  end
end

class SubFoo < Foo
  def initialize
    @@i = 100
  end
end

foo = Foo.new
p foo.return_i #=> 1

sub_foo = SubFoo.new

p foo.return_i #=> 100
p sub_foo.return_i #=> 100

クラス定義内やクラスメソッド内で定義され、クラスおよびそのサブクラスのすべてのインスタンスで共有される。

クラス変数の変更は、すべてのインスタンスに影響を与えるため、意図しない変更を引き起こす可能性について注意する必要がある。

グローバル変数

$n = 1

class Foo
  $i = 2

  def method1; $n; end
  def method2; $i; end
end

foo = Foo.new

p foo.method1 #=> 1
p foo.method2 #=> 2

どこからでも参照することが可能。

グローバル変数の乱用は、コードの可読性を低下させ、バグの原因を引き起こす可能性がある。

必要な場合にのみ使用する。※出来る限り使用は避けるべき

インスタンス変数

@n = 1

class Foo
  # attr_accessor :n, :i
  @i = 2

  # def initialize
  #   @n = 1
  #   @i = 2
  # end

  def method1; @n; end
  def method2; @i; end
end

foo = Foo.new

# foo.n = 1
# foo.i = 2

p foo.method1 #=> nil
p foo.method2 #=> nil

クラスオブジェクトのインスタンスごとに独自の値を持つ変数。

初期化されていない場合は、nilを返す。

クラスの継承

class Foo
  def initialize(x)
    @x = x
  end

  def method1
    @x
  end
end

class SubFoo < Foo
  def initialize(x, y)
    @y = y

    # スーパークラスの初期化メソッドを呼び出す
    super x
  end

  def method2
    @x + @y
  end
end

# インスタンス化
sub_foo = SubFoo.new(3, 7)

p SubFoo.superclass #=> Foo
p sub_foo.class     #=> SubFoo
p sub_foo.method1   #=> 3
p sub_foo.method2   #=> 10
# p sub_foo.method3   #=> NoMethodError

class サブクラス < スーパークラス
で、指定したオブジェクトを継承したクラスオブジェクトを生成する。

Rubyにおいては、スーパークラスを暗黙的に呼び出すことはない。

クラスからオブジェクトを生成することをインスタンス化と呼ぶ。

継承したクラスのインスタンスは、スーパークラスのインスタンスメソッドを呼び出すことが出来る。

メモリ上では、以下に保持される。

  • インスタンスメソッドは、クラスオブジェクト
  • インスタンス変数は、インスタンス

クラスの継承チェーン

p Foo.ancestors    #=> [Foo, Object, Kernel, BasicObject]
p SubFoo.ancestors #=> [SubFoo, Foo, Object, Kernel, BasicObject]

上記の通り、継承はBasicObjectクラスから始まっている。

スーパークラスを省略してクラスを定義すると、自動的にObjectクラスを継承する。

Module#ancestorsについてはこちら

インスタンスメソッドの探索経路

呼び出し元のクラスに、メソッドが存在するかを判定
→次に、そのスーパクラス(継承元)にメソッドが存在するかの順で判定している

最後まで見つからなかった場合は、NoMethodErrorとなる

※例外のNoMethodErrorは、BasicObjectクラスの#method_missingが呼び出している。

method_missingをモンキーパッチしてみる
class Foo; end

class SubFoo < Foo
  def initialize(x, y)
    @x = x
    @y = y
  end

  def method_missing(name)
    # 独自の記述追加
    puts "******************** method_missing args name: #{name} ********************"

    # スーパークラスを呼び出して例外を発生させる
    super
  end
end

sub_foo = SubFoo.new(3, 7)

p sub_foo.method3 #=> ******************** method_missing args name: method3 ********************
                  #=> NoMethodError

インスタンスメソッドのネスト

class Foo
  def method1
    def nested_method1
      100
    end

    1
  end
end

# 定義されているメソッドを確認
p Foo.instance_methods(false) #=> [:method1] ⭐️この時点では、nested_method1は定義されていない扱い

foo = Foo.new
# p foo.nested_method1 #=> NoMethodError ⭐️まだ未定義なのでNoMethodErrorとなる
p foo.method1 #=> 1

p Foo.instance_methods(false) #=> [:method1, :nested_method1] ⭐️method1が実行されたことによって定義された
p foo.nested_method1 #=> 100

ネストされたメソッドは、外側のメソッドが呼び出されることで使うことが出来るようになる。

Module#instance_methodsはこちら

メソッドの可視性(アクセス制限 / カプセル化)

修飾子 説明
public 同じクラスの、どのインスタンスからでもアクセス可能。
protected 同じクラスと、そのサブクラスからのみアクセス可能。
private レシーバを付けた場合はNoMethodErrorとなる。また、レシーバを付けない場合は、同じクラスと(Rubyでは)そのサブクラスからアクセス可能。
class Foo
  def public_method
    puts 'Public'
  end

  def all_method
    obj = Foo.new

    obj.public_method
    obj.protected_method
    # obj.private_method # レシーバ付きの場合は、NoMethodErrorが発生
    private_method # レシーバを外せば、NoMethodErrorは発生しない
  end


  protected

  def protected_method
    puts 'Protected'
  end


  private

  def private_method
    puts 'Private'
  end
end

class SubFoo < Foo
  def access_protected_method
    protected_method
  end

  def access_private_method
    private_method
  end
end

sub_foo = SubFoo.new
sub_foo.access_protected_method #=> Protected
sub_foo.access_private_method #=> Private
# sub_foo.protected_method #=> NoMethodError
# sub_foo.private_method #=> NoMethodError

foo = Foo.new
foo.public_method #=> Public

foo.all_method #=> Public
               #=> Protected
               #=> Private

# インスタンスメソッドとして、直接呼び出すことは出来ない
# foo.protected_method #=> NoMethodError
# foo.private_method #=> NoMethodError

アクセス制限の変更

class Foo
  private

  def private_method1
    puts 'Private1'
  end

  def private_method2
    puts 'Private2'
  end
end

class SubFoo < Foo
  # privateから、publicに変更
  public :private_method1, :private_method2
end

sub_foo = SubFoo.new
sub_foo.private_method1 #=> Private1
sub_foo.private_method2 #=> Private2

public, protected, privateのいずれかを指定し、メソッド名を識別子 or シンボルで記載する。複数のメソッドのアクセス制限を変更する場合は、カンマで区切る。

alias式 と undef

class SubFoo < Foo
  def initialize(x, y)
    @y = y
    super x
  end

  def method2
    @x + @y
  end

  def method3; 1; end

  # 新しいメソッド名 古いメソッド名
  alias :sub_method_a :method2
  alias :sub_method_b :method3

  # method3の定義を取り消す
  undef :method3
end

sub_foo = SubFoo.new(3, 7)
p sub_foo.sub_method_a #=> 10
p sub_foo.sub_method_b #=> 1
# p sub_foo.method3 #=> NoMethodError

alias式で別名をつける/ undef式で定義を取り消す際のメソッド名は、
識別子 or シンボルで指定する。

aliasが定義されている場合に、undef式で片方の定義を取り消しても、もう片方が実行出来てしまう。

オープンクラス

class Foo
  def method1; 1; end
end

class Foo
  def method2; 2; end
end

p Foo.instance_methods(false) #=> [:method1, :method2]

同名のクラスを複数回定義しても、エラーにならない。

定義済みのクラスを再定義出来るクラスのことを、オープンクラスと呼ぶ。

特異クラス(Singleton Class)

  • 特異クラスとは、特定のインスタンスだけに適用される特別なクラス
    • シングルトンクラスとも呼ばれる
class Foo; end

foo = Foo.new

singleton_foo = class << foo
  self # 特異クラス自身を返す
end

p singleton_foo #=> #<Class:#<Foo:0x000000010af94d30>>
p singleton_foo.ancestors #=> [#<Class:#<Foo:0x000000010d094d00>>, Foo, Object, Kernel, BasicObject]
p singleton_foo.superclass #=> Foo

特異クラスは、class << 対象のオブジェクトで定義することが出来る。

Rubyインタプリタは、特異クラスを作成して継承チェーンに組み込む。
※インスタンスの生成元のクラスを、スーパークラスとしている。

  • Rubyインタプリタ
    • プログラムのソースコードを読み取り、実行可能な形式に変換して実行するプログラム
    • つまり、プログラムを実行するためのプログラムのこと

特異メソッド(Singleton Method)

  • オブジェクトの、特定のインスタンスに対して定義されたメソッド
  • 特定のインスタンスにのみ影響を与え、そのインスタンスのみで呼び出すことが出来る
  • 通常、特異メソッドは、あるインスタンスに対してのみ必要な振る舞いを追加するために使用される

特異メソッドの定義方法1

class Foo
  def initialize(num)
    @num = num
  end
end

foo1 = Foo.new(11)
foo3 = Foo.new(33)

# 定義方法①
def foo1.method1
  @num += 100
end

p foo1.method1 #=> 111
# p foo3.method1 #=> NoMethodError

特異メソッドの定義方法2

class Foo
  def initialize(num)
    @num = num
  end
end

foo2 = Foo.new(22)
foo3 = Foo.new(33)

# 定義方法②
class << foo2
  def method2
    @num += 200
  end
end

p foo2.method2 #=> 222
# p foo3.method2 #=> NoMethodError

特定のインスタンスにだけ、特別な性質を持たせたい場面で使用することが出来る。

クラスメソッド

  • クラス自体に関連付けられたメソッド
  • クラスオブジェクトをレシーバとする

クラスメソッドの定義方法1

class Foo
  def Foo.method1
    1
  end
end

p Foo.method1 #=> 1

クラスメソッドの定義方法2

class Foo
  def self.method2
    2
  end
end

p Foo.method2 #=> 2

クラスメソッドの定義方法3

class Foo
  class << self
    def method3
      3
    end
  end
end

p Foo.method3 #=> 3

モジュール

  • モジュールの目的
    • 名前空間の提供と、Mix-inにおける機能の定義

Mix-in:クラスに、モジュールの機能を混ぜ合わせること。

クラスは多重継承が出来ないが、Mix-inをすることで複数の機能を使うことが出来る。(Mix-inによって、多重継承と同等の役割を実現している)

モジュールからインスタンス化(new)することは出来ない。

モジュールは、継承することが出来ない。

モジュールは、クラスや他のモジュールに取り込むことが出来る。

クラス自体にモジュールのメソッドが混ざるのではなく、モジュールに対応した無名クラスがスーパークラスとの間の継承チェーンに挿入される

メソッドの探索経路(モジュールを組み込んだ場合)

共通コード
module Module1
  def method1; 1; end
  def method2; 2; end
  def method3; 3; end
  def method4; 4; end
end

module Module2
  def method1; 10; end
  def method2; 20; end
  def method4; 40; end
end

class Foo
  def method1; 100; end
  def method4; 400; end
  def method5; 500; end
end

Module#includeの場合

includeでMix-in
class SubFoo < Foo
  include Module1
  include Module2

  def method1; 1000; end
end

sub_foo = SubFoo.new

p sub_foo.method1 #=> 1000
p sub_foo.method2 #=> 20
p sub_foo.method3 #=> 3
p sub_foo.method4 #=> 40
p sub_foo.method5 #=> 500

includeの場合、呼び出し元のクラス(SubFoo)→Module2→Module1→呼び出し元のスーパークラス(Foo)の順にメソッド探索が行われる。

Module#prependの場合(Ruby2.0~)

prependでMix-in
class SubFoo < Foo
  prepend Module1
  prepend Module2

  def method1; 1000; end
end

sub_foo = SubFoo.new

p sub_foo.method1 #=> 10
p sub_foo.method2 #=> 20
p sub_foo.method3 #=> 3
p sub_foo.method4 #=> 40
p sub_foo.method5 #=> 500

prependの場合、呼び出し元のクラス(SubFoo)よりも先に、最後にprependされたモジュールから順(Module2→Module1→SubFoo→Foo)にメソッド探索が行われる。

継承チェーン(モジュールを取り込んだ場合)

include
p SubFoo.ancestors  #=> [SubFoo, Module2, Module1, Foo, Object, Kernel, BasicObject]
p SubFoo.superclass #=> Foo
prepend
p SubFoo.ancestors  #=> [Module2, Module1, SubFoo, Foo, Object, Kernel, BasicObject]
p SubFoo.superclass #=> Foo
  • 継承チェーンには、include/prependしたModule1Module2が含まれている
  • 但し、SubFooのスーパークラスはFooのまま

moduleをincludeすると、Rubyインタプリタは指定されたモジュールに対応する無名クラスを作成し、スーパークラスとの間に組み入れる。
対して、prependした場合は先頭に組み入れている。

Object#extend

  • 特異クラスへのMix-inで使用するメソッド

Module#include は、クラス(のインスタンス)に機能を追加しますが、extend は、ある特定のオブジェクトだけにモジュールの機能を追加したいときに使用します。

共通コード
module Module1
  def method1; @num += 700; end
end

class Foo
  def initialize(num)
    @num = num
  end
end

foo1 = Foo.new(77)
extendを使わずに特異クラスを定義した場合
class << foo1
  include Module1
end

p foo1.method1 #=> 777
extendを使用した場合
foo1.extend(Module1)

p foo1.method1 #=> 777

Module#refine(Ruby2.1~)

  • 限られた範囲のみ、Mix-inを有効化したい場合に使用

引数 klass で指定したクラスまたはモジュールだけに対して、ブロックで指定した機能を提供できるモジュールを定義します。定義した機能は Module#refine を使用せずに直接 klass に対して変更を行う場合と異なり、限られた範囲のみ有効にできます。そのため、既存の機能を局所的に修正したい場合などに用いる事ができます。

class Foo
  def method1; 1; end
  def method2; 2; end
end

module Module1
  # 変更を加えるクラスを宣言
  refine Foo do
    # 変更するメソッド及び処理を記述
    def method1; 10000; end
  end
end

foo = Foo.new

p foo.method1 #=> 1
p foo.method2 #=> 2

# usingを呼び出し、変更したメソッドの呼び出しを有効化する
using Module1

p foo.method1 #=> 10000
p foo.method2 #=> 2

usingが有効になる範囲は、クラス・モジュール定義の

  • 外でusingを呼び出した場合はファイルの末尾まで
  • 中でusingを呼び出した場合はそのクラス・モジュール定義の末尾まで

includeextend 両方使う場合

module Module1
  def method1; 1; end
end

class Foo
  include Module1
  extend Module1
end

foo = Foo.new
p foo.method1 #=> 1
p Foo.method1 #=> 1
  • include→モジュール内のメソッドが インスタンスメソッドとして動作する
  • extend→モジュール内のメソッドがクラスメソッドとして動作する

インスタンスメソッドとクラスメソッドの両方として利用可能になる。

ネストした定数の定義・参照

module Module1; end

# Module1の中に、Numberを宣言
Module1::Number = 100

class Foo
  include Module1
  # 警告が出るが、値を書き換えること自体は可能
  Module1::Number = 200

  def number
    Module1::Number
  end
end

# インスタンスを生成しなくても、値は上書きされる
p Module1::Number #=> 200

foo = Foo.new
p foo.number #=> 200

モジュールやクラスオブジェクトが格納された定数に対しては、::という演算子を使用することで、定数のネストを指定することが可能。

Rubyでは柔軟性を高めるため、定数に値を再代入することが可能となっている。
ただし、本来は再代入を避けるべきである。

名前空間

モジュールの中で、クラスを分けている例
module Food
  class Ramen
    def name; 'ラーメン'; end
  end

  class Cake
    def name; 'ケーキ'; end
  end
end

puts Food::Ramen.new.name #=> ラーメン
puts Food::Cake.new.name #=> ケーキ
同じクラス名だが、モジュールで名前の衝突を防いでいる例
module Guest
  class User
    def name; 'ゲストユーザ'; end
  end
end

module Admin
  class User
    def name; '管理者'; end
  end
end

puts Guest::User.new.name #=> ゲストユーザ
puts Admin::User.new.name #=> 管理者

同じ名前を持つクラスや定数が存在しても、名前の衝突を防ぐことが出来る。

関連するクラスや定数をまとめてグループ化し、構造を整理することが出来る。

適切な名前空間を使用することで、クラスや定数の名前に意味を与えることが出来る。

意味や目的毎にモジュールを分けてクラスを定義することが大切。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2