※この記事は2018年01月15日に投稿された記事です。
オブジェクト指向プログラムのポリモーフィズムについて学んだのでまとめてみたいと思います。
以下を参考にしました。
- 基礎からしっかり学ぶC#の教科書C# 7対応 - P.163(ポリモーフィズム~クラスを操作するには)
- 実戦で役立つ C#プログラミングのイディオム/定石&パターン - P.410(実践オブジェクト指向プログラミング)
##ポリモーフィズムってなんだ?
ポリモーフィズムとはオブジェクト指向プログラミングの概念の一つです。
日本語では多態性・多様性などと訳されます。
簡単に言うと同じ名前のメソッドを複数のクラスで使用できるようにし、そのメソッドを通して、暗黙的に複数のインスタンスの動作を切り替えることができるようにします。
例えば、JavaScriptでは以下のようなコードを書くことができます。
3つキャラクタークラスがあるとします。
キャラクター達はそれぞれ**attack()
**メソッドを持っています。
class Soldier {
attack() {
return '戦士は斬りかかった!';
}
}
class Wizard {
attack() {
return '魔法使いは呪文を唱えた!';
}
}
class Hunter {
attack() {
return '狩人は矢を放った!';
}
}
上記のクラスのインスタンスを配列に格納し順に**attack()
**メソッドを実行してみます。
const party = [
new Soldier(),
new Wizard(),
new Hunter(),
];
for (const character of party) {
console.log(character.attack());
}
戦士は斬りかかった!
魔法使いは呪文を唱えた!
狩人は矢を放った!
ここで大事なのは定数**character
のattack()
メソッドが実行されていますが、実際に呼び出されているのはcharacter
に格納されたSoldier
、Wizard
、Hunter
のattack()
メソッドであり、同じattack()
**メソッドでも動作が切り替わっていることです。
これを静的型付け言語(C#)で書いてみるとどうでしょうか。
using System;
public class Soldier {
public string Attack() {
return "戦士は斬りかかった!";
}
}
public class Wizard {
public string Attack() {
return "魔法使いは呪文を唱えた!";
}
}
public class Hunter {
public string Attack() {
return "狩人は矢を放った!";
}
}
public class Program {
static void Main() {
var party = new Object[] {
new Soldier(),
new Wizard(),
new Hunter(),
};
foreach (var character in party) {
Console.WriteLine(character.Attack());
}
}
}
動きません。まず、コンパイルが通らない。
CS1061: Type 'object' does not contain a definition for 'Attack' and no extension method 'Attack' of type 'object' could be found.
CS1061は、存在しないメソッドを呼び出そうとしたときや、存在しないクラス メンバーにアクセスしようとしたときに発生します。
配列にインスタンスを格納するところまでは良いのですが、「**Object
型にはAttack()
**メソッドがないよ」と怒られているようです。
これを動くようにするには以下の2つ方法があります。
- 継承を使う方法
- インターフェースを使う方法
順に見ていきたいと思います。
##継承を使ったポリモーフィズム
まず最初に、CharacterBase
という基底クラスを定義します。
abstractが付加されたクラスは抽象クラスとなりnewによるインスタンス化ができないクラスです。
抽象クラスは継承しインスタンス化できるクラスを定義することが前提になっています。
public abstract class CharacterBase {
public virtual string Attack() {
return "";
}
}
// var character = new CharacterBase(); ←これは無理
続いてCharacterBaseクラスを継承した各キャラクタークラスを定義します。
public class Soldier : CharacterBase {
public override string Attack() {
return "戦士は斬りかかった!";
}
}
public class Wizard : CharacterBase {
public override string Attack() {
return "魔法使いは呪文を唱えた!";
}
}
public class Hunter : CharacterBase {
public override string Attack() {
return "狩人は矢を放った!";
}
}
各キャラクタークラスでは、overrideキーワードを使い**Attack()
メソッドを再定義しています。
これでAttack()
**メソッドを持っているクラスを統一的に扱うことができるようになりました。
それでは、実際に上記のクラスを使ったコードは以下のようになります。
using System;
using System.Collections.Generic;
public class Program {
static void Main() {
var party = new List<CharacterBase>() {
new Soldier(),
new Wizard(),
new Hunter(),
};
foreach (CharacterBase character in party) {
Console.WriteLine(character.Attack());
}
}
}
戦士は斬りかかった!
魔法使いは呪文を唱えた!
狩人は矢を放った!
各キャラクタークラスのインスタンスを格納したList<T>の要素の型がそれぞれの継承元であるCharacterBase
になっています。
foreachブロックではSoldier
もWizard
もHunter
同じCharacterBase
クラスとみなされています。
しかし、呼び出されるAttack()
メソッドはCharacterBase
のAttack()
メソッドではなく、実際のインスタンスのものとなります。
つまり、実際の型がSoldier
ならSoldier
、Wizard
ならWizard
、**Hunter
ならHunter
のAttack()
**メソッドが呼び出されています。
抽象クラスを使うと、異なる型のオブジェクトを同一視し、そのオブジェクトの型によって動作が切り替えることができるようになります。
このようにポリモーフィズムでは静的型つけ言語の厳密性を保ったまま、動的型つけ言語の柔軟性を得ることができます。
##インターフェースを使ったポリモーフィズム
インターフェースを使っても抽象クラスと同じことが可能です。
インターフェースは製品の規格のようなものでプロパティやメソッドの呼び出し方だけを定めたものです。
interface ICharacter {
string Attack();
}
上記は**ICharacter
を実装したクラスにはstringを返すAttack()
メソッドを定義しなければならないと定められていることを表します。
インターフェースにはpublic**と言ったアクセス修飾子は付けることはできません。
それでは次に、**ICharacter
**クラスを実装した各キャラクタークラスを定義します。
インターフェースのメソッドやプロパティの具体的な動作はここに書くことになります。
public class Soldier : ICharacter {
public string Attack() {
return "戦士は斬りかかった!";
}
}
public class Wizard : ICharacter {
public string Attack() {
return "魔法使いは呪文を唱えた!";
}
}
public class Hunter : ICharacter {
public string Attack() {
return "狩人は矢を放った!";
}
}
これらのキャラクタークラスを利用するコードは以下のようになります。
using System;
using System.Collections.Generic;
public class Program {
static void Main() {
var party = new List<ICharacter>() {
new Soldier(),
new Wizard(),
new Hunter(),
};
foreach (ICharacter character in party) {
Console.WriteLine(character.Attack());
}
}
}
利用する側のコードは**List<T>の要素の型がICharacter
になっただけでCharacterBase
**を継承した場合と同じです。
##その他
###抽象メソッドの修飾子、virtual
とabstract
について
抽象クラスの**CharacterBase
**は以下のように書くことでもできます
abstract class CharacterBase {
public abstract string Attack();
}
抽象メソッドの修飾子がabstractの場合には実際の処理を記述しません。
メソッドの修飾子がvirtualの場合にはオーバーライドしなくても構いませんが、abstractの場合には必ずオーバーライドし具体的な処理を定義する必要があります。
###ポリモーフィズムは実際でどこでどう使うか
ポリモーフィズムについて実際いつどこで役に立つのかと考えると、入力値のバリデーションなどに使えそうかなと思いました。
婚活サービスの登録フォームの検証で考えてみます。
ユーザーの入力値は以下のモデルにバインドされるとします。
public class Model {
public string Name { get; set; }
public DateTime Birth { get; set; }
public bool Married { get; set; }
}
基底の検証クラスを定義します。
public abstract class ValidatorBase {
protected Model _model;
public ValidatorBase(Model model) {
this._model = model;
}
public abstract bool IsValid();
}
基底クラスを継承した各検証クラスを定義します。
public class NameValidator : ValidatorBase {
public NameValidator(Model model): base(model) { }
// 入力されているかつ20文字以下なら可
public override bool IsValid() {
if (String.IsNullOrWhiteSpace(this._model.Name)) {
return false;
}
if (this._model.Name.Length > 20) {
return false;
}
return true;
}
}
public class BirthValidator : ValidatorBase {
public BirthValidator(Model model): base(model) { }
// 20歳以上40歳以下なら可
public override bool IsValid() {
var age = DateTime.Now.Year - this._model.Birth.Year;
if (DateTime.Now < this._model.Birth.AddYears(age)) {
age--;
}
if (!(20 <= age && age <= 40)) {
return false;
}
return true;
}
}
public class MarriedValidator : ValidatorBase {
public MarriedValidator(Model model): base(model) { }
// 結婚していない人のみ可
public override bool IsValid() {
return !this._model.Married;
}
}
上記クラスを利用したコードを書いてみます。
using System;
using System.Collections.Generic;
public class Program {
static void Main() {
// ※実際にはユーザーの入力値がバインドされることを想定しています。
var model = new Model() {
Name = "名無しの権兵衛",
Birth = new DateTime(1992, 7, 17),
Married = false
};
var Validators = new List<ValidatorBase>() {
new NameValidator(model),
new BirthValidator(model),
new MarriedValidator(model)
};
foreach (var validaotr in Validators) {
// 順に検証して処理する
}
}
}
検証メソッドは**Model
**に持った方が良いのかもしれません。
おそらく、あまり良い例ではないと思います。
アドバイスがありましたらよろしくお願いします。
###抽象クラスとインターフェースの使い分け
ポリモーフィズムは、抽象クラスとインターフェースを2つ方法で実現できました。
では、この2つはどのように使い分ければよいのでしょうか?
抽象クラス | インターフェース | |
---|---|---|
具体的な実装 | 持つこともできる | 持てない |
多重継承 | できない | できる |
調べると以下のようなことが書かれていました。
抽象クラスと派生クラスは「継承」の関係はIS A関係と呼ばれている対して、インターフェースと実装クラスの「実装」の関係はCAN DO関係と呼ばれているそうです。
鳥によっては鳴かない鳥、飛ばない鳥もいるでしょう。インターフェースが分かれているのと、インターフェースは多重継承できるという仕様上、継承するしない、複数継承といった使い分けができるのです。
###TypeScriptで書いてみる
プライベートではJavaScriptよりTypeScriptを書く場面が多いのでTypeScriptで実装してみました。
abstract class CharacterBase {
public attack(): string {
return "";
}
}
class Soldier extends CharacterBase {
public attack(): string {
return '戦士は斬りかかった!';
}
}
class Wizard extends CharacterBase {
public attack(): string {
return '魔法使いは呪文を唱えた!';
}
}
class Hunter extends CharacterBase {
public attack(): string {
return '狩人は矢を放った';
}
}
const party: Array<CharacterBase> = [
new Soldier(),
new Wizard(),
new Hunter(),
];
for (const c of party) {
console.log(c.attack());
}
※追記 2019/11/04
TypeScriptは構造的部分型(Structural Subtyping)ですのでCharacterBase
抽象クラスは必要ありません。