Rubyでオブジェクト指向の3大要素を解説
前提知識
- Rubyの基礎的なコードの書き方が分かる
- クラス、継承、オーバーライドの書き方が分かる
オブジェクト指向
オブジェクト指向が全くわからないという方は、別の記事で分かりやすく解説していますので、まずはこちらを読んでみてください。
オブジェクト指向の3大要素として以下の3つが挙げられます。
- カプセル化
- 継承
- ポリモーフィズム
カプセル化(RPGを例に)
「クラス内のデータに直接アクセスして勝手に変更されるのを防ぐこと」です。
class Unit
attr_writer :name, :hp
def initialize
@name = "勇者"
@hp = 100
end
end
# => @name, @hpは外部から読み取りはできるが、書き換えは不可
- attr_writer (値の読み取りのみ許可)
- attr_reader (値の変更のみ許可)
- attr_accessor (値の読み取り、書き取りを許可)
これらのattrメソッドを記述することで、クラス変数へのアクセスを制御できます。
ただし、何でもかんでもつけていたら、カプセル化の意味がありません。
外からもアクセスが必要かどうか、また呼び出しに条件付けをする場合は、セッターを定義して、その中で条件を付けることで、想定外の値の変更を防ぐことが可能です。
継承 (食べ物を例に)
コードが増えていくにつれ、コードの「再利用性」と「拡張性」を意識することが重要となってきます。
# ダメな例
class Tomato
def initialize
@name = "トマト"
end
end
class Carrot
def initialize
@name = "にんじん"
end
end
今は野菜だけでまとまっているからいいけど、肉とか魚とか他の食材が増えてくると管理が大変です。
さらに、例えば色情報を持たせたいと思ったときに、増やしたクラスも含め全てに「@color」を追加していく必要があります。
# 良い例
class Food
def initialize(name)
@name = name
end
end
class Vegetable < Food
end
class Meat < Food
end
class Fish < Food
end
tomato = Vegetable.new("トマト")
beef = Meat.new("ビーフ")
tuna = Fish.new("マグロ")
こちらの例では「Food」という抽象的なクラスを用意し、そのFoodクラスを継承して、「Vegetable」クラス、「Meat」クラス、「Fish」クラスが作成されており、食材の種類ごとにクラスが分けられているので、分かりやすいですね。
例えば栄養素の情報を持たせたくなったら、Foodクラスに@nutrientsみたいに記述すれば、継承先のクラスには変更の必要ありません。
また同じように、メソッドを親クラスで定義しておくこともできます。
長い処理だけど、どのクラスでも共通のメソッドを持たせたいときに便利で、継承先にいちいち書かなくても、Foodクラスに書いておけば、継承先のクラスでも使い回しができます。
ポリモーフィズム (RPGを例に)
class Unit
def initialize(name)
@name = name
end
def attack # 内容は定義しない
end
end
class Warriar < Unit
def attack # 内容を定義
puts "#{@name}は剣で攻撃!"
end
end
class Mage < Unit
def attack # 内容を定義
puts "#{@name}は魔法で攻撃!"
end
end
warriar = Warriar.new("戦士A")
mage = Mage.new("魔道士A")
warriar.attack
mage.attack
# => 戦士Aは剣で攻撃!
# => 魔道士Aは魔法で攻撃!
Unitクラスではattackメソッドを定義せず、Unitクラスを継承している「Warriar」クラス、「Mage」クラスでattackメソッドの内容を定義しています。
このように、メソッド名は親クラスと同じにして、継承先で中身を記述することを、「オーバーライド」と言います。
今後、職業が増えたとしても「クラス名.attack」と記述することは共通ですので、変更箇所は最小限で済みます。
例えば同じような処理を、「職業名によって攻撃内容を条件分岐させる」みたいな書き方をすると、コードは冗長になってしまい分かりにくくなります。
さらに、書き方を間違えれば他の職業にも影響を与えてしまいます。
# ダメな条件分岐の例
def attack
if @name == "戦士A"
puts "#{@name}は剣で攻撃!"
elsif @name == "魔道士A"
puts "#{@name}は魔法で攻撃!"
end
end
単一責任の原則
3大要素には挙げられませんが、オブジェクト指向で開発を進める上で非常に重要な原則ですので、同じように解説します。
先程のポリモーフィズムでのRPGのコードを例に見てみます。
現在Unitクラスにはattackメソッドしか定義していないので、ゲームのキャラクターが攻撃しかしないということであれば、Unitクラスは、「攻撃」に対する単一の責任を負っているということになります。
しかし、RPGでは通常、攻撃以外にも、「道具を使う」「スキルを使う」「逃げる」などの行動が選択肢として与えられますよね。そこでそういった行動ができるようにUnitクラスに変更を加えます。
class Unit
def initialize(name)
@name = name
end
def attack
end
def skill # <= 追加
# スキルを選択する処理
end
def item # <= 追加
# 手持ちのアイテムを選択する処理
end
def escape # <= 追加
# バトルから逃げる処理
end
end
このようにコードを変更した場合、Unitクラスは多くの行動に対する責任を負っていることになり、単一責任とは言えません。
この場合、「Action」クラスを別に定義し、行動に関するメソッドはActionクラスに任せれば、Unitクラス、Actionクラスはそれぞれ単一の責任があると言えるでしょう。
まとめ
必ずしも、こういった書き方が正解というのはありません。
オブジェクト指向の根本的な考え方は理解しておきつつも、
- 修正や拡張を想定し柔軟に対応できるコード
- 誰が見ても読みやすいコード
にするためにはどう書くのがbetterか?といった視点で考えることが重要だと思います。
Discussion