今回は、「ポリモーフィズム(多態性)」について解説します。
ポリモーフィズムとは、あるインスタンスメソッドを呼び出したときに、そのインスタンスの型の種類に応じて実際に呼び出されるメソッドがプログラム実行時に切り替わる仕組みのことです。
例えば、動物型の変数animalがあるとしましょう。
animal.MakeSound()というメソッドを呼び出したとき、animalに代入されたインスタンスが、
- 動物クラスを継承した犬型インスタンスであれば、「ワン」と出力する
- 動物クラスを継承した猫型インスタンスであれば、「ニャア」と出力する
というふうに型に応じて処理を切り替えられます。
MakeSoundというメソッドを呼び出しただけで、animalへ代入された型に応じてプログラム実行時に処理を適切に選択できるわけですね。
これにより、条件分岐を使わずに型によって異なる振る舞いをさせることが可能となり、コードの可読性・変更容易性・再利用性が向上します。
本記事ではポリモーフィズムの考え方を「使わなかった場合」と「使った場合」で、どのように可読性・変更容易性・再利用性が変わるのかを、わかりやすく解説します。
ここまで勉強すれば、オブジェクト指向の基本はバッチリです!
YouTubeの動画でも解説しているので、ぜひ御覧ください。
演習1:型による条件分岐の問題点
演習1では、型による条件分岐を行っているコードを機能拡張し、問題点についてみていきましょう。
前回演習コードの復習
前回の演習では、店舗で扱っているドリンクと食事のメニュー情報を登録し、HTML形式のテーブルにして出力するプログラムを作成しました。
詳しくは、以下の記事の演習2、3を御覧ください。
このコードは以下のような構成になっています。
- メニューの基本情報を保持するMenuクラスを定義
- Menuを継承し、DrinkMenu・MainMenuクラスを定義
- Menu型配列からメニュー表(HTMLコード)を生成するMenuTableGeneratorクラスを定義
- 上述したクラスを用いてMenu配列を作成し、HTMLコードを出力する処理を実行
このコードを実行すると例えば以下のようなHTMLコードが生成できます。
「補足」の列では以下の情報を表示しています。
- DrinkMenuクラスのIsColdプロパティの値に基づき、冷たいかどうかの情報を表示する
- MainMenuクラスのIsVegetarianプロパティの値に基づき、ベジタリアン向けかどうかの情報を表示する
このような表示を行うため、MenuTableGeneratorのGenerateTableメソッドでは、以下のように処理を記述しています。
public string GenerateTable()
{
…
foreach (Menu menu in menus)
{
…
string note;
switch (menu)
{
case DrinkMenu drinkMenu when drinkMenu.IsCold:
note = "(冷)";
break;
case MainMenu mainMenu when mainMenu.IsVegetarian:
note = "(菜食)";
break;
default:
note = "";
break;
}
table += $"<td>{note}</td>";
…
}
…
}
デザートメニューの追加によるプログラムの拡張
ここで、メニュー一覧生成プログラムを次のように拡張することを考えてみましょう。
- メニューにデザートを新たに追加する
- デザートは甘さのレベルを独自の情報としてもつ(5段階)
- メニュー一覧で、デザートの補足情報として甘さのレベルを表示する
このような拡張を行うには、以下のようにコードを修正すればよさそうです。
- Menuを継承してデザートメニューを表すDessertMenuを定義する
- DessertMenuは甘さ度合いを表すSweetLevel(5段階)というプロパティをもつ
- MenuTableGeneratorのGenerateTableメソッドで、Dessert型の場合には、SweetLevelを補足情報として出力するようにする
以下のように新規クラス追加と既存クラスへのコード追加を行うイメージです。
まず、DesserMenuクラスのコードは次のようになります。
//デザートメニューの情報を格納するクラス(Menuから派生した子クラス)
class DessertMenu : Menu
{
private int sweetnessLevel;
public DessertMenu(string name, string description, int price, int sweetnessLevel)
: base(name, description, price)
{
this.sweetnessLevel = sweetnessLevel;
}
public int SweetnessLevel
{
get { return sweetnessLevel; }
}
}
DessertMenuをHTMLテーブルに表示するためには、MenuTableGeneratorクラスに以下のようなコードを追加します。
public string GenerateTable()
{
…
foreach (Menu menu in menus)
{
…
string note;
switch (menu)
{
case DrinkMenu drinkMenu when drinkMenu.IsCold:
note = "(冷)";
break;
case MainMenu mainMenu when mainMenu.IsVegetarian:
note = "(菜食)";
break;
//★↓このcaseを追加
case DessertMenu dessertMenu:
note = $"(甘さレベル{dessertMenu.SweetnessLevel})";
break;
default:
note = "";
break;
}
table += $"<td>{note}</td>";
…
}
…
}
これで、DessertMenu型の場合には補足情報に甘さのレベルを表示できます。
メニュー一覧へチョコレートケーキを追加してみます。
…
Menu[] menus = new Menu[] {
new MainMenu("黒毛和牛ステーキ", "ジューシーで柔らかなステーキです。", 3000, false),
new MainMenu("ベジタブルカレー", "野菜をたっぷりと使った、スパイシーなカレーです。", 2400, true),
new DrinkMenu("メロンソーダ", "爽やかな甘さが楽しめるメロンソーダです。", 400, true),
new DrinkMenu("ホットコーヒー", "丁寧に焙煎されたコーヒー豆を使用しています。", 500, false),
//★↓新たに追加
new DessertMenu("チョコレートケーキ", "しっとりとしたチョコレートケーキです。", 600, 4),
};
…
このプログラムを実行してHTMLコードを生成し、それをブラウザで表示すると以下のようになります。
演習1コードの問題点
デザートメニューを追加するためにクラスを以下のように追加・修正しました。
- 新規にMenuを継承したDessertMenuクラスを追加
- 既存のManuTableGeneratorクラスのGenerateTableメソッドを修正
新規のクラス追加だけではなく、既存のクラスにも修正が必要となっていますね。
型を判別して条件分岐を行う方法では、以下のような問題点があります。
- 可読性の問題:型や各条件による複雑な条件分岐であることに加え、補足情報を表示する処理が各クラス(MainMenu等)の定義とは別の離れた場所に記述されてしまっている。
- 変更容易性の問題点:新しいメニュー(新しいクラス)を追加するたびに、既存コード(MenuTableGenerator)も併せて修正する必要があり、変更に手間がかかり誤りも混入しやすい。
- 再利用性の問題点:新しいメニューを追加するたびに既存クラスの修正が必要ということは、既存クラスをブラックボックスな部品として再利用することができない。
これから学ぶポリモーフィズムも、コードの可読性・変更容易性・再利用性を向上させるためにある部品化の仕組みの1つなのです。
演習2:ポリモーフィズムを使ったコードの改善 ~抽象メソッド~
演習1のコードにおける問題点を解決するため、ポリモーフィズムの考え方を導入します。
ポリモーフィズムとは、同じ名前のメソッドが異なるクラスで異なる振る舞いをするという概念です。
つまり、同じメソッド名を使っていても、そのメソッドが呼ばれるインスタンスによって実行される内容が変わるということです。
ポリモーフィズムを実現するためには、親クラスで抽象メソッドと呼ばれるメソッドを宣言しておきます。
そして、各子クラスでそれぞれの具体的な振る舞いを具象メソッドとして実装します。
それでは、実際に演習1のコードを修正していきましょう。
ポイント1:抽象メソッドと具象メソッドの使い方
演習1コードのそれぞれのクラス定義において以下を追加します。
- Menuクラスで、補足情報の文字列を取得する抽象メソッドGetNoteを宣言((a)の部分)
- Menuの子クラスで、GetNoteの具体的な中身の実装を定義((b1)、(b2)、(b3)の部分)
追加したコードは以下のようになります。
//■===以下はクラス定義===■
//メニューの情報を格納するクラス(DrinkMenu、MainMenuの親クラス)
abstract class Menu
{
…
//★(a) 抽象メソッド
public abstract string GetNote();
}
//飲み物メニューの情報を格納するクラス(Menuから派生した子クラス)
class DrinkMenu : Menu
{
…
//★(b1) 具象メソッドを実装
public override string GetNote()
{
return IsCold ? "(冷)" : "";
}
}
//主菜メニューの情報を格納するクラス(Menuから派生した子クラス)
class MainMenu : Menu
{
…
//★(b2) 具象メソッドを実装
public override string GetNote()
{
return IsVegetarian ? "(菜食)" : "";
}
}
//デザートメニューの情報を格納するクラス(Menuから派生した子クラス)
class DessertMenu : Menu
{
…
//★(b3) 具象メソッドを実装
public override string GetNote()
{
return $"(甘さレベル{sweetnessLevel})";
}
}
(a)では「public abstract string GetNote();」と、メソッドの戻り値、メソッド名、引数のみ宣言されていて、メソッドの中身はありませんね。
抽象メソッドを定義するときには「abstract」を修飾子としてつけます。
各子クラスの(b1)、(b2)、(b3)でこのメソッドの具体的な振る舞いである具象メソッドを実装しています。
「public override string GetNote(){…}」と定義することで、具象メソッドを作成できます。
親クラスで宣言された抽象メソッドへ具体的な実装を与え、具象メソッドを定義するには「override」を修飾子としてつけます。
具象メソッドを定義することをオーバーライドすると言います。
Menuの抽象メソッドGetNoteをオーバーライドして、DessertMenuの具象メソッドGetNoteを定義しています。
ポイント2:抽象メソッドの呼び出し方法
抽象メソッドを使うと、GenerateTableは以下のように修正できます。
(c)がMenu型のGetNote抽象メソッドを呼び出している部分です。
public string GenerateTable()
{
string table = "<table border='1'>";
table += "<tr><th>メニュー名</th><th>価格(税込み)</th><th>説明</th><th>補足</th></tr>\n";
foreach (Menu menu in menus)
{
table += "<tr>";
table += $"<td>{menu.Name}</td>";
table += $"<td>{menu.GetPriceWithTax()}円</td>";
table += $"<td>{menu.Description}</td>";
table += $"<td>{menu.GetNote()}</td>"; //★(c) 抽象メソッドを呼び出す
table += "</tr>\n";
//※ +演算子で多数の文字列を連結させていくのは効率が悪いため、
// 本当はStringBuilderクラスを使った方が良いです。
}
table += "</table>";
return table;
}
型により条件分岐を行うコードに比べて、だいぶ簡潔に書けていますね。
(c)の部分では、Menu型変数menuに対して、抽象メソッドGetNoteを呼び出しています。
menu変数にどの型のインスタンスが代入されているかで、実際にどのGetNoteメソッドが呼び出されるかはプログラム実行時に決定します。
例えば、menu変数にDessert型のインスタンスが代入されていれば、呼び出されるGetNoteメソッドはDessertクラスで定義された以下のものになります。
public override string GetNote()
{
return $"(甘さレベル{sweetnessLevel})";
}
条件分岐は存在しないのですが、型の種類によって呼び出されるGetNoteメソッドが切り替わっているのですね。
メニューというのは補足情報を取得できる。
具体的には、ドリンクメニューならば冷たいかどうかの補足情報を取得できる。
主菜メニューならばベジタリアン向けかの情報を取得できる。
…といった感じの意味合いが抽象メソッド・具象メソッドの関係です。
問題点がどのように解決されたか
ポリモーフィズムを導入した演習2コードで、型による条件分岐を行っていた演習1コードの問題点がどのように解決したかを確認していきましょう。
可読性
GeenrateTableメソッドにおいて、複雑な型による条件分岐がなくなり、コードが簡潔になりました。
補足情報を表示する処理が各クラス(MainMenu等)の定義にGetNoteメソッドとして記述されるようになり、各クラスにおける事柄が一箇所にまとまっています。
変更容易性
新しいメニュー(新しいクラス)を追加する場合でも、既存コード部分の修正は不要です。
MenuTableGeneratorクラスのGenerateTableメソッドを修正する必要はありません。
新しいメニューにGetNoteメソッドを定義しておけば、GetNote抽象メソッドの呼び出しがプログラム実行時に適切に切り替わります。
DessertMenuインスタンスであれば、DessertMenuのGetNoteメソッドが呼ばれるのです。
なので、コードの変更が容易になりました。
再利用性
既存クラスMenuTableGeneratorは、新しいメニューを追加したときに修正不要となりました。
そのため、既存クラスはブラックボックスな部品として扱うことができます。
そのため、既存クラスを部品として取り回しがしやすくなり、再利用性が上がっています。
ポリモーフィズムの考え方を使えば、ライブラリ利用者が新規メニューを作るときには、Menuクラスを派生させてクラスを定義すれば良いだけですね。
なので、このライブラリは再利用性が高く、使い勝手が良いと言えるでしょう。
ポリモーフィズムについてはこちらの記事も参考にしてください。
抽象メソッドと、子クラスにおけるメソッドの具体的な実装については、abstract修飾子・override修飾子の記事もそれぞれ参考にしてください。
講義1:仮想メソッドとは?
親クラスで宣言した抽象メソッドは、子クラスで具象メソッドとして実装が必要です。
具象メソッドを実装しないとコンパイルエラーとなります。
具象メソッドが定義されていなければ、子クラスのインスタンスにおいてそのメソッドの呼び出しを行うことができないため、これは当然ですね。
抽象クラスはインスタンス化できないため、そのメソッドが呼び出されることはないためです。
子クラスで具象メソッドを実装しなくても、あらかじめ親クラスでデフォルトの振る舞いを定義しておきたいということもあるかもしれませんね。
例えば演習2のMenuクラスにおいて、GetNoteメソッドにデフォルトの挙動として、”補足情報なし”という文字列を返させたいとします。
このようなデフォルトの振る舞いは、仮想メソッドを使うと以下のように実現できます。
abstract class Menu
{
…
//仮想メソッド
public virtual string GetNote()
{
return "補足情報なし";
}
}
仮想メソッドは「virtual」をメソッドの修飾子としてつけます。
Menuの子クラスでGetNodeをオーバーライドしない場合、この仮想メソッドGetNoteが呼びされます。
オーバーライドするというのは、親クラスの仮想メソッドや抽象メソッドを上書きする(新しい定義に置き換える)という意味となります。
仮想メソッドについては、こちらの記事も参考にしてください。
メソッドだけでなく、プロパティやインデクサといったインスタンスメンバについてもabstract・virtualの修飾子をつけることで、ポリモーフィズムを適用できます。
おまけ:Visual Studioによる抽象メソッド・具象メソッドの確認方法
演習2で説明したように、ポリモーフィズムをうまく使うとコードをより良くできます。
しかし、ポリモーフィズムを使って書かれたコードは、プログラム実行時に呼び出されるメソッドが決定するため、一見するとプログラムの振る舞いがわかりずらいと思うこともあるでしょう。
Visual Studioの支援機能を使うとこのような振る舞いを理解しやすくなります。
ここでは、すぐに使える2つの機能を紹介します。
コードエディタで抽象メソッド・具象メソッドの関係を確認
Visual Studio 2022の最新版では、コードエディタ上で、抽象メソッド(もしくは仮想メソッド)のオーバーライドしている具象メソッド一覧を表示して確認できます。
演習2コードのMenuクラスの抽象メソッドGetNoteをVisual Studioコードエディタで見ると、以下のように「◯↓」のアイコンが左側に表示されています。
このアイコンをクリックすると、以下のようにGetNoteメソッドをオーバーライドしたメソッド一覧が表示されます。
ここでメソッドを選ぶと、そのメソッド定義へジャンプすることも可能です。
コードエディタ上で、具象メソッド定義の右側にも「◯↑」のアイコンがあり、親クラスの抽象メソッド(もしくは仮想メソッド)へジャンプできます。
これらの機能を使うと、抽象メソッド・仮想メソッドとオーバーライドした具象メソッドの間を容易に行き来することができ、とても便利です。
そして、親クラス、子クラスを相互に行き来できます。
デバッガで実際に呼びだされるメソッドを確認
ポリモーフィズムが用いられているコードを理解したい場合、ひとまずコードを動かしてみて、どのメソッドが呼ばれるかを確認するという方法もあります。
以下のように、メソッド呼び出しが行われる直前にブレークポイントを設置します。
プログラムが停止したらステップ実行を行えば、どのメソッドが呼びだされるかを確認できます。
このような方法を使うと、ポリモーフィズムを用いたコードの理解を効率よく行える可能性があります。
まとめ
今回は、ポリモーフィズムについて説明しました。
ポリモーフィズムを使うと、型による条件分岐を行わずに、型ごとに特化した処理を記述できます。
抽象メソッド・仮想メソッド・オーバーライドを使って、ポリモーフィズムを具体的に実現する方法について学びました。
ポリモーフィズムを使うことでコードの可読性・変更容易性・再利用性が向上します。
カプセル化、継承、ポリモーフィズムという3本柱について一通り説明しました。
今回で、オブジェクト指向についての基本的な話はおしまいです。
次回以降は、名前空間・外部ライブラリ・コレクションなど、アプリケーションを作っていくために必要な事柄を学んでいきましょう。