背景
世の中にはオブジェクト指向の解説が多くありますが大半が、コードのオブジェクト化で止まっている物が多く、
オブジェクト指向が一体なんの役に立つのかが全く理解できない人が多くいるのではないでしょうか?
前提としてオブジェクト指向についての筆者の意見を述べると、は以下のとおりです
- オブジェクト指向はこうするべき、という答えは無い
- 現実世界とは別
- 開発を楽にするための設計方針がオブジェクト指向
そこで、Switchで話題になったゲームであるゼルダの伝説を題材に、日本では比較的ユーザーが多いRubyを使ってオブジェクト指向を解説してみます。
(※本来はKotlinのほうがうまく説明できるので、Kotlin編は後日投稿します)
説明するに当たり、ポリモーフィズム、カプセル化などの用語は一切使いません。
ゼルダの伝説 ブレス オブ ザ ワイルドとは
掛け算のあそびができることが特徴であり、物理法則と化学反応を組み合わせた遊び方ができます。
例)空を飛ぶ、燃やす、ものを投げる 等
以下が参考になります
https://game.watch.impress.co.jp/docs/news/1047403.html
実装する要件
以下の機能をRubyで実装してみます。
- プレイヤーと武器と敵がある。武器(マスターソード、森人の剣、強化されたマスターソード)、敵(ボコブリン、ファイアチュチュ、ガーディアン)
- 強化されたマスターソードというアイテムは存在しないのですが、説明のために入れておきます
- プレイヤーが武器を使って敵を攻撃するとダメージを与える
- プレイヤーがガーディアンをマスターソードで攻撃すると2倍のダメージを与えられる
- プレイヤーがファイアチュチュを攻撃すると手持ちの武器が可燃性の場合燃えてなくなる
- 森人の剣は燃える
作成したプログラム
stage1.rbが最初の状態で、徐々にオブジェクト指向を取り入れてコードに変更を加えていっています。
実行例
$ ruby stage1.rb
"装備アイテム 1 マスターソード、2 森人の剣 3 強化されたマスターソード"
1
"攻撃対象。1 ボコブリン 2 ファイアチュチュ 3 ガーディアン"
1
#<Bokoburin:0x00007fb2e31b2608 @health=200>
"敵のHP170"
"プレイヤーの手持ち武器マスターソード"
最初の段階(stage1)
class MasterSword
attr_reader :name, :power
def initialize
@name = 'マスターソード'
@power = 30
end
end
class UpgradedMasterSword
attr_reader :name, :power
def initialize
@name = 'マスターソード'
@power = 60
end
end
class ForestDwellersSword
attr_reader :name, :power
def initialize
@name = '森人の剣'
@power = 20
end
end
class RedChuchu
attr_accessor :health
def initialize
@health = 100
end
end
class Bokoburin
attr_accessor :health
def initialize
@health = 200
end
end
class Guardian
attr_accessor :health
def initialize
@health = 1000
end
end
class Player
attr_accessor :weapon
def initialize
@weapon = nil
end
end
player = Player.new
p '装備アイテム 1 マスターソード、2 森人の剣 3 強化されたマスターソード'
input = gets
player.weapon = case input.chomp!
when '1'
MasterSword.new
when '2'
ForestDwellersSword.new
when '3'
UpgradedMasterSword.new
else
raise '1~3を入力してください'
end
p '攻撃対象。1 ボコブリン 2 赤チュチュ 3 ガーディアン'
input = gets
enemy = case input.chomp!
when '1'
Bokoburin.new
when '2'
RedChuchu.new
when '3'
Guardian.new
else
raise '1~3を入力してください'
end
p enemy
if enemy.class == Guardian && player.weapon.class == MasterSword || enemy.class == Guardian && player.weapon.class == UpgradedMasterSword
enemy.health -= player.weapon.power * 2
else
enemy.health -= player.weapon.power
end
if enemy.class == RedChuchu && player.weapon.class == ForestDwellersSword
player.weapon = nil
end
p "敵のHP#{enemy.health}"
p "プレイヤーの手持ち武器#{player.weapon&.name}"
やったこと
- 武器と敵のオブジェクト化
- ただしオブジェクトはデータのみを持っている
このコードの問題点
神クラスのようなものが存在
if enemy.class == Guardian && player.weapon.class == MasterSword || enemy.class == Guardian && player.weapon.class == UpgradedMasterSword
enemy.health -= player.weapon.power * 2
else
enemy.health -= player.weapon.power
end
if enemy.class == RedChuchu && player.weapon.class == ForestDwellersSword
player.weapon = nil
end
このコードの問題点としては
- 条件分岐が複雑でひと目で何をしているかわからない
- 変更に弱い
- 新しく条件を追加するのが大変
- バグが発生しやすい
このようにコードのオブジェクト化のみでは、オブジェクトを使わないコードと大差が無いです。
オブジェクトにメソッドをもたせる(stage2.rb)
class Sword
attr_reader :name, :power
end
class MasterSword < Sword
def initialize
@name = 'マスターソード'
@power = 30
end
end
class UpgradedMasterSword < MasterSword
def initialize
@name = 'マスターソード'
@power = 60
end
end
class ForestDwellersSword < Sword
def initialize
@name = '森人の剣'
@power = 20
end
end
class Enemy
attr_accessor :health
def damage(player)
@health -= player.weapon.power
end
end
class RedChuchu < Enemy
def initialize
@health = 100
end
def damage(player)
super
if player.weapon.class == ForestDwellersSword
player.weapon = nil
end
end
end
class Bokoburin < Enemy
def initialize
@health = 200
end
end
class Guardian < Enemy
def initialize
@health = 1000
end
def damage(player)
damage = player.weapon.power
if player.weapon.is_a?(MasterSword)
damage *= 2
end
@health -= damage
end
end
class Player
attr_accessor :weapon
def initialize
@weapon = nil
end
end
player = Player.new
p '装備アイテム 1 マスターソード、2 森人の剣 3 強化されたマスターソード'
input = gets
player.weapon = case input.chomp!
when '1'
MasterSword.new
when '2'
ForestDwellersSword.new
when '3'
UpgradedMasterSword.new
else
raise '1~3を入力してください'
end
p '攻撃対象。1 ボコブリン 2 ファイアチュチュ 3 ガーディアン'
input = gets
enemy = case input.chomp!
when '1'
Bokoburin.new
when '2'
RedChuchu.new
when '3'
Guardian.new
else
raise '1~3を入力してください'
end
p enemy
enemy.damage(player)
p "敵のHP#{enemy.health}"
p "プレイヤーの手持ち武器#{player.weapon&.name}"
やったこと
- 敵にdamageというメソッドをもたせた * ダメージの条件は敵自身が知っている
- クラスの継承を導入した
変更点
- Enemyクラスにメソッドを作り、各クラスでオーバーライドする
class Enemy
attr_accessor :health
def damage(player)
@health -= player.weapon.power
end
end
class Guardian < Enemy
def initialize
@health = 1000
end
def damage(player)
damage = player.weapon.power
if player.weapon.is_a?(MasterSword)
damage *= 2
end
@health -= damage
end
end
このように変更することでstage1.rbで存在していた神クラスのようなものは以下1行になりました。
enemy.damage(player)
このように設計することで、新しい条件を持った敵や武器を追加することが簡単になりました。
条件の追加(stage3.rb)
以下の条件を新しく追加します
- 武器を追加する
- 木の枝 攻撃力1 可燃性
- Swordクラスを継承する
- 森人の槍 攻撃力20 可燃性
- Spearクラスを継承する
class Weapon
attr_reader :name, :power
end
class Sword < Weapon
end
class Spear < Weapon
end
class MasterSword < Sword
def initialize
@name = 'マスターソード'
@power = 30
end
end
class UpgradedMasterSword < MasterSword
def initialize
@name = 'マスターソード'
@power = 60
end
end
class FlammableSword < Sword
def burn(player)
player.weapon = nil
end
end
class ForestDwellersSword < FlammableSword
def initialize
@name = '森人の剣'
@power = 20
end
end
class TreeBranch < FlammableSword
def initialize
@name = '木の枝'
@power = 1
end
end
class FlammableSpear < Spear
def burn(player)
player.weapon = nil
end
end
class ForestDwellersSpear < FlammableSpear
def initialize
@name = '森人の槍'
@power = 20
end
end
class Enemy
attr_accessor :health
def damage(player)
@health -= player.weapon.power
end
end
class RedChuchu < Enemy
def initialize
@health = 100
end
def damage(player)
super
if player.weapon.is_a?(FlammableSword) || player.weapon.is_a?(FlammableSpear)
player.weapon.burn(player)
end
end
end
class Bokoburin < Enemy
def initialize
@health = 200
end
end
class Guardian < Enemy
def initialize
@health = 1000
end
def damage(player)
damage = player.weapon.power
if player.weapon.is_a?(MasterSword)
damage *= 2
end
@health -= damage
end
end
class Player
attr_accessor :weapon
def initialize
@weapon = nil
end
end
player = Player.new
p '装備アイテム 1 マスターソード、2 森人の剣 3 強化されたマスターソード 4 木の枝 5 森人の槍'
input = gets
player.weapon = case input.chomp!
when '1'
MasterSword.new
when '2'
ForestDwellersSword.new
when '3'
UpgradedMasterSword.new
when '4'
TreeBranch.new
when '5'
ForestDwellersSpear.new
else
raise '1~3を入力してください'
end
p '攻撃対象。1 ボコブリン 2 ファイアチュチュ 3 ガーディアン'
input = gets
enemy = case input.chomp!
when '1'
Bokoburin.new
when '2'
RedChuchu.new
when '3'
Guardian.new
else
raise '1~3を入力してください'
end
p enemy
enemy.damage(player)
p "敵のHP#{enemy.health}"
p "プレイヤーの手持ち武器#{player.weapon&.name}"
やったこと
- 燃えるという処理を武器クラスに持たせた
- FlammableSword、FlammableSpear
変更点
class FlammableSword < Sword
def burn(player)
player.weapon = nil
end
end
class FlammableSpear < Spear
def burn(player)
player.weapon = nil
end
end
可燃性の武器をいう概念を作りました。
※ 本来は抽象クラスとして実装しますが、Rubyには抽象クラスという概念がないため行っていません。
このコードの問題点
条件分岐、クラス構造が複雑
def damage(player)
super
if player.weapon.is_a?(FlammableSword) || player.weapon.is_a?(FlammableSpear)
player.weapon.burn(player)
end
end
この分岐は良くないです。
例えば、燃える盾というものを追加しなければならないとき
FlammableShieldを更に条件に追加しなければならないです。
追加を忘れてしまうとバグの原因になります。
クラスの関係図を図で表すと、上記のように、役割が被っているクラスがあります。
クラスの継承だけでは解決できません。
性質を導入する(stage4.rb)
class Weapon
attr_reader :name, :power
end
class Sword < Weapon
end
class Spear < Weapon
end
class MasterSword < Sword
def initialize
@name = 'マスターソード'
@power = 30
end
end
class UpgradedMasterSword < MasterSword
def initialize
@name = 'マスターソード'
@power = 60
end
end
module Flammable
def burn(player)
player.weapon = nil
end
end
class ForestDwellersSword < Sword
include Flammable
def initialize
@name = '森人の剣'
@power = 20
end
end
class TreeBranch < Sword
include Flammable
def initialize
@name = '木の枝'
@power = 1
end
end
class ForestDwellersSpear < Spear
include Flammable
def initialize
@name = '森人の槍'
@power = 20
end
end
class Enemy
attr_accessor :health
def damage(player)
@health -= player.weapon.power
end
end
class RedChuchu < Enemy
def initialize
@health = 100
end
def damage(player)
super
if player.weapon.include?(Flammable)
player.weapon.burn(player)
end
end
end
class Bokoburin < Enemy
def initialize
@health = 200
end
end
class Guardian < Enemy
def initialize
@health = 1000
end
def damage(player)
damage = player.weapon.power
if player.weapon.is_a?(MasterSword)
damage *= 2
end
@health -= damage
end
end
class Player
attr_accessor :weapon
def initialize
@weapon = nil
end
end
player = Player.new
p '装備アイテム 1 マスターソード、2 森人の剣 3 強化されたマスターソード 4 木の枝 5 森人の槍'
input = gets
player.weapon = case input.chomp!
when '1'
MasterSword.new
when '2'
ForestDwellersSword.new
when '3'
UpgradedMasterSword.new
when '4'
TreeBranch.new
when '5'
ForestDwellersSpear.new
else
raise '1~3を入力してください'
end
p '攻撃対象。1 ボコブリン 2 ファイアチュチュ 3 ガーディアン'
input = gets
enemy = case input.chomp!
when '1'
Bokoburin.new
when '2'
RedChuchu.new
when '3'
Guardian.new
else
raise '1~3を入力してください'
end
p enemy
enemy.damage(player)
p "敵のHP#{enemy.health}"
p "プレイヤーの手持ち武器#{player.weapon&.name}"
やったこと
- 可燃性(Flammable)という性質をインターフェースで実装した * Rubyではmoduleというものになる。
変更点
module Flammable
def burn(player)
player.weapon = nil
end
end
class ForestDwellersSword < Sword
include Flammable
def initialize
@name = '森人の剣'
@power = 20
end
end
class ForestDwellersSpear < Spear
include Flammable
def initialize
@name = '森人の槍'
@power = 20
end
end
このように定義し、ファイアチュチュの処理を以下のように変更しました。
class RedChuchu < Enemy
def initialize
@health = 100
end
def damage(player)
super
if player.weapon.include?(Flammable)
player.weapon.burn(player)
end
end
end
可燃性であれば燃やすという書き方に変えました。
これにより可燃性の武器を追加する場合、そのクラスに対してFlammableをincludeするだけで完了することになります。
さらなる仕様変更(stage5.rb)
以下の仕様変更を行います
- 攻撃したらプレイヤーを感電させるエレキチュチュを追加。
- 電気を通す導電性という性質を追加する * 感電すると、武器を落とす
class Weapon
attr_reader :name, :power
end
class Sword < Weapon
end
class Spear < Weapon
end
module Flammable
def burn(player)
player.weapon = nil
end
end
module Conductive
def shock(player)
player.weapon = nil
end
end
class MasterSword < Sword
include Conductive
def initialize
@name = 'マスターソード'
@power = 30
end
end
class UpgradedMasterSword < MasterSword
include Conductive
def initialize
@name = 'マスターソード'
@power = 60
end
end
class ForestDwellersSword < Sword
include Flammable
def initialize
@name = '森人の剣'
@power = 20
end
end
class TreeBranch < Sword
include Flammable
def initialize
@name = '木の枝'
@power = 1
end
def burn(player)
p '火がついた'
end
end
class ForestDwellersSpear < Spear
include Flammable
def initialize
@name = '森人の槍'
@power = 20
end
end
class Enemy
attr_accessor :health
def damage(player)
@health -= player.weapon.power
end
end
class RedChuchu < Enemy
def initialize
@health = 100
end
def damage(player)
super
if player.weapon.class <= Flammable
player.weapon.burn(player)
end
end
end
class YellowChuchu < Enemy
def initialize
@health = 100
end
def damage(player)
super
if player.weapon.class <= Conductive
player.weapon.shock(player)
end
end
end
class Bokoburin < Enemy
def initialize
@health = 200
end
end
class Guardian < Enemy
def initialize
@health = 1000
end
def damage(player)
damage = player.weapon.power
if player.weapon.class <= MasterSword
damage *= 2
end
@health -= damage
end
end
class Player
attr_accessor :weapon
def initialize
@weapon = nil
end
end
player = Player.new
p '装備アイテム 1 マスターソード、2 森人の剣 3 強化されたマスターソード 4 木の枝 5 森人の槍'
input = gets
player.weapon = case input.chomp!
when '1'
MasterSword.new
when '2'
ForestDwellersSword.new
when '3'
UpgradedMasterSword.new
when '4'
TreeBranch.new
when '5'
ForestDwellersSpear.new
else
raise '1~3を入力してください'
end
p '攻撃対象。1 ボコブリン 2 ファイアチュチュ 3 ガーディアン 4 エレキチュチュ'
input = gets
enemy = case input.chomp!
when '1'
Bokoburin.new
when '2'
RedChuchu.new
when '3'
Guardian.new
when '4'
YellowChuchu.new
else
raise '1~3を入力してください'
end
p enemy
enemy.damage(player)
p "敵のHP#{enemy.health}"
p "プレイヤーの手持ち武器#{player.weapon&.name}"
変更点
マスターソードに導電性を追加しました。
module Conductive
def shock(player)
player.weapon = nil
end
end
class MasterSword < Sword
include Conductive
def initialize
@name = 'マスターソード'
@power = 30
end
end
導電性の武器の場合、感電させるようにしました。
class YellowChuchu < Enemy
def initialize
@health = 100
end
def damage(player)
super
if player.weapon.class <= Conductive
player.weapon.shock(player)
end
end
end
まとめ
オブジェクト指向における重要な要素
以下の要素をうまく使うことがオブジェクト指向だと思います。
- クラス
- 実態があるものを表現するときに使う
- インターフェース
- 性質などを表現するときに使う
- Rubyではmodule
- インターフェースにメソッド持たせるかで宗教戦争になりますが、私はメソッドをもたせても良いと思います。
- 抽象クラス
- 実態が無い概念を表現するときに使う
- Rubyでは存在しない
Rubyはオブジェクト指向に向いている言語ではないため、抽象クラスの説明ができませんでしたが、
剣や槍という概念はゼルダの伝説に存在しますが、「剣」や「槍」といったアイテムはゲーム内には存在しません。
そういった実態はないが概念として存在するものが抽象クラスとなります。
SwordやEnemyクラスがそれに当たります。
オブジェクト指向とはなにか
- オブジェクト指向は設計の方針を大まかに指し示したもの
- オブジェクト指向の答えは無い
- 例では、武器をクラス、可燃性をインターフェースにしましたが、それを逆にしたほうがうまく設計できるのであればそちらのほうが良いです。
- 明確な正解はなく、どのようにして開発を楽にしてバグを少なくするかとしての手段としてオブジェクト指向を使います。
- クラス、インターフェース、(抽象クラス)をつかって表現する
終わりに
最後に思ったことを書いておきます。
オブジェクト指向が必要なのか
フレームワークに則って開発を行う場合、オブジェクト指向はほとんど役に立たないとおもいます。
自分でフレームワークだったり、ゲームエンジンを自作するときにオブジェクト指向が生かされます。
ゼルダの伝説の実際のコードとの対比
実際のコードを見たことが無いのでわからないですが、一部分くらいは合っていてほしいなと言う感想です。
ただし、このコードは投げる、移動させるということが考慮されていないです。
それらを追加する場合、説明しきれなくなってしまうため、簡略化して実装しました。