概要
皆さんは「多態性」とは何かを説明できますか?
約2年前にエンジニアデビューを果たした私は、当時このように感じていました。
「多態性の説明はなんとなくわかるんだけど、なんでこんな複雑な書き方するんだろう...」
多態性を利用したコードを理解することはできるのですが、いまいち何が嬉しいのかわからずに学習を進めていたように思います。
本記事は、初学者に向けて多態性とは何か?から始め、多態性を利用するうまみやメリットを中心に解説していきます。初学者の方にとって少しでも学習の助けやモチベーションになれば幸いです。
なお、本記事内に登場するサンプルコードは全てRubyで書かれています。
多態性(ポリモーフィズム)とは?
多態性とは何か?と手っ取り早く言い表すならば、
「同一名称のメソッドがクラスによって異なるふるまいを持つ」
ということです。よくわからないので早速次の章から例を見ていきたいとおもいます。
例を挙げてみる
以下のように、Dogクラス・Catクラス・Birdクラスを用意します。
各クラスは、Animalクラスのサブクラスであり、greet(あいさつする)というメソッドのみを持っています。
class Animal
def greet
raise NotImplementedError
end
end
class Dog < Animal
def greet
puts 'ワン!'
end
end
class Cat < Animal
def greet
puts 'にゃー'
end
end
class Bird < Animal
def greet
puts 'ちゅん!'
end
end
クラス図にすると以下のようになります。
この時、以下の3つのgreet
メソッドは何を出力するでしょうか。
animal = Dog.new
animal.greet
animal = Cat.new
animal.greet
animal = Bird.new
animal.greet
わかりましたか?
出力はそれぞれ以下のようになります。
# => 'ワン!'
# => 'にゃー'
# => 'ちゅん!'
注目ポイントは、どのコードもgreet
という同名のメソッドを使用している点です。
にも関わらず、3つのコードで出力が違うのはなぜでしょうか。
それは、animal
というレシーバのクラスがDog
、Cat
、Bird
と種類が違うためです。
このように、同一名称のメソッドであるにもかかわらず、クラスの違いによって振る舞いに差異が現れることを多態性(ポリモーフィズム)と言います。
多態性の何が嬉しいのか
多態性が何なのかはなんとなくわかっていただけたかと思います。
では、本題の「多態性を利用して何が嬉しいのか」について書いていきたいと思います。
先ほどの動物の例を忘れ、以下のような3パターンの出力をランダムに行うプログラムを考えてみます。
【今日のお天気は晴れです!】
お洋服情報: あついかも!半袖で出かけちゃう?
持ち物情報: 荷物は少ない方がいいね!タオルをわすれずに!
おでかけ情報: いい天気!歩いていこっ
【今日のお天気は雨です!】
お洋服情報: 長靴はいちゃえ!
持ち物情報: 傘持って行こうね!
おでかけ情報: 濡れるとやだな、電車にのろう!
【今日のお天気は雪です!】
お洋服情報: コートとマフラー忘れずに!
持ち物情報: さむいよ!カイロ持っていこうね!
おでかけ情報: 遅れちゃうかも!タクシー使おう!
お天気(晴れ・雨・雪)をランダムに選択し、その天気に応じたお洋服情報・持ち物情報・おでかけ情報を表示するプログラムです。
あなたならこのプログラムをどう実装しますか?
多態性を使用しない例
以下の作戦で実装してみようと思います。
- お洋服クラス・持ち物クラス・おでかけクラスを作る
- 各クラスの中で入力値による分岐を書く
それでは、いざ実装。
require './cloth'
require './baggage'
require './transport'
class Main
def self.exec(weather)
cloth = Cloth.new(weather)
baggage = Baggage.new(weather)
transport = Transport.new(weather)
puts "【今日のお天気は#{weather}です!】"
puts "お洋服情報: #{cloth.info}"
puts "持ち物情報: #{baggage.info}"
puts "おでかけ情報: #{transport.info}"
end
end
# 実行
weather_list = ['晴れ', '雨', '雪']
weather_list.shuffle!
Main.exec(weather_list[0])
class Cloth
def initialize(weather)
@weather = weather
end
def info
case @weather
when '晴れ'
'あついかも!半袖で出かけちゃう?'
when '雨'
'長靴はいちゃえ!'
when '雪'
'コートとマフラー忘れずに!'
end
end
end
class Baggage
def initialize(weather)
@weather = weather
end
def info
case @weather
when '晴れ'
'荷物は少ない方がいいね!タオルをわすれずに!'
when '雨'
'傘持って行こうね!'
when '雪'
'さむいよ!カイロ持っていこうね!'
end
end
end
class Transport
def initialize(weather)
@weather = weather
end
def info
case @weather
when '晴れ'
'いい天気!歩いていこっ'
when '雨'
'濡れるとやだな、電車にのろう!'
when '雪'
'遅れちゃうかも!タクシー使おう!'
end
end
end
無事に実装できました!やったね!
突然の仕様変更
?「実装完了したところ悪いんだけど、くもりのケースを考慮するのを忘れていたよ。」
?「くもりの時の出力は以下みたいな感じ!修正よろしく!」
【今日のお天気はくもりです!】
お洋服情報: 少し肌寒いかも!羽織ものを持っていこう!
持ち物情報: 折り畳み傘があるとあんしん!
おでかけ情報: 車で出かけるのがおすすめ!
やれやれ...
というわけで、くもりケースを追加していきたいと思います。
まず、Mainクラスからですが、Mainクラス自体を触る必要はありません。
ただし、くもりの条件を増やすため、実行部は以下のように変更する必要があります。
# weather_list = ['晴れ', '雨', '雪'] 修正前のコード
weather_list = ['晴れ', '雨', '雪', 'くもり'] # 修正後のコード
そして、Cloth
クラス、Baggage
クラス、Transport
クラスにそれぞれ以下のような処理を追加します。
class Cloth
def initialize(weather)
@weather = weather
end
def info
case @weather
when '晴れ'
'あついかも!半袖で出かけちゃう?'
when '雨'
'長靴はいちゃえ!'
when '雪'
'コートとマフラー忘れずに!'
when 'くもり' # ここを追加
'少し肌寒いかも!羽織ものを持っていこう!' # ここを追加
end
end
end
class Baggage
def initialize(weather)
@weather = weather
end
def info
case @weather
when '晴れ'
'荷物は少ない方がいいね!タオルをわすれずに!'
when '雨'
'傘持って行こうね!'
when '雪'
'さむいよ!カイロ持っていこうね!'
when 'くもり' # ここを追加
'折り畳み傘があるとあんしん!' # ここを追加
end
end
end
class Transport
def initialize(weather)
@weather = weather
end
def info
case @weather
when '晴れ'
'いい天気!歩いていこっ'
when '雨'
'濡れるとやだな、電車にのろう!'
when '雪'
'遅れちゃうかも!タクシー使おう!'
when 'くもり' # ここを追加
'車で出かけるのがおすすめ!' # ここを追加
end
end
end
修正完了しました!
この修正のどこが嫌なのか
今回は簡単なサンプルコードなので大した修正量ではなかったですが、この修正には以下のような嫌ポイントが潜んでいました。
【修正の必要なクラスが多い】
上記の通り、今回はCloth
, Baggage
, Transport
の3クラスを修正する必要がありました(実行部を除く)。実務で使用するコードではもっとたくさんのクラスがあるt想定できます。その大量のクラスたちの中からこの3クラスを見つけ出さなくてはなりません。さらに、たくさんのクラスに触れれば触れるほど、変更によるバグ発生の危険性が高まってしまいます。
【似たような分岐処理が3クラスに散らばっている】
上記の例では、3クラスにcase文が書かれています。どうみても同じ分岐処理なのに、違う箇所に何度も書かれているのは読みづらいですよね。さらに、この書き方には修正漏れのリスクがあります。今回使用したのは3クラスだけなので、修正漏れは発生しづらいですが、似たような分岐処理が数十クラスに渡っている場合、全てのクラスを修正して回るのは大変ですし、修正漏れのリスクを高めてしまいます。
多態性を利用する例
上記のような嫌ポイントを解決するため、作戦を変更してみたいと思います。
- 晴れ・雨・雪の3クラスを作成する
- 各クラスは自身の天気に対して返却するお洋服・持ち物・おでかけの情報を持つ
実際に書いてみましょう。
require './sunny'
require './rainy'
require './snowy'
class Main
def self.exec(weather_input)
weather = case weather_input
when '晴れ'
Sunny.new
when '雨'
Rainy.new
when '雪'
Snowy.new
end
puts "【今日のお天気は#{weather.value}です!】"
puts "お洋服情報: #{weather.cloth}"
puts "持ち物情報: #{weather.baggage}"
puts "おでかけ情報: #{weather.transport}"
end
end
# 実行
weather_list = ['晴れ', '雨', '雪']
weather_list.shuffle!
Main.exec(weather_list[0])
require './weather'
class Sunny < Weather
attr_reader :value
def initialize
@value = '晴れ'
end
def cloth
'あついかも!半袖で出かけちゃう?'
end
def baggage
'荷物は少ない方がいいね!タオルをわすれずに!'
end
def transport
'いい天気!歩いていこっ'
end
end
require './weather'
class Rainy < Weather
attr_reader :value
def initialize
@value = '雨'
end
def cloth
'長靴はいちゃえ!'
end
def baggage
'傘持って行こうね!'
end
def transport
'濡れるとやだな、電車にのろう!'
end
end
require './weather'
class Snowy < Weather
attr_reader :value
def initialize
@value = '雪'
end
def cloth
'コートとマフラー忘れずに!'
end
def baggage
'さむいよ!カイロ持っていこうね!'
end
def transport
'遅れちゃうかも!タクシー使おう!'
end
end
class Weather
attr_reader :value
def initialize; end
def cloth
raise NotImplementedError
end
def baggage
raise NotImplementedError
end
def transport
raise NotImplementedError
end
end
同様に仕様変更してみる
先ほどの例と同じように、くもりの条件を足してみたいとおもいます。
require './sunny'
require './rainy'
require './snowy'
require './cloudy' # ここを追加
class Main
def self.exec(weather_input)
weather = case weather_input
when '晴れ'
Sunny.new
when '雨'
Rainy.new
when '雪'
Snowy.new
when 'くもり' # ここを追加
Cloudy.new # ここを追加
end
puts "【今日のお天気は#{weather.value}です!】"
puts "お洋服情報: #{weather.cloth}"
puts "持ち物情報: #{weather.baggage}"
puts "おでかけ情報: #{weather.transport}"
end
end
weather_list = ['晴れ', '雨', '雪', 'くもり'] # ここを追加
weather_list.shuffle!
Main.exec(weather_list[0])
まず、Mainクラスに分岐の条件が1つ増えました。
次にCloudy
クラスを作成してみます。
require './weather'
class Cloudy < Weather
attr_reader :value
def initialize
@value = 'くもり'
end
def cloth
'少し肌寒いかも!羽織ものを持っていこう!'
end
def baggage
'折り畳み傘があるとあんしん!'
end
def transport
'車で出かけるのがおすすめ!'
end
end
これで実装は完了です。
今回の変更では何が嬉しかったのか
さっきの変更と要件はまったく同じでしたが、今回の変更は何が良かったのでしょうか。
【既存クラスに修正をする必要がなかった】
今回の修正で注目すべきなのは、Sunny
・Rainy
・Snowy
という既存クラスに触れないで実装を完了できた点です。前述した通り、既存クラスを触らないといけないということは、それだけバグを生み出してしまう可能性を高めてしまうということです。
【Cloudyクラスの実装が明確】
今回新たにCloudy
クラスを実装しましたが、どのように実装すべきかは親クラスであるWeather
を見れば一目瞭然です。新しいクラスを作成する際にも迷わずに素早く実装を終えることができます。
なぜこのような恩恵を受けることができたのでしょうか。
それは、分岐処理をインスタンス生成箇所に集結させ、振る舞いの差異は多態性によって表現したためです。
このように、多態性を利用することで分岐処理を散らばらせるのを防ぎ、仕様変更に強いコードを書くことができます。
[おまけ]
今回の例では、Mainというクライアントコードにインスタンス生成のロジックが露呈してしまっています。本来ならば隠蔽したいところですが、多態性の話から外れてしまうのでまたの機会にしておきます。
まとめ
今回は初学者の方向けに多態性について解説してみました。
この記事がオブジェクト指向プログラミングに興味を持ってもらえるきっかけになれば幸いです。