Edited at

ゼルダの伝説でオブジェクト指向がほぼ理解できる記事

More than 1 year has passed since last update.


背景

世の中にはオブジェクト指向の解説が多くありますが大半が、コードのオブジェクト化で止まっている物が多く、

オブジェクト指向が一体なんの役に立つのかが全く理解できない人が多くいるのではないでしょうか?

前提としてオブジェクト指向についての筆者の意見を述べると、は以下のとおりです


  • オブジェクト指向はこうするべき、という答えは無い

  • 現実世界とは別

  • 開発を楽にするための設計方針がオブジェクト指向

そこで、Switchで話題になったゲームであるゼルダの伝説を題材に、日本では比較的ユーザーが多いRubyを使ってオブジェクト指向を解説してみます。

(※本来はKotlinのほうがうまく説明できるので、Kotlin編は後日投稿します)

説明するに当たり、ポリモーフィズム、カプセル化などの用語は一切使いません。


ゼルダの伝説 ブレス オブ ザ ワイルドとは

掛け算のあそびができることが特徴であり、物理法則と化学反応を組み合わせた遊び方ができます。

例)空を飛ぶ、燃やす、ものを投げる 等

以下が参考になります

https://game.watch.impress.co.jp/docs/news/1047403.html


実装する要件

以下の機能をRubyで実装してみます。


  • プレイヤーと武器と敵がある。武器(マスターソード、森人の剣、強化されたマスターソード)、敵(ボコブリン、ファイアチュチュ、ガーディアン)


    • 強化されたマスターソードというアイテムは存在しないのですが、説明のために入れておきます



  • プレイヤーが武器を使って敵を攻撃するとダメージを与える

  • プレイヤーがガーディアンをマスターソードで攻撃すると2倍のダメージを与えられる

  • プレイヤーがファイアチュチュを攻撃すると手持ちの武器が可燃性の場合燃えてなくなる


    • 森人の剣は燃える



スクリーンショット 2018-10-31 10.33.14.png


作成したプログラム

https://gist.github.com/klriutsa/834d6a5336d9c77cfab01705971a18f5

stage1.rbが最初の状態で、徐々にオブジェクト指向を取り入れてコードに変更を加えていっています。

実行例

$ ruby stage1.rb

"装備アイテム 1 マスターソード、2 森人の剣 3 強化されたマスターソード"
1
"攻撃対象。1 ボコブリン 2 ファイアチュチュ 3 ガーディアン"
1
#<Bokoburin:0x00007fb2e31b2608 @health=200>
"敵のHP170"
"プレイヤーの手持ち武器マスターソード"


最初の段階(stage1)


stage1.rb

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}"


やったこと


  • 武器と敵のオブジェクト化


    • ただしオブジェクトはデータのみを持っている




このコードの問題点


神クラスのようなものが存在

スクリーンショット 2018-10-31 9.46.16.png

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)


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クラスを継承する




stage3.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

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を更に条件に追加しなければならないです。

追加を忘れてしまうとバグの原因になります。

スクリーンショット 2018-10-31 10.05.33.png

クラスの関係図を図で表すと、上記のように、役割が被っているクラスがあります。

クラスの継承だけでは解決できません。


性質を導入する(stage4.rb)


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)

以下の仕様変更を行います


  • 攻撃したらプレイヤーを感電させるエレキチュチュを追加。

  • 電気を通す導電性という性質を追加する
      * 感電すると、武器を落とす


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クラスがそれに当たります。


オブジェクト指向とはなにか


  • オブジェクト指向は設計の方針を大まかに指し示したもの

  • オブジェクト指向の答えは無い


    • 例では、武器をクラス、可燃性をインターフェースにしましたが、それを逆にしたほうがうまく設計できるのであればそちらのほうが良いです。

    • 明確な正解はなく、どのようにして開発を楽にしてバグを少なくするかとしての手段としてオブジェクト指向を使います。



  • クラス、インターフェース、(抽象クラス)をつかって表現する


終わりに

最後に思ったことを書いておきます。


オブジェクト指向が必要なのか

フレームワークに則って開発を行う場合、オブジェクト指向はほとんど役に立たないとおもいます。

自分でフレームワークだったり、ゲームエンジンを自作するときにオブジェクト指向が生かされます。


ゼルダの伝説の実際のコードとの対比

実際のコードを見たことが無いのでわからないですが、一部分くらいは合っていてほしいなと言う感想です。

ただし、このコードは投げる、移動させるということが考慮されていないです。

それらを追加する場合、説明しきれなくなってしまうため、簡略化して実装しました。