データの管理の側面から見るクラスの特徴
要件:ユーザーを表すデータをプログラム上で処理したいとする。
この要件に対して、ハッシュ配列を扱う事で対応できる。
users=[]
users << {first_name:'Alice',last_name:'Ruby',age:20}
users << {first_name:'Bob',last_name:'Python',age:30}
users.each do |user|
puts "氏名:#{user[:first_name]}#{user[:last_name]}、年齢:#{user[:age]}"
end
#=> 氏名:AliceRuby、年齢:20
# 氏名:BobPython、年齢:30
しかしハッシュを扱う上での注意点
- キーをタイプミスした場合にnilが返ってくる
- 新しいキーの追加・内容の変更が簡単にできる
これらの性質から、ハッシュによるデータの管理は、「脆くて壊れやすい」特徴を持ちます。
大きなプログラムの場合、ハッシュでは管理しきれなくなってしまう。
そこで登場するのが、クラスです。
Userクラスと言う新しいデータ型を作り、「堅牢なプログラム」を確立する。
ハッシュの注意点に対し、
- タイプミスでは例外を排出
- 新しい属性の追加や変更も防止
そしてクラス内には、独自でメソッドを追加もできる。
クラスはこのように、内部にデータを保持し、さらに自分が保持しているデータを利用する独自のメソッドを持つことができます。
オブジェクト指向プログミングにおける用語説明
1.クラス
2.オブジェクト/インスタンス/レシーバ
クラスをもとにして作られたデータのかたまり
3.メソッド/メッセージ
何らかの処理をひとまとめにして名前を付け、何度も再利用できるようにしたものがメソッド
4.状態(ステート)
オブジェクトごとに保持されるデータのことを「オブジェクトの状態(もしくはステート)」と呼ぶことがあります。
5.属性(アトリビュート/プロパティ)
オブジェクトごとに保持されるデータのことを「オブジェクトの状態(もしくはステート)」と呼ぶことがあります。
クラスの定義
オブジェクトの作成とinitializeメソッド
クラスからオブジェクトを作成する際は、newメソッドを使う。
User.new
その際に呼ばれているのがinitializeメソッド
インスタンスを初期化するために実行したい処理があれば、このinitializeメソッドでその処理を実装します
インスタンスメソッド
用語の定義
- クラスの内部で定義したメソッドのこと
- インスタンスメソッドは、そのクラスのインスタンスに対して呼び出し可能
インスタンス変数とアクセサメソッド
クラス内部では、インスタンス変数を作成可能。
インスタンス変数は、同じインスタンスの内部で共有される変数。
一方、メソッドやブロックの内部で作成される変数のことを、ローカル変数と呼ぶ
メソッドやブロックが呼び出される度に毎回作り直される。
インスタンス変数はクラスの外部から参照(読み込み)することはできません。
もし参照したい場合には、参照用のメソッドを作成する必要がある。
class User
def initialize(name)
@name = name
end
# @nameを**外部から参照する為のメソッド**
def name
@name
end
end
========================
user = User.new('Alice')
# nameメソッドを経由して@nameの内容を取得する
user.name #=> "Alice"
同様に、インスタンス変数の内容を外部から変更(書き込み)することはできない。
もし変更したい場合には、変更用のメソッドを作成する必要がある。
class User
def initialize(name)
@name = name
end
# @nameを**外部から変更する為のメソッド**
def name=(value)
@name = value
end
end
========================
user = User.new('Alice')
# これは変数の代入ではなく、name=メソッドを呼び出している
user.name = 'Bob'
user.name #=> "Bob"
このようにインスタンス変数の値を読み書きするメソッドのことをアクセサメソッドと呼ぶ。(ゲッターメソッド、セッターメソッド)
attr_accessor
:読み書き可能
attr_reader
:読み込みのみ可能
attr_writer
:書き込みのみ可能
これまでの具体例は以下のようにまとめる事ができる。
class User
attr_accessor :name
def initialize(name)
@name = name
end
end
========================
user = User.new('Alice')
user.name #=> "Alice"
user.name = 'Bob'
user.name #=> "Bob"
クラスメソッド
クラスに関連は深いものの、ひとつひとつのインスタンスに含まれるデータは使わないメソッドを定義したい場合もあります。
そのような場合はクラスメソッドを定義したほうが使い勝手が良くなります。
ただクラスメソッドは、厳密に言うとクラスオブジェクトの特異メソッドと言う位置付け。
# 定義方法
# パターン1
def クラス名
def self.クラスメソッド名
# 処理
end
end
# パターン2
def クラス名
class << self
def クラスメソッド名
# 処理
end
end
end
========================
# 呼び出し方法
# クラス名.メソッド名
User.hello
メソッド名の表記
# インスタンスメソッドの場合
# クラス名#メソッド名
# String#to_i
# クラスメソッドの場合
# クラス名.メソッド名 または クラス名::メソッド名
# File.exsit?
# File::exsit?
定数の扱い
-
定数は必ず大文字で書き始める
-
定数はクラスの外部から直接参照できる
クラス名::定数名
-
メソッドの内部では作成することはできない
-
定数には再代入が可能(クラス外部からでも可能)
クラスを凍結させる事で、再代入を防止できる。しかしクラスの凍結は、デメリットの方が大きい。暗黙の了解として、定数を上書きする人はいない。
-
定数はミュータブルなオブジェクトに注意
再代入せずとも、文字列や配列などのミュータブルなオブジェクトの場合、オブジェクトそのものを破壊的に変更可能。
その場合は、定数の値を凍結させる。
SOME_NAMES=['Foo', 'Bar', 'Baz'].freeze
しかし配列の場合は各要素も凍結させないといけない点に注意が必要。
self
Rubyではインスタンス自身を表すselfキーワードが存在する。メソッド内部で、他のメソッドを呼び出す際に暗黙的にselfに対してメソッドを呼び出している。
ただしメソッド内でselfを省略する際は、ローカル変数と解釈されないか・セッターメソッドなどでは注意が必要。
クラスの継承
継承を使いたいと思ったときは機能ではなく、性質や概念の共通点に着目してください。
性質や概念が共通しているか確認する方法「is-aの関係」
「サブクラスはスーパークラスの一種である(サブクラス is a スーパークラス)」
「DVDはProductの一種である」
デフォルトの継承
独自に作成したクラスは、デフォルトでObjectクラスを継承する。
to_sやnil?などが適用可能。
意図的に他のクラスを参照したい場合は、クラス名の部分に明記
class サブクラス < スーパークラス
end
super
スーパークラスと全く同じ処理を行う場合に扱うメソッド。
オーバーライド
サブクラスでは、スーパークラスで作成したメソッドと、同名のメソッドを定義できる。
その場合、サブクラスではサブクラスの同名メソッドが参照される、このことをメソッドのオーバライドという。
メソッドの公開レベル
publicメソッド
クラスの外部からでも自由に呼び出す事ができるメソッド。
privateメソッド
クラスの外からは呼び出せず、クラスの内部でのみ使えるメソッド
厳密に言うとprivateメソッドは「レシーバを指定して呼び出すことができないメソッド」
またprivateメソッドに関しては、サブクラスで呼び出すことも可能。
よって、Rubyで継承を扱う際にはサーパークラスの実装もしっかりと把握していないといけない。
protectedメソッド
そのメソッドを定義したクラス自身と、そのサブクラスのインスタンスメソッドからレシーバ付きで呼び出せる。
外部には公開したくないが、同じクラスやサブクラスの中であればレシーバ付きで呼び出せるようにしたい
その他
ネストしたクラス
クラス名の予期せぬ衝突を防ぐ「名前空間」を作る際によく使われる
class Product
class Clothes
end
end
しかし、名前空間を作る場合はクラスよりもモジュールが使用されることの方が多い。
オープンクラスとモンキーパッチ
Rubyのクラスの継承に制限はない。
また定義済みのクラスに対して、メソッドの追加・メソッドの定義の上書きなどが可能であり、変更に対してオープンであることから「オープンクラス」と呼ばれる。
そして既存のメソッドに対して、自分が期待する挙動へ上書きすることを、「モンキーパッチ」と言う。
これらの機能のおかげで、外部ライブラリに対して、変更を加え、上書きを行う事で、自らの期待する動作に近づける事ができる。
ダックタイピング
オブジェクトのクラスが何であろうとそのメソッドが呼び出せれば良しとするプログラミングスタイルのこと
その他
- 演算子を再定義する
- 統治を判断する演算子の理解
演習問題
改札機プログラムの作成。
- 梅田 - 十三 - 三国の3駅
- 運賃は以下のように設定。
1区間(梅田 ⇄ 十三, 十三 ⇄ 三国)は150円, 2区間(梅田 ⇄ 三国)は190円
この設定を実現するプログラムと、それを確認するテストを追加する。
require 'minitest/autorun'
require '../lib/gate.rb'
require '../lib/ticket.rb'
class GateTest < Minitest::Test
# 全てのテストで共通する改札オブジェクトを事前に作成する
def setup
@umeda = Gate.new(:umeda)
@juso = Gate.new(:juso)
@mikuni = Gate.new(:mikuni)
end
# ここから下はそれぞれ4つのテストケース
def test_umeda_to_juso
ticket = Ticket.new(150)
@umeda.enter(ticket)
assert @juso.exit(ticket)
end
def test_umeda_to_mikuni_when_fare_is_not_enough
ticket = Ticket.new(150)
@umeda.enter(ticket)
refute @mikuni.exit(ticket)
end
def test_umeda_to_mikuni_when_fare_is_enough
ticket = Ticket.new(190)
@umeda.enter(ticket)
assert @mikuni.exit(ticket)
end
def test_juso_to_mikuni
ticket = Ticket.new(150)
@juso.enter(ticket)
assert @mikuni.exit(ticket)
end
end
class Gate
STATIONS = [:umeda, :juso, :mikuni] # それぞれの改札名の配列を定数で管理
FARES = [150, 190] # それぞれの区間の料金の配列を定数で管理
def initialize(name)
@name = name # オブジェクトが作成された際に、引数をインスタンス変数の@nameに格納
end
def enter(ticket)
ticket.stamp(@name) # 引数のticketオブジェクトをレシーバに、引数に改札オブジェクトのインスタンス変数を持つ、stampメソッドを呼び出す
end
def exit(ticket) # ticketオブジェクトの状態と、必要な料金を比較する
fare = calc_fare(ticket)
fare <= ticket.fare
end
def calc_fare(ticket)
#
from = STATIONS.index(ticket.stamped_at)
to = STATIONS.index(@name)
distance = to - from
FARES[distance - 1]
end
end
class Ticket
attr_reader :fare, :stamped_at # クラス外部から、値を読み取れるようにする
def initialize(fare)
@fare = fare # インスタンス作成時に、引数を@fareインスタンス変数に格納
end
def stamp(name)
@stamped_at = name # 引数をインスタンス変数@stamed_atに格納
end
end
余談
この章はこれらまでの章の中でも特にボリューミーであった。
具体的なアクセサーメソッドの利用方法や、Rubyにおけるクラスの概念であるダックタイピング・モンキーパッチなど、今後恐らく実務開発で触れる事になるであろう技術がてんこ盛りでした。