内容を増強したC#で「ポリモーフィズム」実現する手段と利点を書きました。内容が若干濃くなったのでよければこちらもご覧ください。
『オブジェクト指向でなぜつくるのか 第3版』を読んだ際、ポリモーフィズムについての理解が全く足りてないことを再確認しました。
(ぶっちゃけオブジェクト指向なんもわからん)
そこで実際にコーディングしてポリモーフィズムの恩恵を感じつつ、学んだことをメモ書き程度にまとめてみようと思った次第です。
なぜこの記事を書こうと思ったのか
・メモとして見返す為
・アウトプットすることで知識を定着させるため
・間違いがあった場合に指摘してもらうため
今回の題材
様々な職業の人が自己紹介するだけのアプリケーションを題材にします。
具体的には「私は[名前]。[職業名]です。」という文字列を画面に出力するコンソールアプリケーションを作ります。
そもそもポリモーフィズムとは何か
ポリモーフィズムはひと言で表現すると,共通メインルーチンを作るための仕組みと言えます。
平澤 章 著『オブジェクト指向でなぜつくるのか 第3版』(日経BP,2021年4月19日) p95 l9~l10 より引用
実際にコーディングしてみると「なるほど!!」となります。
例としてお米好きの人が「[品種名]美味しい!!」と叫ぶプログラムを作ります。
まずお米全体をクラスにします。
class Rice
{
//品種名を返却する仮想メソッド
public virtual string GetBreed()
{
return "いろいろあるよ";
}
}
次に品種ごとにRiceクラスを継承したクラスを作っていきます。
class Akitakomachi : Rice
{
//RiceのGetBreedメソッドをオーバーライドする
public override string GetBreed()
{
return "あきたこまち";
}
}
Akitakomachiクラスと同じような構成のクラスをコシヒカリバージョンでも作ります。
RiceLoversクラスを作ります。EatRiceメソッドの引数にはRiceを指定します。
class RiceLovers
{
public void EatRice(Rice rice)
{
System.Console.WriteLine($"{rice.GetBreed()}美味しい!!");
}
}
実際にRiceLoversクラスのEatRiceメソッドを利用するとこのようになります。
class Program
{
static void Main(string[] args)
{
var person = new RiceLovers();
person.EatRice(new Akitakomachi());
person.EatRice(new Koshihikari());
}
}
/*出力結果*/
/*あきたこまち美味しい!!*/
/*コシヒカリ美味しい!!*/
EatRiceメソッドの引数にぶち込むお米は「あきたこまち」だろうと「コシヒカリ」だろうと美味しくいただけます。
仮に新しく「ななつぼし」クラスを作ってインスタンスを引数にぶち込んでも「ななつぼし美味しい!!」と出力されます。
ポリモーフィズムの性質をうまく使えば呼び出される側(今回は米の品種ごとにあるGetBreedメソッド)が増えても呼び出す側(今回はEatRiceメソッド)を修正する必要がなくなるという恩恵を享受できます。
ポリモーフィズムを活用しないとEatRiceメソッドをお米の品種が増えるごとに作らないといけなくなってしまいます。
拡張性が死んでしまいます。
やっと本題。ポリモーフィズムのありがたみをじっくり感じていくうううう!!!
それでは「私は[名前]。[職業名]です。」という文字列を画面に出力するコンソールアプリケーションを作ります。
まずは継承、ポリモーフィズムを使わずに作る
「医者」、「システムエンジニア」、「ミュージシャン」の三つの職業のクラスを作ります。
class Doctor
{
public string Name { get; }
public Doctor(string name)
{
Name = name;
}
public string GetOneselfString()
{
return $"私は{Name}。医者です。";
}
}
人の名前が入るNameプロパティを持ち、自己紹介文を返すGetOneselfStringメソッドを実装しただけのクラスです。同様にして残りの二つも実装します。
次に画面に自己紹介文を出力する部分を実装していきます。
class Program
{
static void Main(string[] args)
{
var taro = new Doctor("太郎");
var hanako = new SystemEngineer("花子");
var gonzales = new Musician("ゴンザレス");
System.Console.WriteLine(taro.GetOneselfString());
System.Console.WriteLine(hanako.GetOneselfString());
System.Console.WriteLine(gonzales.GetOneselfString());
}
}
これでも一応動きますが、このコードだと職業の種類が増えるごとに「中身が若干違うけど処理の内容は同じプログラム」が量産されていくことになります。これではメンテナンスも拡張も大変です。
「継承」を使って人をまとめる
私は思いました。
「こいつら全員人間じゃん!!!(当たり前)」
というわけで新しくPersonクラスを作って各職業のクラスの重複したコードを消し飛ばしていきます。
どの職業の人も名前を持っているのでNameプロパティをPersonクラスに実装して、各職業のクラスに継承させます。
class Person
{
public string Name { get; }
public Person(string name)
{
Name = name;
}
}
各職業のクラスは次のように書き換えます。
class Musician:Person
{
public Musician(string name) : base(name) { }
public string GetOneselfString()
{
return $"私は{Name}。ミュージシャンです。";
}
}
継承したことによって各職業の人をPersonという大きな集合でまとめることができるようになりました。
class Program
{
static void Main(string[] args)
{
var people = new System.Collections.Generic.List<Person>()
{
new Doctor("太郎"),
new SystemEngineer("花子"),
new Musician("ゴンザレス")
};
foreach(var person in people)
{
//PersonクラスにGetOneselfString()は存在しないので動作しない
System.Console.WriteLine(person.GetOneselfString());
}
}
}
「Listに出来たのであとは反復処理で解決だ!!」
なんてことはありません。当たり前です。PersonクラスにGetOneselfStringメソッドなんぞ実装してませんから。
かといって各職業ごとに処理の内容が異なるGetOneselfStringメソッドをPersonクラスにそのまま移動することはできません。
そこでポリモーフィズムの要素が救いの手を差し伸べてきます。
メソッドをオーバーライドしてポリモーフィズムを感じていくうううう(謎)
考え方は単純で、Personクラスに実装したメソッドを各職業クラスで別の処理に上書きすればよいのです。
そうすればPersonクラスにGetOneselfStringメソッドを実装しつつ各職業クラスではそれぞれ異なった振る舞いをするようにできます。
PersonクラスにGetOneselfString仮想メソッドを実装します。
class Person
{
public string Name { get; }
public Person(string name)
{
Name = name;
}
public virtual string GetOneselfString()
{
return $"私は{Name}。無職です。";
}
}
次に各職業クラスのGetOneselfStringメソッドを書き換えます。
class Doctor:Person
{
public Doctor(string name) : base(name) { }
public override string GetOneselfString()
{
return $"私は{Name}。医者です。";
}
}
override修飾子を追加しただけです。
ついに各職業の人をListでまとめ、反復処理で簡潔に処理を記述できるようになりました。
拡張
なんかいろいろあって新しく職業を追加しないといけなくなっても・・・
class Program
{
static void Main(string[] args)
{
var people = new System.Collections.Generic.List<Person>()
{
new Doctor("太郎"),
new SystemEngineer("花子"),
new Musician("ゴンザレス"),
new ProfessionalSoldier("バンガロール"),
new Scientist("ガスおじ")
};
foreach(var person in people)
{
System.Console.WriteLine(person.GetOneselfString());
}
}
}
上のようにメインの処理であるforeach文スコープ内を修正せず済みました。
もし継承、ポリモーフィズムを活用しなかったら
class Program
{
static void Main(string[] args)
{
var taro = new Doctor("太郎");
var hanako = new SystemEngineer("花子");
var gonzales = new Musician("ゴンザレス");
var banga = new ProfessionalSoldier("バンガロール");
var gasOji = new Scientist("ガスおじ");
System.Console.WriteLine(taro.GetOneselfString());
System.Console.WriteLine(hanako.GetOneselfString());
System.Console.WriteLine(gonzales.GetOneselfString());
System.Console.WriteLine(banga.GetOneselfString());
System.Console.WriteLine(gasOji.GetOneselfString());
}
}
という凄惨なものが爆誕していたでしょう・・・
まとめ
実際使うとめっちゃ便利
感想
ポリモーフィズムを使う流れを意識して学習したかったので内容の割には長ったらしい記事になりました。
今回は簡単な例でしたが、もっと実践的な使い方を知る必要があると感じたので他人の書いたコードを読んで分析する必要があると感じました。ポリモーフィズムの実感がわかなかったのは当たり前のように感じていたからかもしれないからだと考えました。(オブジェクト指向というよりはC#という言語の話というとらえ方をしていたかも?)設計の話になるとクラスの依存関係など気にしないといけないことが増えるので今のうちに基礎を固めないといけないと感じました。