Edited at

プログラマー1年生がポリモーフィズムについて学んだのでRPGで説明する。

More than 1 year has passed since last update.

オブジェクト指向プログラムのポリモーフィズムについて学んだのでまとめてみたいと思います。

以下を参考にしました。


ポリモーフィズムってなんだ?

ポリモーフィズムとはオブジェクト指向プログラミングの概念の一つです。

日本語では多態性・多様性などと訳されます。

簡単に言うと同じ名前のメソッドを複数のクラスで使用できるようにし、そのメソッドを通して、暗黙的に複数のインスタンスの動作を切り替えることができるようにします。

例えば、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());
}

戦士は斬りかかった!

魔法使いは呪文を唱えた!
狩人は矢を放った!

ここで大事なのは定数characterattack()メソッドが実行されていますが、実際に呼び出されているのはcharacterに格納されたSoldierWizardHunterattack()メソッドであり、同じattack()メソッドでも動作が切り替わっていることです。

これを静的型付け言語(C#)で書いてみるとどうでしょうか。


jsのコードをそのまま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によるインスタンス化ができないクラスです。

抽象クラスは継承しインスタンス化できるクラスを定義することが前提になっています。


CharacterBaseクラス

public abstract class CharacterBase {

public virtual string Attack() {
return "";
}
}

// var character = new CharacterBase(); ←これは無理


続いて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ブロックではSoldierWizardHunter同じCharacterBaseクラスとみなされています。

しかし、呼び出されるAttack()メソッドはCharacterBaseAttack()メソッドではなく、実際のインスタンスのものとなります。

つまり、実際の型がSoldierならSoldierWizardならWizardHunterならHunterAttack()メソッドが呼び出されています。

抽象クラスを使うと、異なる型のオブジェクトを同一視し、そのオブジェクトの型によって動作が切り替えることができるようになります。

このようにポリモーフィズムでは静的型つけ言語の厳密性を保ったまま、動的型つけ言語の柔軟性を得ることができます。


インターフェースを使ったポリモーフィズム

インターフェースを使っても抽象クラスと同じことが可能です。

インターフェースは製品の規格のようなものでプロパティやメソッドの呼び出し方だけを定めたものです。


ICharacterインターフェース

interface ICharacter {

string Attack();
}

上記はICharacterを実装したクラスにはstringを返すAttack()メソッドを定義しなければならないと定められていることを表します。

インターフェースにはpublicと言ったアクセス修飾子は付けることはできません。

それでは次に、ICharacterクラスを実装した各キャラクタークラスを定義します。

インターフェースのメソッドやプロパティの具体的な動作はここに書くことになります。


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を継承した場合と同じです。


その他


抽象メソッドの修飾子、virtualabstractについて

抽象クラスのCharacterBaseは以下のように書くことでもできます


abstractを使った抽象メソッド

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 CharactorBese {
public attack(): string {
return '狩人は矢を放った';
}
}

const party: Array<CharacterBase> = [
new Soldier(),
new Wizard(),
new Hunter(),
];

for (const c of party) {
console.log(c.attack());
}

ここまで読んでいただきありがとうございました。


Web エンジニアを目指しているSIer 所属の2年目プログラマーです。

良かったらつながってくれると嬉しいです。:grinning:

twitter: のさ@nosa_programmer