4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Railsチュートリアル 第4章 - Rails風味のRubyを学ぶ…クラス

Last updated at Posted at 2019-09-17

「実際にオブジェクトをいくつか定義してみる」というパートまで至りました。

コンストラクタ

リテラルコンストラクタ

Rubyでは、文字列(String)・配列(Array)・ハッシュ(Hash)・範囲(Range)などのクラスに対し、当該オブジェクトのインスタンスを暗黙的に作成する記法が存在します。Railsチュートリアルでは、こうした記法を「リテラルコンストラクタ」と呼んでいます。以下はリテラルコンストラクタの例です。

リテラルコンストラクタの例
# 文字列のリテラルコンストラクタ
>> s = "foobar"
=> "foobar"
>> s.class
=> String

# 配列のリテラルコンストラクタ
>> ar = ['foo', 'bar']
=> ["foo", "bar"]
>> ar.class
=> Array

%記法も、Railsチュートリアルでいうところのリテラルコンストラクタの一種です。

%記法の例
# %qによる文字列のリテラルコンストラクタ
>> s = %q[foo bar'baz']
=> "foo bar'baz'"
>> s.class
=> String

# %wによる配列のリテラルコンストラクタ
>> ar = %w[foo bar baz]
=> ["foo", "bar", "baz"]
>> ar.class
=> Array

名前付きコンストラクタ

暗黙のリテラルコンストラクタが存在する一方、明示的に同等の名前付きコンストラクタを呼び出して使うことも可能です。名前付きコンストラクタは、クラス名に対してnewメソッド1を呼び出すことにより実行されます。

名前付きコンストラクタの例
# 文字列の名前付きコンストラクタ
>> s = String.new("foobar")
=> "foobar"
>> s.class
=> String

# 配列の名前付きコンストラクタ
>> ar = Array.new(['foo', 'bar'])
=> ["foo", "bar"]
>> ar.class
=> Array

ハッシュの名前付きコンストラクタ

ハッシュの名前付きコンストラクタHash.newの引数は、ハッシュの内容そのものではなく、ハッシュのデフォルト値(キーが存在しない場合の値)を与えるために用いられます。Hash.newの引数がない場合、ハッシュのデフォルト値はnilとなります。

ハッシュの名前付きコンストラクタ
>> h = Hash.new()
=> {}
>> h[:foo]  # 存在しないキー(:foo)の値を取り出してみる
=> nil
>> h
=> {}  # 存在しないキー(:foo)の値を取り出しても、ハッシュの中身に変化はない

>> h = Hash.new(0)  # 存在しないキーのデフォルト値を0としてハッシュを作成する
=> {}
>> h[:foo]  # 存在しないキー(:foo)の値を取り出してみる
=> 0
>> h
=> {}  # 存在しないキー(:foo)の値を取り出しても、ハッシュの中身に変化はない

ハッシュの名前付きコンストラクタにブロックを与えることにより、「存在しないキーの値を取り出すと、指定したキーに対する値を自動でセットする」という動作を定義することも可能です。

「存在しないキーの値を取り出すと、指定したキーに対する値を自動でセットする」ハッシュの例
>> h = Hash.new(){|h,k| h[k] = 0}
=> {}
>> h[:foo]  # 存在しないキー(:foo)の値を取り出してみる
=> 0
>> h
=> {:foo=>0}  # :fooというキーに対し、0という値がセットされている

演習 - コンストラクタ

1. 1から10の範囲オブジェクトを生成するリテラルコンストラクタは何でしたか? (復習です)

>> 1..10
=> 1..10

2. 今度はRangeクラスとnewメソッドを使って、1から10の範囲オブジェクトを作ってみてください

>> r = Range.new(1,10)
=> 1..10
>> r.class
=> Range

3. 比較演算子==を使って、上記2つの課題で作ったそれぞれのオブジェクトが同じであることを確認してみてください。


>> rr = 1..10
=> 1..10
>> rr.class
=> Range
>> r == rr
=> true

クラスメソッドとインスタンスメソッド

メソッドがクラス自身に対して呼び出されるとき、当該メソッドをクラスメソッド2といいます。newメソッドはクラスメソッドの例です。

クラスのnewメソッドを呼び出した場合、その戻り値は、当該クラスの個別オブジェクトとなります。クラスに属する個別オブジェクトのことをインスタンスと呼びます。

lengthequal?といった、インスタンスに対して呼び出すメソッドはインスタンスメソッドと呼ばれます。

クラス継承

Class#superclassメソッドにより、オブジェクトのクラス階層を調べていくことができます。

>> s = String.new("foobar")
=> "foobar"
>> s.class.superclass
=> Object
>> s.class.superclass.superclass
=> BasicObject
>> s.class.superclass.superclass.superclass
=> nil

String.png

Rubyにおける全てのクラスは、BasicObjectクラスを継承しています。また、ほとんどのクラスは、BasicObjectを基底クラスとするObjectクラスを継承しています。

とにかくクラスを作ってみる

ある単語を前からと後ろからのどちらから読んでも同じ (つまり回文になっている) ならばtrueを返すpalindrome?メソッドを含む、Wordというクラスを作ってみます。

とにかくWordクラスを作ってみる
>> class Word
>>   def palindrome?(string)
>>     string == string.reverse
>>   end
>> end
=> :palindrome?

このクラスは、以下のように使うことができます。

>> w = Word.new  # Wordオブジェクトを作成する
=> #<Word:0x000055e4debd1320>
>> w.palindrome?("foobar")
=> false
>> w.palindrome?("level")
=> true

>> w.class.superclass    
=> Object

WordクラスがObjectクラスを直接継承していることは注目に値します。Rubyのクラス定義においては、基底クラスを明示しなければ、暗黙的にObjectクラスを継承します。

既存クラスから継承したクラスを作る

…ちょっと待ってください。文字列を引数に取るメソッドを作るためだけに、Objectクラスを直接継承する新しいクラスを定義するのもおかしな話ですよね。単語は文字列なのだから、WordクラスはStringクラスを継承して作られるべきではないでしょうか…

というわけで、今度はStringクラスを継承したWordクラスを定義します。

Stringクラスを継承したWordクラスを作る
>> class Word < String
>>   def palindrome?  
>>     self == self.reverse
>>   end
>> end
=> :palindrome?

このクラスは、以下のように使うことができます。

>> s = Word.new("level")  # 新しいWordを作成し、"level"で初期化する
=> "level"
>> s.palindrome?  # Wordが回文かどうか調べる
=> true
>> s.length  # WordはStringで使える全てのクラスを継承している
=> 5

Wordクラスの継承階層を調べてみましょう。

>> s.class.superclass
=> String
>> s.class.superclass.superclass
=> Object

UMLのクラス図は以下のようになります。

Word.png

演習 - クラス継承

1.1. Rangeクラスの継承階層を調べてみてください。

Rangeクラスの継承階層
>> Range.superclass
=> Object
>> Range.superclass.superclass
=> BasicObject
>> Range.superclass.superclass.superclass
=> nil

Range.png

1.2. 同様にして、HashSymbolクラスの継承階層も調べてみてください。

Hashクラスの継承階層
>> Hash.superclass
=> Object
>> Hash.superclass.superclass
=> BasicObject
>> Hash.superclass.superclass.superclass
=> nil

Hash.png

Symbolクラスの継承階層
>> Symbol.superclass
=> Object
>> Symbol.superclass.superclass
=> BasicObject
>> Symbol.superclass.superclass.superclass
=> nil

Symbol.png

余談 - Classクラスの継承階層

Classクラスの継承階層
>> Class.superclass
=> Module
>> Class.superclass.superclass
=> Object
>> Class.superclass.superclass.superclass
=> BasicObject
>> Class.superclass.superclass.superclass.superclass
=> nil

Class.png

RangeHashSymbolClassの継承階層をひとまとめにすると、以下のようになります。

Class.png

2. リスト 4.15にあるself.reverseselfを省略し、reverseと書いてもうまく動くことを確認してみてください。

>> class Word < String
>>   def palindrome?
>>     self == reverse  # self.reverseのself.を省略
>>   end
>> end
=> :palindrome?

>> s = Word.new("level")
=> "level"
>> s.palindrome?
=> true
>> s.length
=> 5

確かにうまく動いています。

組み込みクラスの変更

Rubyでは、組み込みクラスそのものを拡張したり、組み込みクラスのメソッドの動作を変更することも可能です。

Stringクラスにpalindrome?メソッドを追加する例
>> class String
>>   def palindrome?
>>     self == self.reverse
>>   end
>> end
=> :palindrome?

>> "deified".palindrome?
=> true

blank?メソッド

Rails3によって追加されるメソッドの一つです。

blank?メソッドの動作例
>> "".blank?
=> true
>> "    ".empty?
=> false
>> "    ".blank?
=> true
>> nil.blank?
=> true
  • blank?メソッドのソースコード
    • 最も基底では、Objectクラスに定義されている
    • blank?の否定を返すpresent?というメソッドも定義されている
    • NilClass#blank?FalseClass#blank?は常にtrueを返す
    • TrueClass#blank?は常にfalseを返す
    • StringArrayHashNumericTimeに対するblank?の定義は上書きされている

Railsによる、配列に対する追加メソッド

実は、配列に対するsecondthirdfourthfifthforty_twoというメソッドも、Rails3によって組み込みクラスArrayに追加されたメソッドです。

second,third,forty_twoの用例
>> [*('a'..'z')].second
=> "b"

>> [*('a'..'z')].third 
=> "c"

>> [*(1..42)].forty_two
=> 42

演習 - 組み込みクラスの変更

1.1. palindrome?メソッドを使って、“racecar”が回文であり、“onomatopoeia”が回文でないことを確認してみてください。

>> "racecar".palindrome?
=> true
>> "onomatopoeia".palindrome?
=> false

1.2. 南インドの言葉「Malayalam」は回文でしょうか?

>> "Malayalam".palindrome?
=> false
>> "Malayalam".downcase.palindrome?
=> true

大文字小文字を区別しないようにすれば、回文になるようです。

2. リスト 4.16を参考に、Stringクラスにshuffleメソッドを追加してみてください。

Railsチュートリアルより、リスト4.16(「?」を適切なメソッドに置き換える)

class String
def shuffle
self.?('').?.?
end
end
"foobar".shuffle
=> "borafo"

>> class String
>>   def shuffle
>>     self.split('').shuffle.join
>>   end
>> end
=> :shuffle

>> "foobar".shuffle
=> "obfora"  # 結果は実行するごとに変わります

処理順序としては、以下の通りでしたね。

  1. split('')メソッドにより、文字列を1文字ずつの配列に変換する
  2. shuffleメソッドにより、元の配列と同じ内容を持ち、要素の順番だけが異なる配列を生成する
  3. joinメソッドにより、配列を一つの文字列に変換する

3. リスト 4.16のコードにおいて、self.を削除してもうまく動くことを確認してください。

これまでのshuffleメソッドの定義を消去するため、一旦Rails Consoleから抜けた上で、再びRails Consoleを起動します。そのうえで、以下のコードを入力していきます。

>> class String
>>   def shuffle
>>     split('').shuffle.join  # self.を削除
>>   end
>> end
=> :shuffle

>> "foobar".shuffle
=> "oaobrf"  # 結果は実行するごとに変わります

確かにうまく動きますね。

Railsのコントローラクラス

Railsのコントローラクラスの継承関係
>> controller = StaticPagesController.new
=> #<StaticPagesController:0x000055de496db6e0 @_action_has_layout=true, @_routes=nil, @_request=nil, @_response=nil>

>> controller.class
=> StaticPagesController
>> controller.class.superclass
=> ApplicationController
>> controller.class.superclass.superclass
=> ActionController::Base
>> controller.class.superclass.superclass.superclass
=> ActionController::Metal
>> controller.class.superclass.superclass.superclass.superclass
=> AbstractController::Base
>> controller.class.superclass.superclass.superclass.superclass.superclass
=> Object
>> controller.class.superclass.superclass.superclass.superclass.superclass.superclass
=> BasicObject
>> controller.class.superclass.superclass.superclass.superclass.superclass.superclass.superclass
=> nil

継承関係を表すクラス図は以下のようになります。

StaticPagesController.png

演習 - Railsのコントローラクラス

1. 第2章で作ったToyアプリケーションのディレクトリでRailsコンソールを開き、User.newと実行することでuserオブジェクトが生成できることを確認してみましょう。

>> u = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>

2. 生成したuserオブジェクトのクラスの継承階層を調べてみてください。

>> u.class 
=> User(id: integer, name: string, email: string, created_at: datetime, updated_at: datetime)
>> u.class.superclass
=> ApplicationRecord(abstract)
>> u.class.superclass.superclass
=> ActiveRecord::Base
>> u.class.superclass.superclass.superclass
=> Object
>> u.class.superclass.superclass.superclass.superclass
=> BasicObject
>> u.class.superclass.superclass.superclass.superclass.superclass
=> nil

User.png

ユーザークラス

ここで、Railsコンソールから抜け、sample_appの開発環境に戻ります。その上で、以下のUserクラスを、/example_user.rbに作成します。

/example_user.rb
class User
  attr_accessor :name, :email

  def initialize(attributes = {})
    @name  = attributes[:name]
    @email = attributes[:email]
  end

  def formatted_email
    "#{@name} <#{@email}>"
  end
end

「当該Userクラスでは、DBを扱わない」というのが一つのポイントです。DBを扱うとなると、「DBのマイグレーション処理」等、Rails特有の処理が多数発生し、クラスやメソッドの振る舞いも純粋なRubyからかけ離れていきます。今回は、DBを扱わないので、クラスやメソッドの振る舞いは純粋なRubyに近いものになります。

attr_accesorメソッド

attr_accessor :name, :email

attr_accessorというのは、インスタンス変数に対する読み取り・書き込み両方を行うためのメソッドを用意するメソッドです。このコードでは、インスタンス変数@name@emailに対する読み取り・書き込み4を行うためのメソッドが用意されるということです。VBAのProperty GetProperty Setをセットにしたもの」に近いものでしょうか。

以下のコードは、先ほど記述したattr_accesorと同じメソッドを定義するコードです。

def name=(str)
  @name = str
end

def name
  @name
end

def email=(str)
  @email = str
end

def email
  @email
end

attr_accessorについては、以下のような説明も参考になるかと思われます。

initializeメソッド

def initialize(attributes = {})
  @name  = attributes[:name]
  @email = attributes[:email]
end

initializeというのは、Rubyの特殊なクラスメソッドの一つです。クラスメソッドをnewしたとき(今回だとUser.newしたときですね)に自動で呼び出され、メソッド内の記述に基づいてインスタンス変数を初期化します。Java等でいうところのコンストラクタですね。

今回のinitializeメソッドは、デフォルトの引数として空のハッシュを一つ取ります。当該ハッシュは、initializeメソッド内部ではattributesという名前で使われます。Rubyでは、存在しないキーに対するハッシュの値はnilであるため、attributes[:name]attributes[:email]はいずれもnilとなり、最終的には@name=nil, @email=nilとなります。

formatted_emailメソッド

def formatted_email
  "#{@name} <#{@email}>"
end

formatted_emailメソッドは、文字列の式展開を利用し、@name@emailに割り当てられた値をユーザのメールアドレスとして構成するメソッドです。@name@emailはいずれもUserクラスのインスタンス変数であり、formatted_emailメソッドでもそのまま使うことができます。

自作したUserクラスを使ってみる

example_user.rbを作成したRails環境のルートディレクトリでRailsコンソールを実行します。以降はRailsコンソールでの入力と、その応答です。

自作Userクラスを使ってみる
>> require './example_user'  # example_userのコードを読み込む
=> true

>> example = User.new
=> #<User:0x000055de496ee8d0 @name=nil, @email=nil>
>> example.name  # :attributes[:name]は存在しないのでnil
=> nil
>> example.name = "Example User"  # 名前を代入する
=> "Example User"
>> example.email = "user@example.com"  # メールアドレスを代入する
=> "user@example.com"
>> example.formatted_email
=> "Example User <user@example.com>"
  • ./example_userは、カレントディレクトリのexample_userファイルを指す
    • .は、Unixのカレントディレクトリを指す
  • newメソッドで空のexample_userを作成する
    • この時点では@name@emailともnil
  • 対応する属性にそれぞれ手動で値を代入している
    • attr_accessorを定義しているので、このような形で代入できる

以上が動作のポイントでしょうか。

マスアサインメント

マスアサインメントの例
>> user = User.new(name: "Michael Hartl", email: "mhartl@example.com")
=> #<User:0x000055de49681780 @name="Michael Hartl", @email="mhartl@example.com">
>> user.formatted_email
=> "Michael Hartl <mhartl@example.com>"

Userクラスに属するuserオブジェクトが、生成時点で@name="Michael Hartl", @email="mhartl@example.com"となっていますね。

例えば今回の自作Userクラスの場合、newメソッドの引数にハッシュを与えることにより、インスタンス変数が定義済みのインスタンスを生成することができます。こうした技法を「マスアサインメント」といいます。Railsアプリケーションでも広く使われる技法だそうです。

この章の学習内容との関係で言えば、「(メソッドの最後の引数なので)ハッシュの波括弧が省略されている」というのは一つのポイントですね。

なお、マスアサインメントに関係して、以下のような概念が後々登場することになりますが、それはまた別の話です。

  • マスアサインメント脆弱性
  • マスアサインメント脆弱性への対策であるStrong Parameters

演習 - ユーザークラス

1.1. Userクラスで定義されているname属性を修正して、first_name属性とlast_name属性に分割してみましょう。

User#attr_accessorを書き換えていきます。

User#attr_accessorの実装変更
attr_accessor :first_name, :last_name, :email

この部分を実装すると、User.newの挙動が以下のようになります。

>> example = User.new            
=> #<User:0x000055de48a8d7a8 @first_name=nil, @last_name=nil, @email=nil>

1.2. また、それらの属性を使って "Michael Hartl" といった文字列を返すfull_nameメソッドを定義してみてください。

1.1.に引き続き、User#full_nameメソッドを追加します。

User#full_nameの実装
def full_name
  "#{@first_name} #{@last_name}"
end

この部分まで実装したときの、User#full_nameの挙動は以下の通りになります。

>> example.first_name = "Michael"
=> "Michael"
>> example.last_name = "Hartl"
=> "Hartl"
>> example.full_name
=> "Michael Hartl"

最初、{@last_name}の前の#を入れ忘れてしまい、"Michael {@last_name}"のような戻り値が出力されて「???」となりました。こういうイージーミスはやってしまいがちですよね。

1.3. 最後に、formatted_emailメソッドのnameの部分を、full_nameに置き換えてみましょう (元々の結果と同じになっていれば成功です)

1.2.に引き続き、User#formatted_emailメソッドを書き換えていきます。

User#formatted_emailの実装変更
def formatted_email
  "#{full_name} <#{@email}>"
end

この部分まで実装したときの、User#formatted_emailの挙動は以下の通りになります。

>> example.first_name = "Michael"
=> "Michael"
>> example.last_name = "Hartl"
=> "Hartl"
>> example.email = "user@example.com"  
=> "user@example.com"
>> example.formatted_email
=> "Michael Hartl <user@example.com>"

2. "Hartl, Michael" といったフォーマット (苗字と名前がカンマ+半角スペースで区切られている文字列) で返すalphabetical_nameメソッドを定義してみましょう。

1.3.に引き続き、User#alphabetical_nameメソッドを追加します。

User#alphabetical_nameの実装
def alphabetical_name
  "#{@last_name}, #{@first_name}"
end

User#alphabetical_nameの挙動は以下の通りになります。

>> require './example_user'
=> true
>> example = User.new 
=> #<User:0x000055de49caed60 @first_name=nil, @last_name=nil, @email=nil>
>> example.first_name = "Michael"
=> "Michael"
>> example.last_name = "Hartl"
=> "Hartl"
>> example.alphabetical_name
=> "Hartl, Michael"

期待通りの挙動になりましたね。

3. full_name.splitalphabetical_name.split(’, ’).reverseの結果を比較し、同じ結果になるかどうか確認してみましょう。

2.まで完了した後に、Rails Consoleで以下のコードを入力していきます。

>> example.full_name.split == example.alphabetical_name.split(', ').reverse
=> true
>> example.full_name.split
=> ["Michael", "Hartl"]
>> example.alphabetical_name.split(', ').reverse
=> ["Michael", "Hartl"]

同じ結果になっているようです。

  1. 実際には、Class#newメソッド(自身もメソッドである)は、内部でクラスのインスタンスを生成するClass#allocateメソッドと、インスタンスを初期化するObject#initializeメソッドを実行しています。このあたりの話は、Rubyの言語仕様についての突っ込んだ話になり、Railsチュートリアルの範疇を超えます。

  2. より厳密に言えば、Rubyにおいて「クラスメソッド」と呼ばれるものは、Classというクラスのインスタンスメソッドです。ただ、このあたりの話は、Rubyの言語仕様についての突っ込んだ話になり、Railsチュートリアルの範疇を超えます。

  3. 厳密には、Railsが使っているActiveSupportというライブラリです。 2

  4. 読み取りのみであればattr_reader、書き込みのみであればattr_writerという名前のメソッドを使います。

4
3
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
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?