僕は、ドラクエライバルズ がかなり好きでよくプレイしているのですが、ライバルズを題材に Ruby のプログラミング問題を考えてみたので公開します。
回答を見る前に問題を解きたい場合は、スライドで見るのがおすすめです。
気が向いたらどうぞ。
問題
ドラクエライバルズには、7人のリーダーが存在する。各リーダーは、それぞれ以下のスキルを持っている。
リーダー名 | 役職(role) | スキル名 |
---|---|---|
テリー | 戦士(warrior) | 稲妻の加護 |
ゼシカ | 魔法使い(witch) | 紅蓮の火球 |
アリーナ | 武闘家(monk) | おてんば姫 |
ククール | 僧侶(priest) | 癒しの波動 |
トルネコ | 商人(merchant) | お宝発見 |
ミネア | 占い師(fortune_teller) | 水晶占い |
ピサロ | 魔王(satan) | 魔族の戦士 |
それぞれのリーダーの情報のうち、以下のものが配列として渡される。
leaders = [
{ name: "テリー", role: "warrior" },
{ name: "ゼシカ", role: "witch" },
{ name: "アリーナ", role: "monk" },
{ name: "ククール", role: "priest" },
{ name: "トルネコ", role: "merchant"},
{ name: "ミネア", role: "fortune_teller" },
{ name: "ピサロ", role: "satan" }
]
上記の渡された情報をもとに、インスタンスを作成し、それぞれのインスタンスメソッド execute_skill
を実行すると、以下の出力が得られるようなコードを実装せよ。
(ただし、渡される配列の値を全て使う必要はない。必要だと判断したものだけ使って実装して良いものとする。)
テリーは稲妻の加護を使った!
ゼシカは紅蓮の火球を使った!
アリーナはおてんば姫を使った!
ククールは癒しの波動を使った!
トルネコはお宝発見を使った!
ミネアは水晶占いを使った!
ピサロは魔族の戦士を使った!
注) 渡された情報のうち、戦士であるテリーの情報からインスタンスを作成し、execute_skill を実行したときの例
leader = Leader.new(params)
leader.execute_skill
=> テリーは稲妻の加護を使った!
解答例 その1. case で分岐する
コード量は冗長になってしまうが、case で分岐して処理するのがひとつ。
あまりいい実装ではありません。
class Leader
attr_reader :name, :role
def initialize(**params)
@name = params[:name]
@role = params[:role]
end
def execute_skill
skill =
case @role
when "warrior"
"稲妻の加護"
when "witch"
"紅蓮の火球"
when "monk"
"おてんば姫"
when "priest"
"癒しの波動"
when "merchant"
"お宝発見"
when "fortune_teller"
"水晶占い"
when "satan"
"魔族の戦士"
else
raise "role が不適切です"
end
puts "#{@name}は#{skill}を使った!"
end
end
leaders = [
{ name: "テリー", role: "warrior" },
{ name: "ゼシカ", role: "witch" },
{ name: "アリーナ", role: "monk" },
{ name: "ククール", role: "priest" },
{ name: "トルネコ", role: "merchant"},
{ name: "ミネア", role: "fortune_teller" },
{ name: "ピサロ", role: "satan" }
]
leaders.each do |params|
leader = Leader.new(params)
leader.execute_skill
end
解答例 その2. 振る舞いが同じなクラスを作る
同じ execute_skill
というメソッドを持ったクラスを準備する(振る舞いを同じにする)ことで以下のようなコードを書くことができます。
class Warrior
def execute_skill
puts "テリーは稲妻の加護を使った!"
end
end
class Witch
def execute_skill
puts "ゼシカは紅蓮の火球を使った!"
end
end
class Monk
def execute_skill
puts "アリーナはおてんば姫を使った!"
end
end
class Priest
def execute_skill
puts "ククールは癒しの波動を使った!"
end
end
class Merchant
def execute_skill
puts "トルネコはお宝発見を使った!"
end
end
class FortuneTeller
def execute_skill
puts "ミネアは水晶占いを使った!"
end
end
class Satan
def execute_skill
puts "ピサロは魔族の戦士を使った!"
end
end
require "active_support/all"
leaders = [
{ name: "テリー", role: "warrior" },
{ name: "ゼシカ", role: "witch" },
{ name: "アリーナ", role: "monk" },
{ name: "ククール", role: "priest" },
{ name: "トルネコ", role: "merchant"},
{ name: "ミネア", role: "fortune_teller" },
{ name: "ピサロ", role: "satan" }
]
roles = leaders.map { |params| params[:role].camelize }
roles.each do |role|
# role の文字列から各 class のインスタンスを作成
leader = role.constantize.new
leader.execute_skill
end
ダックタイピング
解答例 その2 のように、同じメソッドを持ったクラスを準備しておくと、以下のようなコードが動作するようになります。
class Player
def touch_skill(leader)
leader.execute_skill
end
end
# リーダーとして戦士を選択済みとする
selected_leader = Warrior.new
player = Player.new
player.touch_skill(selected_leader)
これは、実際にプレイするひとが、スキルを発動することをイメージして書いたコードです。
この例では、 Warrior
のインスタンスを touch_skill
の引数に渡していますが、渡すインスタンスは、別のクラスのものでも動くわけです。
このように、 振る舞いを同じにしておく(同じメソッドを持っていて実行できる) ようにしておくと、拡張性の高いコードを書くことができます。
このような実装を、ダックタイピング と呼びます。
その他の解説
文字列から class を作る
今回の解答では、文字列を class に変換する処理をしています。これは、 ActiveSupport が提供している constantize
メソッドで実現できます。
もう少し簡単な例で書くと、こんな感じ。
require "active_support/all"
# 文字列 "Array" から class の実態を作成して使う
"Array".constantize.new(5)
=> [nil, nil, nil, nil, nil]
ただ、クラス名と同じ文字列を準備する必要があります。
クラス名と同じ文字列を準備する
ミネアの role である fortune_teller
を例にして考えてみます。
この文字列を元に、クラスを作るには FortuneTeller
というかたちに変える必要がありますね。
- アンダーバーで文字をつなげる書き方を、スネークケース(snake case)
- 切り替わり部分を大文字にしてつなげる書き方を、キャメルケース(camel case)
と呼びます。
Ruby は基本的にスネークケースを使いますが、クラスはキャメルケースで書かれるので、スネークケースとキャメルケースの変換が必要になります。
キャメルケースへの変換は、これも ActiveSupport が提供している camelize
メソッドで簡単に変換できます。
require "active_support/all"
role = "fortune_teller"
role.camelize
=> "FortuneTeller"
あとは、これをもとにクラスに変換してあげればいいことがわかりますね。ちなみに、スネークケースに戻す場合は underscore
を使います。
case で分けずに class で分けるとどんなメリットがあるのか
例えば、 「トルネコ強くてムカつくので、スキルを弱体化させてやる」 と思い、変更を加えるとしましょう。
すると、Leader クラスひとつで処理を書いていると、case
文がめちゃくちゃ長くなって、とても読みづらいことになるんです。
今は execute_skill
だけなのでまだマシですが、実際はたくさんのメソッドがあり、すべて分岐処理が必要になるので、Leader クラスが肥大化します。
一つのファイルが長くなると、「トルネコ(商人)の挙動を変えたいだけなのに、関係のないリーダーのコードもたくさん書かれている大きなファイル」を見て行く必要がある んですよね。(以下を見れば、その気持ちがわかると思います笑)
# トルネコのスキルを弱体化させたいだけなのに
# 関係のないリーダーのスキルがたくさん並んでいて読みにくい
def execute_skill
skill =
case @role
when "warrior"
"稲妻の加護"
when "witch"
"紅蓮の火球"
when "monk"
"おてんば姫"
when "priest"
"癒しの波動"
when "merchant"
"お宝発見"
when "fortune_teller"
"水晶占い"
when "satan"
"魔族の戦士"
else
raise "role が不適切です"
end
puts "#{@name}は#{skill}を使った!"
end
余計な情報が詰まっているので、負担がかかるわけです。
対して、クラスごとに分割しておくと、とても見やすくなります。
トルネコ(商人)であれば、Merchant クラスを見てあげればいいだけなので
class Merchant
def execute_skill
puts "トルネコはお宝発見を使った!"
end
end
どこを直せばいいのか、すぐにわかりますよね。負担も少ない。
あとは以下のように変えてあげれば OK ですね。
class Merchant
def execute_skill
puts "トルネコはお宝発見を使った!しかし何も見つからなかった!"
end
end
こういった理由から、case でわけずに、適切にクラスにして分けてあげると読みやすく、変更に強いコードが書けるようになります。
ちなみに僕はテリーが一番好きですが、超幸せトルネコは決まると楽しいのでよく使ってます。
おわり。