[プログラミング演習(Ruby) >  クラス,オブジェクト指向プログラミング]

クラス,オブジェクト指向プログラミング

Rubyのデータはすべてオブジェクトで,オブジェクトはデータとそのデータに対する処理機能としてのメソッドからなります. オブジェクトには種類があります. これまでにも「数値」,「文字列」,「配列」などのオブジェクトを扱ってきました. これらのオブジェクトにはそれぞれに固有のデータがあり,またメソッドがあります. Rubyでは最初から用意されている種類のオブジェクトを使えるだけでなく,プログラムの中で新しい種類のオブジェクトを作ることもできます.

さて,オブジェクトのもつデータとメソッドの仕様はどのように決まるのでしょうか. 新しい種類のオブジェクトはどのように作ることができるのでしょうか. Rubyでは,オブジェクトの仕様を「クラス」によって記述します. ある程度規模の大きなRubyプログラムは,クラスをベースにして記述していくのが一般的です. オブジェクトを(クラスによって)設計しながらプログラムを作っていくスタイルをオブジェクト指向プログラミングといいます.

ここでは,クラスとオブジェクト指向プログラミングについて,その基礎を簡単に説明します.

「クラス」あるいは「オブジェクト指向プログラミング」の全容を知りたい場合には,各種参考文献にあたって下さい.

もくじ

  1. 例題
  2. プログラム
  3. クラス
  4. オブジェクト指向プログラミング
  5. クラスの作成
  6. クラスの拡張
  7. self
  8. その他の話題

例題

n桁の秘密の数字列を予想して当てるプログラムを作成する. すべての桁が一致したときに正解として,予想回数を表示して終了する. そうでなければ,予想した数字列について「正解と一致した桁の個数」,「正解に含まれない数字である桁の個数」,「それ以外の桁の個数」を表示する.

プログラム

例題のプログラムを以下に示します. プログラムを分かりやすくするために, 次の二つのファイルに分割して作成してあります(分割しなければ実現できないわけではありません).

nsguess.rb処理本体
nsg.rbクラス定義

nsguess.rbが実際に処理を行う部分です. nsg.rbは,nsguess.rbの処理で利用する定義を行っています. nsguess.rbでnsg.rbをrequireにより取り込んでいます.

プログラムは次のように実施します(nsguess.rbとnsg.rbが同じディレクトリにあるものと仮定します). なお正解を当てられない場合でも[Ctrl]+[d]を入力すると正解を示して終了します.

$ ruby nsguess.rb 3
------------------------------------------------------------------------
3桁の秘密の数字列を当ててください
------------------------------------------------------------------------
(!) 予想した数字列に対して次の情報を表示します
  一致:  秘密の数字列と位置も含めて一致した数字の個数
  関連:  「一致」以外で秘密の数字列に含まれる数字の個数
  無関係: 秘密の数字列には含まれない数字の個数
------------------------------------------------------------------------
予想(#1)? 123
123 一致:0,関連:0,無関係:3
予想(#2)? 456
456 一致:0,関連:0,無関係:3
予想(#3)? 879
879 一致:1,関連:2,無関係:0
予想(#4)? 897
897 一致:0,関連:3,無関係:0
予想(#5)? 978
978 一致:0,関連:3,無関係:0
予想(#6)? 789
正解!!(予想回数:6)

$ ruby nsguess.rb 
------------------------------------------------------------------------
4桁の秘密の数字列を当ててください
------------------------------------------------------------------------
(!) 予想した数字列に対して次の情報を表示します
  一致:  秘密の数字列と位置も含めて一致した数字の個数
  関連:  「一致」以外で秘密の数字列に含まれる数字の個数
  無関係: 秘密の数字列には含まれない数字の個数
------------------------------------------------------------------------
予想(#1)? 1234
1234 一致:0,関連:1,無関係:3
予想(#2)? ギブアップ! # [Ctrl]+[d]を入力する
正解は8726でした

プログラム(処理本体)

処理本体の部分です. 先頭でnsg.rbをrequireにより取り込んでいます.

nsguess.rb
[行番号つきプログラムを別のウィンドウで開く] [行番号なしのプログラム]

プログラム(定義部)

nsguess.rbでの処理で利用するさまざまな定義を行っています.

nsg.rb
[行番号つきプログラムを別のウィンドウで開く] [行番号なしのプログラム]

クラス

Rubyのオブジェクトは全て何らかのクラス(class)に属します. 整数オブジェクトは整数クラス,文字列オブジェクトは文字列クラス, 配列オブジェクトは配列クラスに属するオブジェクトです. たとえば「1」と「2」はともに整数クラス(正確には固定長整数クラス)のオブジェクトです. 一方,"This is a string",[0,1,2]はそれぞれ文字列,配列という異なるクラスのオブジェクトです.

Rubyではオブジェクトの属するクラスをclassというメソッドで調べることができます.

  "This is a string".class   # ==> String
  [0,1,2].class              # ==> Array

この例の場合,"This is a string"というオブジェクトのクラスはString,[0,1,2]というオブジェクトのクラスはArrayということが分かったわけです.

クラスとはオブジェクトの種類を規定するものであるといえます. 同じクラスのオブジェクトは,同じ種類のデータと同じメソッドをもっています. 言い換えれば,クラスとは, そのクラスに属するオブジェクトがどのようなデータと機能を備えているか決める仕組み,オブジェクトの仕様書,設計書であるといえます.

一般にオブジェクト指向の世界では,クラスに属するオブジェクトのことを,そのクラスのインスタンス(instance)とよびます. クラスがオブジェクトの種類を規定していて,そのクラスに属している具体的なオブジェクトの実例をインスタンスとよぶわけです. 各インスタンス(オブジェクト)は,それぞれが別々の実体をもちます. インスタンスは,プログラム実行中にクラスの仕様にしたがって作りだされます. Rubyでクラスのインスタンスを作るには,一般的にはそのクラスのnewメソッドを利用します.

  a = Array.new(5)      # 長さ5の配列(Arrayクラスのインスタンス)を生成する
  s = String.new("abc") # "abc"という文字列(Stringクラスのインスタンス)を生成する

明示的にnewを使わなくてもインスタンスを生成できる場合もあります. これまでにも見てきた通り,数値,文字列,配列などは,データそのものを記述することで新しくインスタンスを生成できます.

  n = 8        # 整数のインスタンスを新たに生成する
  s = "string" # 文字列のインスタンスを新たに生成する
  a = [0,1,2]  # 配列のインスタンスを新たに生成する

なお,オブジェクトのもつデータのことを属性(attribute),内部状態などと呼びます. 属性値(属性の値)はオブジェクト(インスタンス)のインスタンス変数(instance variable)で管理されます(インスタンス変数の値が属性値というわけです).

クラスの階層構造

さきほども見たように,Rubyではclassメソッドで,オブジェクトのクラスを調べることができます.

  1.class               # ==> Fixnum
  10000000000000.class  # ==> Bignum

整数「1」と「10000000000000」のクラスが「Integer」ではなく「Fixnum」と「Bignum」になっています. これらはいずれも「Integer」クラスのサブクラス(subclass)です. 逆に「Integer」クラスは「Fixnum」と「Bignum」のスーパークラス(superclass)とよばれます. サブクラスはスーパークラスを細分類したクラスになります.

    Integer ─┬─ Fixnum
              │
              └─ Bignum

「1」と「10000000000000」は異なるクラスのインスタンスですが,いずれも「Integer」の一種です. is_a?メソッドを使えば,オブジェクトがあるクラスに属するインスタンスの一種とみなせるかどうかを判定することができます.

  1.is_a?(Integer)               # ==> true
  10000000000000.is_a?(Integer)  # ==> true
  1.0.is_a?(Integer)             # ==> false

数値には整数の他に浮動小数点数もあります. Rubyの数値関連のクラスは全体で次のような関係になっています.

    Numeric─┬─Integer ─┬─ Fixnum
             │            │
             │            └─ Bignum
             └─ Float

この例のように,クラスには階層関係があります. たとえば,「生物」が「動物」,「植物」などに大別され,さらに「動物」は「無脊椎動物」と「脊椎動物」に分けられるなどのように大きな分類がさらに細かく分類されるようなものです.

ところで「Integer」クラスが「Fixnum」と「Bignum」に分類されるのは, Ruby(コンピュータ上で)で小さな整数と大きな整数のデータ表現が異なるためです. 実際には,整数を使ったプログラムを作成する上でこれらを区別しなければならないことはまずありません.

Rubyに組み込まれているクラス

Rubyには数値(整数,浮動小数点数),文字列,配列の他にも,ファイル,手続き,スレッド,正規表現など多数のクラスが最初から組み込まれています. 組み込みクラスについては,Rubyのドキュメントを参照して下さい.

オブジェクト指向プログラミング

オブジェクト指向プログラミングとは,オブジェクトを設計して,オブジェクト間のデータのやりとりによってプログラムを構成する手法です. オブジェクト指向プログラミングの特徴とされることに,クラス,クラスの 継承,あるいはポリモルフィズム(polymorphism;多相性)といった概念があげられます.

「継承」とはベースとなるスーパークラスの仕様を受け継いでサブクラスを設計することです. 言語によっては複数のクラスを受け継ぐ「多重継承」という仕組みをもつ場合があります. Rubyでは「多重継承」は採用していません.その代わりにMix-inという仕組みを利用します.
「ポリモルフィズム」とは異なる種類のオブジェクトにおいても,それらが共通の機能をもつ場合に,常に同じ名前のメソッドを利用できるようにする仕組みです. これは(同じ目的の処理を行う)同じ名前のメソッドを複数のクラスで同時に定義できることを意味します. Rubyでは,たとえばどんなオブジェクトでもその文字列表現を与えるメソッドは共通して「to_s」という名前になっています. 一方,Cのようなポリモルフィズムをもたない言語では,同じ目的の処理を行う場合でも,処理するデータの種類によって別々の名前のメソッドを定義しなければなりません(Cの場合はメソッドではなく関数という). たとえば,to_sにあたるものは,int2str(整数),float2str(浮動小数点数)などのように定義するしかありません.

オブジェクト指向プログラミングのもっとも重要な特徴はカプセル化(encapsulation)です. カプセル化とは,プログラム内で論理的にデータを構造化した上でそれらを操作する機能(メソッド)と一体化してオブジェクトとしてまとめ,オブジェクトのデータをオブジェクトの外部から直接操作できないように閉じ込めることを意味します. オブジェクトのデータは決められたメソッドを通してのみ操作され,外部での処理で予期しない形で変更されてしまうことがなくなります. カプセル化とは,オブジェクトをブラックボックス化することだといえます.

カプセル化することで,オブジェクトを抽象的なプログラムの部品として使えるようになります. オブジェクトがメソッドを通してどのように振舞うかが分かっていれば,オブジェクトの実際のデータ構造がどうなっているか,その詳細な実装方法に左右されることなく,オブジェクトを利用することができます. カプセル化しておくことで,プログラムを作成する過程でオブジェクトのデータ構造を変更する必要があったとしても,メソッドの振舞いさえ変えなければ,オブジェクトの外部のプログラムは全く変更する必要がなくなります.

またこのときプログラムの変更部分がオブジェクトの定義(クラス)内部に限定されることも重要です. カプセル化されていないとすれば,プログラムの規模が大きくなるほど,データがプログラムのどこでいつ変更されるか把握することが困難になります. カプセル化することでプログラムを管理することが容易になります.

クラスの作成

クラスを作成するとは,新たなオブジェクトを設計することです. オブジェクトを設計するとは,つまり,どのような属性データをもち,それを操作するどのようなメソッドを持つかを定義することです.

クラスはゼロから設計することもできますし,既存のクラスをベースにして設計することもできます. 既存のクラスをベースとする場合には,新たに作成するクラスは,既存のクラスの属性データやメソッドを基本的に継承(受け継ぐ)することになります. この継承の元となるクラスがスーパークラス,そこから新たに作成したクラスがサブクラスとなります.

クラスの仕様はクラス定義式で記述します.

  class クラス名 # クラス名は定数として扱われる

    # このクラスの定義を記述する
  
  end

例題のプログラムでも次のようなクラスを定義しています.


011  # 「数字列予想ゲーム」のクラス
012  class NumberSequenceGuess
 :
067  end # class NumberSequenceGuess

initializeメソッド,インスタンス変数

クラスのnewメソッドを使ってインスタンスを新たに生成する際に必要となる処理はinitializeという特別なメソッドに定義します. newメソッドが呼び出されるとインスタンスが生成されます. またそのときnewに渡された引数がそのままinitializeメソッドに渡されて処理が行われます.

initializeメソッドで行う主な処理として,インスタンスの属性を扱うインスタンス変数を用意することが挙げられます. インスタンス変数の変数名には必ず「@」を先頭に付けます.


014    # 初期化処理
015    def initialize(n=4) # n:桁数(defaultは4)
016      @n = n                          # 桁数
017      @seq = generate_sequence(n) # 数字列の生成
018      @gmatch = GuessMatch.new(n)     # 予想チェック用データ
019    end

なお初期化処理が要らないのであれば,initializeメソッドを書く必要はありません.

インスタンスメソッド

オブジェクトの機能として利用するメソッドを実装するにはクラスの定義の中でメソッドを定義します. 特別な定義をしない限り,クラス定義の中で定義したメソッドはインスタンスのメソッドとなります. インスタンスメソッドでは,引数,ローカル変数の他に,インスタンス変数(@から始まる名前をもつ),クラス定数,クラス変数(@@から始まる名前をもつ)などを参照することができます.

たとえば,例題のプログラムでは,次のような定義をしています.

011  # 「数字列予想ゲーム」のクラス
012  class NumberSequenceGuess
  : 
021    # 予想チェックの結果を得る
022    def info(guess)
023      exact,rel = matched_info()
024      [exact,rel,@n-exact-rel]
025    end
  : 

これにより,NumberSequenceGuessクラスのインスタンスにinfoというメソッドが定義されることになります. nsguess.rbでこれを利用しています.

 : 
015  require 'nsg' # nsg.rbの内容を読み込む
 : 
054  secret = NumberSequenceGuess.new(n) # 「数字列予想ゲーム」インスタンス
 : 
090      exact,rel,other = secret.info(guess)
 :

プライベートメソッド

インスタンスメソッドを定義するときにその下請け作業をするようなメソッドを使う場合があります. もちろん,そのような下請けメソッドもクラス内で定義すれば,必要な機能を実現することができます.

ただ,そのような下請けメソッドは単独でオブジェクトの機能として使うものではないでしょうから,オブジェクトのメソッドとして使えるようになっている必要はありません. むしろそのような内部の処理が不必要に表にでてしまうことは好ましくないといえます. オブジェクトのメソッドを設計する際には,オブジェクトの機能として使うメソッドと内部で下請けを行うメソッドは区別して,実際にオブジェクトの機能となるメソッドのみがインスタンスのメソッドとして使えるようしておくべきです.

クラス定義の中で「private」と書くと,その後に定義するメソッドは,すべてプライベートメソッド(private method)になります. プライベートメソッドが利用できるのはそのクラスの内部のみになり,外部には非公開となります. プライベートメソッドはオブジェクトのメソッドとしては使えません. 一方,プライベートでないメソッドをパブリックメソッド(public method)といいます.


011  # 「数字列予想ゲーム」のクラス
012  class NumberSequenceGuess
  : 
052    private ## 以下はプライベートメソッド
053    
054    # 長さnの数字列(0-9)をランダムに生成する
055    # すべて異なる数字とする
056    def generate_sequence(n)
057      return nil if n > 10
058      pool = Array.new(10) { |i| i } # [0,1,...,9]
059      n2 = n**2
060      n2.times do |i|
061        j = i % n
062        k = (j+1+rand(9)) % 10 
063        pool[j],pool[k] = pool[k],pool[j]
064      end
065      pool[0,n]
066    end
067  end # class NumberSequenceGuess

プライベートメソッドをオブジェクトのメソッドとして使おうとするとエラーになります.


  require 'nsg'    

  s = NumberSequenceGuess.new(4)
  s.generate_sequence(2,4) # ==> private method `generate_sequence' called for ... (NoMethodError)

クラス定数

クラス定義の中で定数を定義することができます. その定数はクラス内ではそのまま利用できます. またクラス定義の外部でも「クラス名::定数名」とすることで,その定数を参照することができます.


  class Foo
    CONST_FOO  = "Foo!"
    CONST_FOO2 = 0
  end

  Foo::CONST_FOO  # ==> "Foo!"
  Foo::CONST_FOO2 # ==> 0

クラスメソッド

クラスにはインスタンスに関連づけられたメソッド以外にも,クラスに関連した処理を行うクラスメソッドを定義できます. たとえば,Fileクラスにはファイルの状態を調べるクラスメソッドが多数用意されています. ここで例を示します.


  File.exist?(file)    # fileは存在するか?
  File.writable?(file) # fileに書き込みが許可されているか?

クラスメソッドは,次のように定義されます.


  class クラス名
    def クラス名.メソッド名(引数1,引数2,...)  

    end
  end

なお後で説明するselfを使うと次のように定義できます. こちらの方がスマートです.


  class クラス名
    def self.メソッド名(引数1,引数2,...)  

    end
  end

なおnewメソッドもクラスメソッドです. newメソッドはクラスに自動的に定義されます.

クラスのネスト定義

クラス定義の中でさらに別のクラスを入れ子にして定義できます. このようなクラスは,継承によって作成するサブクラスとは異なるものです. 入れ子で定義された内部のクラスには,その外側のクラスのメソッド,変数,定数などは受け継がれません.

このような仕組みはクラス同士の関係を構造化するのに使うことができます. 内部のクラスは「外側のクラス::内側のクラス」という形式の名前をもつことになります.


  class Foo
    CONST_FOO = "Foo!"

    def foo_bar()
      Bar::CONST_BAR # 内部クラスBarの定数を参照する
    end      

    # クラスFoo::Barの定義
    class Bar 
      CONST_BAR = "Bar!"

      def bar_foo()
        Foo::CONST_FOO
      end
    end

  end

  Foo::CONST_FOO      # ==> "Foo!"
  Foo::Bar::CONST_BAR # ==> "Bar!"
  f = Foo.new
  b = Foo::Bar.new
  f.foo_bar           # ==> "Bar!"
  b.bar_foo           # ==> "Foo!"

  Foo::Bar::CONST_FOO # ==> NameError
  b.foo_bar           # ==> NoMethodError

例題でもNumberSequenceGuessにGuessMatchという内部クラスを定義しています.


069  # NumberSequenceGuessの内部クラスとして
070  # 「予想チェック」のためのクラスGuessMatchを定義する
071  # 正解と予想を比較して桁と数字が一致した数(exact_match),
072  # それらを除いて,数字のみが一致した数(other_match)を調べる
073  class NumberSequenceGuess
074    class GuessMatch
:
144    end # class GuessMatch
145  end # class NumberSequenceGuess

クラスの拡張

既存のクラスに手を加えて,新しいメソッドを追加してクラスを拡張することもできます. クラスの拡張は継承により新しいサブクラスを作成するのとは異なります.

既存のクラスに新しいメソッドを追加するには,新しいクラスを定義する場合と 全く同様に「class...end」内でメソッドを定義します. 指定するクラス名がこれまでにないものであれば新しいクラスを作ることになり,既存のクラスであれば,そのクラスへの定義の追加となるわけです.



  class クラス名

    # 新しいメソッドの定義を記述する

  end


なお,既存のメソッドと同じ名前を使ってメソッドの定義を行うとそのメソッドの定義を書き換えることになります.

例題でもStringクラスにメソッドto_nseqを追加しています. またNumberSequenceGuessクラスの定義を二つに分けて,GuessMatchという内部クラスを定義する部分を独立させています. これも形式的には一度NumberSequenceGuessクラス定義を終わって,その後GuessMatchの定義を追加していることになります. これらの定義は一つにまとめても問題ないのですが,読みやすさを考慮して二つに分けています.


002  # 文字列クラスの拡張
003  class String
004    # 数字列を「数字の配列」に変換するメソッド
005    # (例) "1234" --> [1,2,3,4]
006    def to_nseq 
007      self.scan(/\d/).collect { |s| s.to_i }
008    end
009  end
010  
011  # 「数字列予想ゲーム」のクラス
012  class NumberSequenceGuess
  : 
067  end # class NumberSequenceGuess
  : 
073  class NumberSequenceGuess
074    class GuessMatch
  :
144    end # class GuessMatch
145  end # class NumberSequenceGuess

self

例題のプログラムで文字列クラスにto_nseqメソッドを追加定義しました. そこにselfという変数が現れています. selfはどこにも定義が見当たりませんが,これは何でしょうか. 「self」(自身)という名前から推測される通り,これは(定義しているインスタンスメソッドの対象である)インスタンス自身を示します. つまりselfとはこのto_nseqを起動したインスタンスそのものです. selfのメソッドを起動することは自分自身のメソッドを起動することです.


002  # 文字列クラスの拡張
003  class String
004    # 数字列を「数字の配列」に変換するメソッド
005    # (例) "1234" --> [1,2,3,4]
006    def to_nseq 
007      self.scan(/\d/).collect { |s| s.to_i }
008    end
009  end

例題のプログラムの他のインスタンスメソッドの定義でも(同じクラスの)別のメソッドを使っていますが,selfのようにレシーバを明示しないで関数的メソッドとして呼び出しています.


021    # 予想チェックの結果を得る
022    def info(guess)
023      exact,rel = matched_info() # selfのmatched_infoメソッドを起動
024      [exact,rel,@n-exact-rel]
025    end
:
033    # 予想のチェック結果を返す
034    def matched_info
035      @gmatch.retrieve
036    end

このようにインスタンスメソッドから同じインスタンスのメソッドを呼び出すとき,selfは省略可能です. 上のStringクラスの例の場合も,実はselfは省略可能です. ただし次のようにselfが必要となる場合もあります.


  class String
    def same?(other)
      self == other
    end
  end

  "abc".same?("abc") # ==> true
  "abc".same?("ABC") # ==> false

なおクラスの定義(class ... end)の直下のレベルではselfはそのクラスです(Rubyのクラスはオブジェクトで,クラスもselfで参照できます).

その他の話題

ここでは,例題のプログラムで使っていて今回のトピックには直接関係のない文法事項を紹介します.

alias

同じメソッドをいろいろな名前で使えると便利です. Rubyでは,aliasによりメソッドに別名をつけることができます.


048    # メソッドの別名定義
049    alias equal? match?   # equal?をmatch?の別名とする
050    alias correct? match? # correct?をmatch?の別名とする

別名をつけたメソッドは,別名でも元の名前でも呼び出すことができます.

[プログラミング演習(Ruby) >  クラス,オブジェクト指向プログラミング]