ソフトウェア設計

OOPのポリモーフィズム完全整理:オーバーライドからプロトコルまで

OOPのポリモーフィズムを、オーバーライドからプロトコルまで一気に理解しようとして、混乱したことはありませんか?

読了 4 分
OOPのポリモーフィズム完全整理:オーバーライドからプロトコルまでのカバー画像

OOPのポリモーフィズムを、オーバーライドからプロトコルまで一気に理解しようとして、混乱したことはありませんか?

オーバーロード、オーバーライド、プロトコルは似て見えるため、混同しやすいものです。

結論から言うと、ポリモーフィズムとは「同じ名前の呼び出しが、対象によって異なる動作をすること」です。オーバーライドは継承で、プロトコルは規約でこれを実現します。

この記事では、この3つの概念がどう関係するのかを順を追って整理します。

ポリモーフィズムとは?

ポリモーフィズム(Polymorphism)は、ギリシャ語で「複数(poly)の形(morph)」を意味します。

簡単に言えば、同じメッセージを送っても、受け取るオブジェクトごとに異なる反応をする性質です。

たとえば、犬と猫に同じように「声を出して」と言っても、犬はワンワン、猫はニャーと鳴きます。

呼び出し側は소리내()を呼ぶだけです。何を出力するかは各オブジェクトが決めます。

ポリモーフィズムの核心は「呼び出し側は知らなくてよい」という点です。

どんなものが来ても、同じコードで扱えるからです。

この性質によってコードは柔軟になります。新しい動物を追加しても、呼び出し側のコードを変更する必要はありません。


オーバーロードとオーバーライド、何が違う?

名前が似ているため、ここが最も混同しやすいポイントです。

オーバーロードとは、同じ名前の関数を、引数だけ変えて複数定義することです。

オーバーライドとは、親が定義した関数を子が再定義して上書きすることです。

言葉だけでは難しいので、コードで見てみましょう。

class Animal {
    func sound() { print("...") }
}

class Dog: Animal {
    override func sound() { print("ワンワン") }  // オーバーライド
}

let a: Animal = Dog()
a.sound()
// 出力:ワンワン

ここで面白いのは、aの型がAnimalなのに、Dogの声が出る点です。

これが、実行時に実際のオブジェクトを見て決まるポリモーフィズムです。

1つの親に対し、子がそれぞれ異なる形で再定義する構造
1つの親に対し、子がそれぞれ異なる形で再定義する構造

オーバーロードは少し異なります。呼び出す関数はコンパイル時に決まります。

func add(_ a: Int, _ b: Int) -> Int { a + b }
func add(_ a: String, _ b: String) -> String { a + b }

print(add(1, 2))       // 出力: 3
print(add("か", "な"))  // 出力:かな

そのため、オーバーロードを静的ポリモーフィズム、オーバーライドを動的ポリモーフィズムと呼ぶこともあります。


なぜプロトコルが必要なのか?

継承だけでもポリモーフィズムは実現できます。それなのに、なぜプロトコルがあるのでしょうか?

継承は「is-a」関係なので、親は1つでなければなりません。猫が動物でありながら、同時に別の階層に属するのは難しいでしょう。

プロトコルは「この規約を守れば何でもよい」という仕組みなので、はるかに自由です。

protocol Soundable {
    func sound()
}

struct Cat: Soundable {
    func sound() { print("ニャー") }
}

struct Car: Soundable {
    func sound() { print("ブーブー") }
}

let things: [Soundable] = [Cat(), Car()]
things.forEach { $0.sound() }
// 出力:ニャー
// 出力:ブーブー

猫と車には継承関係がありません。それでも同じ配列に入れて、同じように扱えます。

継承という系譜がなくてもポリモーフィズムを得られること、それがプロトコルの力です。

Swiftのような言語が継承よりプロトコルを重視する理由でもあります。

実際に入力して、protocolとoverrideの感覚をつかみました
実際に入力して、protocolとoverrideの感覚をつかみました

継承とプロトコル、いつ使い分ける?

私が実務で判断するときの基準を表にまとめました。

状況 判断
共通コードを継承させたい 継承(オーバーライド)
関係はないが、同じ動作を保証したい プロトコル
複数の性質を同時に持たせたい プロトコル(複数採用が可能)
値型(struct)にポリモーフィズムを持たせたい プロトコル

簡単な原則をいくつか覚えておけば十分です。

  • 親の実装を多く再利用するなら継承
  • 「できる能力」を定義したいならプロトコル
  • 拡張性が気になるなら、まずプロトコルを検討する

面接ではこう聞かれます

Q. オーバーロードとオーバーライドの違いを説明してください。

オーバーロードは同じ名前で異なる引数を定義するもので、コンパイル時に決まります。オーバーライドは継承したメソッドを子が再定義するもので、実行時に実際のオブジェクトを見て決まります。そのため前者を静的ポリモーフィズム、後者を動的ポリモーフィズムと呼びます。

Q. 継承の代わりにプロトコルを使うメリットは何ですか?

継承は親が1つに制限され、値型には使えません。一方、プロトコルは複数を同時に採用でき、structにも適用できます。関係のない型同士も同じ規約でまとめて扱えるため、拡張に向いています。


ポリモーフィズムは最初は抽象的ですが、「呼び出し側は知らなくてよい」という一文を押さえれば、ほかのことも自然に理解できます。

今日学んだオーバーライドとプロトコルを、ぜひ一度ずつコードで試してみてください。きっと身につきます。応援しています!

あわせて読みたい