🏹

Rubyでオブジェクト指向の3大要素を解説

2023/10/27に公開

前提知識

  • Rubyの基礎的なコードの書き方が分かる
  • クラス、継承、オーバーライドの書き方が分かる

オブジェクト指向

オブジェクト指向が全くわからないという方は、別の記事で分かりやすく解説していますので、まずはこちらを読んでみてください。
https://zenn.dev/makoto00000/articles/28d7ff6cae2c4e

オブジェクト指向の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を例に)

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か?といった視点で考えることが重要だと思います。

GitHubで編集を提案

Discussion