ポリモーフィズムは「いろんな型を同じように扱えるようにする」ことです。これを実現するための要素として「オーバーロード」「継承」「インターフェイス」「ジェネリック(Javaならジェネリクスまたは総称型、C++ならテンプレート)」という4つの機能がオブジェクト指向(OO)言語に備わっています。今回は分量の都合でジェネリック以外の機能に関して見ていきたいと思います。
オブジェクト指向入門 一覧
- その1 概要編
- その2 カプセル化編
- その3 ポリモーフィズム編 ←ここ
- その4 ジェネリック編
オーバーロード:One Method Name Fits All
public static int Add(int a, int b)
{
return a + b;
}
public static double Add(double a, double b)
{
return a + b;
}
このように引数によって同名メソッドを多重定義することをオーバーロード(Overload)と言います。アインズの方(Overlord)とはつづりが違います。 単に多重定義と呼ばれることも多いです。これはC言語では考えられないことでした(が実装した猛者がいます)。
オーバーロードを理解するうえで重要な項目の一覧をメソッドシグネチャと呼びます。以下の通りです。
- アクセス指定子
- その他指定子(上例ではstatic)
- メソッド名
- 引数一覧
関数の戻り値の型が含まれていないのは引数によって型を変えることができるように、という配慮です。逆に言えば、戻り値の型だけが違う同名メソッドは定義できません。コンピューターも賢いので呼び分けは可能そうですが、コードを読むほうがどこで何を呼ばれるか分からなくて混乱しますよね。また、ユーザーに優しくないので「同じ名前なのに別の動作をするオーバーロード」はやめましょう。そのようなメソッドは名前を(あとできるなら戻り値のクラスも)変えるべきです。
しかし、オーバーロードだけではシグニチャが異なる関数を大量に作らなければならず大変ですよね。オーバーロードはポリモーフィズムの根幹と言えますが、それだけでは使い勝手が悪いです。ガラスの靴はシンデレラにしか合いませんでしたが、オーバーロードはまさにパーティー出席者全員に専用のガラスの靴を作るようなものです。ポリモーフィズムをサポートするにはスニーカーのような柔軟性が必要ですよね。また、先ほども釘を刺したようにオーバーロードで実装を変わるリスクをどうにかしたいです。
継承:基本的には一子相伝
そこでまず考えられたのは、「あるクラスが使えるメソッドは、そのクラスをベースに作成された『派生クラス』でも使えるようにする」ということでした。この場合、派生クラスはあるクラス(基底クラス)を継承していると言います。
C++のような例外を除き、大抵のオブジェクト指向プログラミング言語が、多重継承を認めていません。これは複数のクラス間で同名メンバが存在するすると、その解決が面倒だからです。また、基底クラスを辿ると同じクラスから派生していたということもあります。これが俗に言う菱形継承問題です。C++はオブジェクト指向黎明期に誕生したこともありますし、もともと仕様をコンパクトにすることで縛りを緩くしているという側面もあるので、複数クラスから継承することができます。
コードとしてはこんな感じです。
public class Harp // ハープ。チューニングなどの細かいことは省略
{
public int NumberOfStrings { get; } // コンストラクタでのみ設定可能
public Harp(int numberOfStrings)
{
NumberOfStrings = nunberOfStrings;
}
public virtual void Strum (int[] stringsToPlay) { /* ハープを弾く処理 */ }
}
public class AutoHarp : Harp // オートハープ。任意の弦を鳴らさないようにできるハープ
{
public AutoHarp(int numberOfStrings)
{
NumberOfStrings = nunberOfStrings;
}
public void Mute(int[] stringsToMute) { /* 任意の弦を鳴らさないようにする */ }
public void Strum (int start, int end) { /* オートハープを掻き鳴らす処理 */ }
}
public class Harper // ハープ奏者
{
private Harp MyHarp;
public Harper(Harp harp)
{
MyHarp = harp;
}
public void PlayCadenza() // カデンツァというコード進行を弾く
{
harp.Strum([1, 3, 5, 8]); // ドミソド
harp.Strum([1, 4, 6, 8]); // ドファラド
harp.Strum([2, 4, 5, 7]); // レファソシ
harp.Strum([1, 3, 5, 8]); // ドミソド
}
public static void Main()
{
AutoHarp autoHarp = new AutoHarp(8);
Harper harper = new Harper(autoHarp); // オートハープもハープとして扱える!
harper.PlayCadenza(); // 当然この処理も無事動作する
}
}
このようにHarp
を継承しているのであれば、どのクラスでもHarper
は同じように扱えます。これが継承によるポリモーフィズムです。
継承先でメソッドの処理を変更するために、オブジェクト指向プログラミング言語にはオーバーライドという機能があります。正直言って日本語で「再定義」といった方が伝わりやすいと思うのですが、C言語などで型の別名を付けることを再定義と呼んでいるのでカタカナ語になってしまっているみたいです。C#の場合はC++同様オーバーライドを許可するメソッドにvirtual
と指定しておき1、派生クラスの当該メソッド定義部分にoverride
と書くことでオーバーライドできます。この辺りのルールは言語によって異なるので、新しい言語では確認しておきたいです。
また、継承に関わるアクセス指定子としてprotected
があります。private
は完全に私的なメンバなので、継承先にすら受け継がれません。しかしprotected
は継承されます。使用例をこの流れで説明すると、ボディの共鳴は楽器ごとに変わってきますが、音を出すメソッドを用意する際にボディの共鳴を処理するprotectedメソッドを用意して、オーバーライドするというのが考えられます。
そして、継承させること自体を目的とした抽象クラスが存在します。C#など、この機構を備えた大半のプログラミング言語はabstract
というキーワードをクラス宣言で使うことで抽象クラスにできます。抽象クラスは直接インスタンス化できません。たいていの場合、フレームワークで使う処理の大部分を抽象クラスとして書いておいて、コードで必要になる部分を継承して補完してね、という使い方です。抽象クラスは大抵の場合、protected
なメンバを持っています。
継承の問題点として、継承先にpublic
およびprotected
メソッドが全て受け継がれてしまうために基底クラスの動作を迂闊に変更できない、無駄な機能まで継承してしまう、virtual
ではないメソッド(Javaならfinal
メソッド)をオーバーライドする必要が出てきた際に詰む2などの問題があります。これは遅くとも2000年代初頭には指摘されていました。Javaを生み出したジェイムズ・ゴスリングさんが継承を実装すべきではなかったと悔やんでいたことは有名です。若い言語であるRustでは継承することは全くできません。
インターフェイス:名は体を表すから、体は見なくて良い
継承の問題(Fragile base class problem、筆者は「基底クラスの脆弱性問題」と訳しています)とは無関係に生み出されたようですが、それを解決する有効手段として知られるのがインターフェイスです。ポリモーフィズムとの関わりも大きいですが、カプセル化とも関連が深い概念です。
インターフェイスはそのクラスがどんなpublic
メンバ(ただしフィールドを除く)を持つか定義する規則のようなものです3。Javaでは定数とメソッド、C#ではメソッドとプロパティが定義できます。かつては(これからも基本的には)メンバ定義のみでしたが、近年はデフォルト処理を書けるようになりました。これは実装の手助けになったり、フェイルセーフとして機能するなどの利点があります。
インターフェイスは何らの実装を提供しないので、継承と違い多重に宣言することができます。そして、インターフェイスを介したアクセスをすることで、実装が隠せます。つまり、インターフェイスはカプセル化を達成する手段でもあるわけです。
なお、C#ではインターフェイスの名前の先頭にI
を付けるという慣例がありますが、仕様で決まっているわけではありません。しかし、習慣に従わないのは読む人にとってもストレスですし、名前を見ただけではインターフェイスだと分からないという問題があります(Javaのインターフェイスはそういう習慣が無いのでややこしいです)。郷に入っては郷に従えです。付けないと「そこにI
はあるんか?」とどこぞの女将に怒られます。
ちなみにC++では多重継承ができることからか、インターフェイスを備え付けていません。苦肉の策として、仮想メソッドのみの抽象クラスで代用しています。
public interface IHarp //ハープが持つ機能を抽出
{
int NumberOfStrings { get; }
void Strum(int[] stringsToPlay);
void PlayNote(int stringToPlay);
}
public interface IGuitar //ギターが持つ機能を抽出
{
int NumberOfStrings { get; }
void Strum(List<Fretting> fingering); // コード弾き。<>のジェネリック宣言は後述
void PlayNote(Fretting fret); // 単音弾き
}
public class Fretting // 運指
{
public int FretNumber { get; }
public int StringNumber { get; }
public Fretting(int stringNumber, int fretNumber)
{
StringNumber = stringNumber;
FretNumber = fretNumber;
}
}
public class HarpGuitar : IHarp, IGuitar // ハープギター。ギターにさらに開放弦をたくさん張ったギター
{
public int IHarp.NumberOfStrings { get; } // このようにメソッドシグネチャが完全に被るメンバーは
public int IGuitar.NumberOfStrings { get; } // インターフェイス毎に定義しないといけない
public HarpGuitar(int harpStrings, int guitarStrings = 6)
{
IHarp.NumberOfStrings = harpStrings;
IGuitar.NumberOfStrings = guitarStrings;
}
public void Strum (int[] stringsToPlay) { /* ハープ部分を弾く処理 */ } // IHarpインターフェイスの実装
public void Strum(List<Fretting> fingering) { /* ギター部分を弾く処理 */ }
// IGuitarインターフェイスの実装。メソッドシグネチャが違うのでインタフェイス名を付けなくてもよい
public void PlayNote(int stringtoPlay) { /*ハープの単音引き*/ }
public void PlayNote(Fretting fret) { /* ギターの単音弾き */ }
}
public class Harper // ハープ奏者
{
private IHarp Harp;
public Harper(IHarp harp)
{
Harp = harp;
}
public void PlayCadenza() // カデンツァというコード進行を弾く
{
Harp.Strum([1, 3, 5, 8]); // ドミソド
Harp.Strum([1, 4, 6, 8]); // ドファラド
Harp.Strum([1, 3, 5, 8]); // ドミソド
Harp.Strum([2, 4, 5, 7]); // レファソシ
Harp.Strum([1, 3, 5, 8]); // ドミソド
}
}
public class Guitarist
{
private IGuitar Guitar;
public Guitarist(IGuitar guitar)
{
Guitar = guitar;
}
public void PlayCadenza() // カデンツァというコード進行を弾く
{
Guitar.Strum(new List<Fingering> { new Fingering(1, 0), new Fingering(2, 1), new Fingering(3, 0), new Fingering(4, 2), new Fingering(5, 3) }); // ドミソド
Guitar.Strum(new List<Fingering> { new Fingering(1, 1), new Fingering(2, 1), new Fingering(3, 2), new Fingering(4, 3), new Fingering(5, 3), new Fingering(6, 1) }); // ドファラド
Guitar.Strum(new List<Fingering> { new Fingering(1, 0), new Fingering(2, 1), new Fingering(3, 0), new Fingering(4, 2), new Fingering(5, 3) }); // ドミソド
Guitar.Strum(new List<Fingering> { new Fingering(1, 1), new Fingering(2, 0), new Fingering(3, 0), new Fingering(4, 0), new Fingering(5, 2), new Fingering(6, 3) }); // レファソシ
Guitar.Strum(new List<Fingering> { new Fingering(1, 0), new Fingering(2, 1), new Fingering(3, 0), new Fingering(4, 2), new Fingering(5, 3) }); // ドミソド
}
}
public static class Program
{
public static void Main()
{
IHarp iHarp = new HarpGuitar(8); // ハープギターだが、インターフェイスとしてインスタンス化できる。
Harper harper = new Harper(iHarp);
harper.PlayCadenza();
HarpGuitar harpGuitar = new HarpGuitar(8);
Guitarist guitarist = new Guitarist(harpGuitar);
guitarist.PlayCadenza();
}
}
このように、インスタンスの種類に関係なく、機能のみの呼び出しに頼っているので、iHarp
をハープのみを実装したクラスに変更しても何も問題ありません。これを発展させた方法として、依存性の注入が挙げられます。
いわゆる「継承よりも合成」というのはクラスを継承して機能を拡張するのではなく、クラスのインターフェイスを使用(合成)して機能を足していくほうがいいよという意味です。その最も分かりやすい例がデコレーターパターンです。
デコレーターパターン:実装が無いなら他の実装を借りればいいじゃない
個人的に好きなデザインパターンです。デコレーターパターンではある機能をインターフェイスとして切り出して、それを実装したクラスAを別の実装クラスBが読みだして機能を補完するという設計手法です。
ここでは名作STGグラディウスの名機ビッグバイパーのパワーアップを例にクラスを作ります。
public class Position // 左下を基準に考える
{
public int X { get; }
public int Y { get; }
public Position(int x, int y)
{
X = x;
Y = y;
Console.WriteLine($"(X, Y) = ({X}, {Y})");
}
}
public enum Direction
{
Up, Right, Down, Left
}
public interface IBigVyper // ビッグバイパーの動作
{
int Speed { get; }
Position CurrentPosition { get; }
Position Move(Direction direction);
void ShootBullet();
void ShootMissile();
}
public class BigVyper : IBigVyper // ビッグバイパーの初期状態
{
public int Speed => 10;
public Position CurrentPosition { get; private set; }
public BigVyper()
{
CurrentPosition = new(0, 100);
}
public Position Move(Direction direction)
{
switch(direction)
{
case Direction.Up:
CurrentPosition = new(CurrentPosition.X, CurrentPosition.Y + Speed);
break;
case Direction.Right:
CurrentPosition = new(CurrentPosition.X + Speed, CurrentPosition.Y);
break;
case Direction.Down:
CurrentPosition = new(CurrentPosition.X, CurrentPosition.Y - Speed);
break;
case Direction.Left:
CurrentPosition = new(CurrentPosition.X - Speed, CurrentPosition.Y);
break;
}
return CurrentPosition;
}
public void ShootBullet()
{
Console.WriteLine("Fire!");
}
public void ShootMissile() { } // 何もしない
}
public abstract class PowerUpVyper : IBigVyper // パワーアップ状態
{
protected IBigVyper _Vyper;
public virtual int Speed => _Vyper.Speed;
public Position CurrentPosition { get; private set; }
public PowerUpVyper(IBigVyper vyper)
{
_Vyper = vyper;
CurrentPosition = vyper.CurrentPosition;
}
public Position Move(Direction direction)
{
switch(direction)
{
case Direction.Up:
CurrentPosition = new(CurrentPosition.X, CurrentPosition.Y + Speed);
break;
case Direction.Right:
CurrentPosition = new(CurrentPosition.X + Speed, CurrentPosition.Y);
break;
case Direction.Down:
CurrentPosition = new(CurrentPosition.X, CurrentPosition.Y - Speed);
break;
case Direction.Left:
CurrentPosition = new(CurrentPosition.X - Speed, CurrentPosition.Y);
break;
}
return CurrentPosition;
}
public virtual void ShootBullet()
{
_Vyper.ShootBullet();
}
public virtual void ShootMissile()
{
_Vyper.ShootMissile();
}
}
public class SpeedUpVyper : PowerUpVyper // スピードアップ状態
{
private static readonly int _SpeedIncrease = 5;
public override int Speed => _Vyper.Speed + _SpeedIncrease;
public SpeedUpVyper(IBigVyper vyper) : base(vyper) { }
}
public class MissileVyper : PowerUpVyper // ミサイル状態
{
public MissileVyper(IBigVyper vyper) : base(vyper) { }
public override void ShootMissile()
{
_Vyper.ShootMissile();
Console.WriteLine("Bomb!");
}
}
public class DoubleVyper : PowerUpVyper // ミサイル状態
{
public DoubleVyper(IBigVyper vyper) : base(vyper) { }
public override void ShootBullet()
{
_Vyper.ShootBullet();
Console.WriteLine("Fire!");
}
}
public static class Gradius
{
public static void Main()
{
IBigVyper vyper = new BigVyper(); // (X, Y) = (0, 100)
vyper.Move(Direction.Right); // (X, Y) = (10, 100)
vyper.ShootBullet(); // Fire!
vyper.ShootMissile(); // 何も表示されない
vyper = new SpeedUpVyper(vyper);
vyper.Move(Direction.Right); // (X, Y) = (25, 100)
vyper.ShootBullet(); // Fire!
vyper.ShootMissile(); // 何も表示されない
vyper = new SpeedUpVyper(vyper);
vyper.Move(Direction.Right); // (X, Y) = (45, 100)
vyper.ShootBullet(); // Fire!
vyper.ShootMissile(); // 何も表示されない
vyper = new MissileVyper(vyper);
vyper.Move(Direction.Right); // (X, Y) = (65, 100)
vyper.ShootBullet(); // Fire!
vyper.ShootMissile(); // Bomb!
vyper = new DoubleVyper(vyper);
vyper.Move(Direction.Right); // (X, Y) = (85, 100)
vyper.ShootBullet(); // Fire! Fire!
vyper.ShootMissile(); // Bomb!
}
}
このような感じで、インスタンスをもらうたびに動作が拡張されていくのがデコレーターパターンの特徴です。また、同じ型を2重に引き継げるなど、継承にはない柔軟性も持ち合わせています。グラディウスの積み重なっていくパワーアップをよく表していますね。
今回は記述の楽さと、デコレーターパターンのよくある例にならって抽象クラスを利用していますが、プログラムの堅牢性を上げるためには面倒くさがらずにインターフェイスだけを受け継いで全部書くほうがいいかもしれません。
まとめ
- ポリモーフィズムとは違う型でも同じように扱えるということ
- 同じ名前の関数を引数の型だけを変えて実装するのがオーバーロード
- 基底クラスを継承した派生クラスも基底クラスとして扱うことができる
- インターフェイスだと実装をしなければならない反面、継承にありがちな問題を回避できる
- インターフェイスを用いて動作を装飾していくのがデコレーターパターン
次回はポリモーフィズムの画竜点睛と呼ぶべきジェネリックに関して説明していきます。
-
逆にJavaでは、派生クラスでの変更をしたくないメンバに
final
を付けて、オーバーライドする関数にはアノテーションで@Override
と書きます。この辺りはC#がC++の良い点を受け継ごうとしたのと、C#の方が後発言語なので、意図しないメソッドの変更でプログラムがクラッシュしないようにしたいという意図があると思います。 ↩ -
メソッドに
new
修飾子を付けることで強引にオーバーライドできますが、解決がコンパイル時になるためお勧めできません(「Effective C# 6.0 / 7.0」(Bill Wagner 著、鈴木幸敏 監訳、翔泳社)、36p)。「effective C#」では、「派生クラスで既に使用済みのメソッド名が、新しいバージョンの親クラスに定義されたメンバと競合した場合」のみnew
修飾子を指定すること、とあります。 ↩ -
厳密にはC#の場合、
protected
メンバも宣言できますが、実際には何の役にも立ちません。 ↩