はじめに
この記事は、オブジェクト指向についての勉強を始めたばかりの初学者を対象に、オブジェクト指向の三大要素の1つ「ポリモーフィズム」について、なんとなく理解した気になることを目標とした記事です。説明の都合上、三大要素のもう1つ「継承」についても説明します。
※ オブジェクト指向の三大要素:カプセル化(クラス)、継承、ポリモーフィズムのこと
結論 : ポリモーフィズムとは
ポリモーフィズムとは「インターフェースに "一貫性" を持たせよう!」と言うアイディアです。
結論としては上記の通りなのですが、初学者がポリモーフィズムについて理解するにはいくつかの要素が不足しています。
その要素とは「 継承 」「 オーバーライド 」「 オーバーロード 」の3つです。
なので以降ではそれらについて、特に ポリモーフィズムとの関係性 に焦点を当てて説明します。
用語の定義
まずは用語の定義を押さえないと始まりません。が、その定義が難解であることが理解の妨げになっていると思うので、ここでは筆者独自の説明を書きます。最終的には書籍などできちんと確認することを推奨します。
-
ポリモーフィズム(多態性)
ポリモーフィズムとは、異なるクラス(型)から生成された複数のインスタンス(オブジェクト)が、同じインターフェース(メソッドやプロパティの呼び出し方)を備えている、つまり「インターフェースに "一貫性" がある」ことです。 -
継承
継承とは、あるクラス(親クラス)のプロパティやメソッドを別のクラス(子クラス)が引き継ぐことです。子クラスは、親クラスの機能(実装)を継承し、必要に応じてそれをオーバーライド(上書き)することができます。これにより ポリモーフィズムを実現します。 -
オーバーライド
オーバーライドとは、子クラスにおいて親クラスから継承したメソッドの内部を上書きすることです。このオーバーライドされたメソッドの内部は、上書きにより継承元のメソッドとは違う処理結果が返るにもかかわらず、そのメソッド名は元のまま変更することなく実装できます。これにより ポリモーフィズムを実現することができます。 -
オーバーロード
オーバーロードとは、同じ名前のメソッドが、異なる引数の "型" や "数" に対応できるようにする手法です。これにより、異なる型(クラス)のオブジェクト(インスタンス)に対して同じインターフェースを提供し、ポリモーフィズムを実現することができます。オーバーロードは主に静的型付け言語1 (TypeScriptなど)で用いられる手法であり、動的型付け言語2 (Rubyなど)では基本的に存在しません。
これらの用語の関係をまとめると、
「 継承 や オーバーライド、 オーバーロード は、ポリモーフィズム (インターフェースの一貫性) を実現するための手段である 」
と言えます。
※ より正確な説明はこちら(IT用語辞典 e-Words) :ポリモーフィズム 、継承 、オーバーライド 、オーバーロード
どのようにしてポリモーフィズムを実現するのか
先ほど説明しましたが、ポリモーフィズムとは「インターフェースに "一貫性" を持たせよう!」と言うアイディアです。
そしてそのアイディアを実現するための手段として「継承」「オーバーライド」「オーバーロード」があります。
ここでは、それぞれについて簡単な例とともに、どのようにしてポリモーフィズムを実現しているのかを説明します。
1. 継承
「継承」を使うことで、親クラスのプロパティやメソッドを子クラスが引き継ぐことができ、その子クラスから生成されたインスタンスは親クラスと同一のメソッド名を用いてメソッドを呼び出すことができます。また、同じ親クラスを継承した別の子クラスにおいても、同一のメソッド名でメソッドを呼び出すことができます。
【具体例】(動物を意味するAnimalクラスと、それを継承するDogクラスとCatクラス)
# 動物クラス
class Animal
attr_accessor :name
def initialize(name)
@name = name
end
def speak
"I am an animal."
end
end
# 犬クラス(継承)
class Dog < Animal
end
# 猫クラス(継承)
class Cat < Animal
end
dog = Dog.new("わんこ君")
cat = Cat.new("ねこちゃん")
# メソッドにアクセスするインターフェース(.speakメソッド)が同じ
puts dog.speak # => "I am an animal."
puts cat.speak # => "I am an animal."
# プロパティにアクセスするインターフェース(.nameメソッド)が同じ
puts dog.name # => "わんこ君"
puts cat.name # => "ねこちゃん"
この特性により、クラスの異なるインスタンスであっても、継承を用いることで同じインターフェースを用いることができ、結果として ポリモーフィズムを実現しています。
2. オーバーライド
親クラスから継承したメソッドに独自のアレンジを加えたい時、オーバーライドを用いることで、親クラスと同じメソッド名および引数の型・個数(これをメソッドシグネチャ3といいます)をそのまま子クラスでも使うことができます。
【具体例】(図形を意味するShapeクラスと、それを継承するCircleクラスとSquareクラス)
# 図形クラス
class Shape
def area
# 子クラスでオーバーライドしなかった場合に、わざとエラーが出るようにしている
raise NotImplementedError, 'このメソッドはサブクラスでオーバーライドしてください。'
end
end
# 円クラス
class Circle < Shape
attr_accessor :radius
def initialize(radius)
@radius = radius
end
# オーバーライド
def area
Math::PI * @radius * @radius
end
end
# 正方形クラス
class Square < Shape
attr_accessor :side_length
def initialize(side_length)
@side_length = side_length
end
# オーバーライド
def area
@side_length * @side_length
end
end
# 出力結果はそれぞれ独自のものであるにも関わらず、メソッドの呼び出し方(インターフェース)は共通している
circle = Circle.new(5)
puts "Circle area: #{circle.area}"
square = Square.new(4)
puts "Square area: #{square.area}"
上記のコードでは、Shapeクラスはareaメソッドを持ちますが、具体的な実装は提供せず、NotImplementedErrorを発生させてサブクラスでのオーバーライドを促しています。CircleクラスとSquareクラスは、それぞれShapeクラスを継承し、areaメソッドをオーバーライドして独自の面積計算を実装しています。この設計により、親クラスのインターフェースを子クラスでもそのまま使うことができるため、ポリモーフィズムを実現している事になります。
3. オーバーロード
オーバーロードは、同じクラス内で同じ名前のメソッドを、異なる引数の "型" や "数" で定義することにより、同じメソッド名を使用していながら異なる引数の組み合わせに対応する手法のことでした。
以下の例では、静的型付け言語であるTypeScriptを用いて説明しています。
【具体例】(add関数が異なる引数の型と数に対応しています)
// シグネチャの定義(引数と戻り値の型を定義している)
function add(a: number, b: number): number;
function add(a: string, b: string): string;
// 実装部分(引数が数値の場合は数値を、文字列の場合は文字列を返す。その他の型の場合はエラー。)
function add(a: number | string, b: number | string): number | string {
if (typeof a === 'number' && typeof b === 'number') {
return a + b;
} else if (typeof a === 'string' && typeof b === 'string') {
return a.concat(b);
} else {
throw new Error("Invalid argument types");
}
}
const result1 = add(1, 2); // => 3 (number)
const result2 = add("Hello", " World"); // => "Hello World" (string)
上記コードの解説(TypeScriptを知らない方向け)
TypeScriptは、JavaScriptを拡張した言語です。 もともとJavaScriptは動的型付け言語だったのですが、それを静的型付け言語(変数の型を宣言できる)に拡張したものがType(型)Scriptです。 ちなみに、『データ型とは、プログラミング言語などが扱うデータをいくつかの種類に分類し、それぞれについて名称や特性、範囲、扱い方、表記法、メモリ上での記録方式などの規約を定めたもの』です。整数型(Integer)や文字列型(String)などがあります。 以下は上記コードの解説となります。雰囲気だけ掴めれば大丈夫だと思います。 ※ IT用語辞典 e-Words「データ型」より一部引用 // シグネチャの定義(引数と戻り値の型を、以下のように定義している)
// function 関数名(引数: 型の指定, 引数: 型の指定): 戻り値の型指定
function add(a: number, b: number): number; // 数値の場合
function add(a: string, b: string): string; // 文字列の場合
// 実装部分(引数が数値の場合は数値を、文字列の場合は文字列を返す。その他の型の場合はエラー。)
// add関数の引数a,bは、それぞれ数値または文字列を受け取れる。また戻り値も同様。
// そして、a,b共に数値ならば足し算をした結果の数値を戻り値に、a,b共に文字列ならば連結した結果を戻り値に、それ以外の型が引数だった場合はエラーを返す。
function add(a: number | string, b: number | string): number | string {
if (typeof a === 'number' && typeof b === 'number') {
return a + b;
} else if (typeof a === 'string' && typeof b === 'string') {
return a.concat(b);
} else {
throw new Error("Invalid argument types");
}
}
const result1 = add(1, 2); // => 3 (number)
const result2 = add("Hello", " World"); // => "Hello World" (string)
上記の例では、add関数がオーバーロードされており、2つの引数が数値の場合は数値として加算され、文字列の場合は文字列として連結されます。このように、オーバーロードは異なる引数の型や数に対応する同じ名前の関数を定義することで、ポリモーフィズムを実現しています 。
まとめ
ポリモーフィズムとは「 インターフェースに "一貫性" を持たせよう! 」と言うアイディアです。
そして、その ポリモーフィズムを実現するための手段として、「継承」や「オーバーライド」、「オーバーロード」があるのです。
【補足】 Rubyにおけるオーバーロードについて
今回オーバーロードについては、Rubyではなく、TypeScriptを用いて説明しました。
というのも、Rubyは動的型付け言語という特性上、オーバーロードという考えは基本的にないからです。
どういうことかと言うと『Rubyの場合は1つのメソッドでいろんなデータ型や個数の引数を受け取ることができるため、同じ名前で複数のメソッド定義を持つ必要がありません。ゆえに、オーバーロードという考え方も必要ない』のです。
"引数の型" に関しては、例えば引数に整数がくることを前提としているメソッドに文字列がくる可能性もある場合、その引数に対してメソッド内で .to_iメソッドを用いて整数に変換することで対応できます。
また "引数の個数" に関しては、『引数のデフォルト値や可変長引数を使うことで、メソッド呼び出し時の引数の個数を柔軟に変えることができます。』
詳細は『プロを目指す人のためのRuby入門』のコラム「Rubyでメソッドのオーバーロード?」をお読みください。
(『プロを目指す人のためのRuby入門』伊藤淳一著より一部引用 明確に引用している箇所は『』で示しています)
【補足】 TypeScriptにおけるオーバーロードについて
TypeScriptにおいてオーバーロードは一般的な機能の1つですが、オーバーロードを使わずに、よりシンプルな方法で同じ目的を達成できる場合もあります。例えば、"ジェネリクス" や "union型" を使って、複数の型を同時にサポートする関数を定義できます。
ジェネリクスやunion型に関する補足記事を書きました:【補足】TypeScriptのunion型とジェネリクスについて
【この記事は主に以下の情報を参考にして執筆しました】
『オブジェクト指向でなぜ作るのか』、『プロを目指す人のためのRuby入門』、IT用語辞典 e-Words
-
『コンパイル時に変数の型が定まる言語。型にまつわる問題はプログラムを実行しなくても発見できる』(「サバイバルTypeScript」より引用) ↩
-
『実行時に変数の型が定まる言語。型にまつわる問題はプログラムを実行してみないと発覚しない』(「サバイバルTypeScript」より引用) ↩
-
メソッドシグネチャとは、メソッドの名前、引数の型、および戻り値の型をまとめたものを指します。シグネチャは、関数やメソッドがどのように呼び出され、どのような引数を取り、どのような戻り値を返すかを定義します。 ※ より正確な説明はこちら(IT用語辞典 e-Words) : シグネチャ ↩