はじめに
PHPの学習を進める中で、最初の大きな壁となるのがオブジェクト指向だと思います。そしてそのオブジェクト指向の概念の中でも重要なキーワードである”ポリモーフィズム”について学習したのですが、初心者の自分にとってかなり難しい概念だったと思います。
しかし、ChatGPTさんと一緒に学ぶことで、効率的かつ深く理解を深められたと感じています。そこで今回は、その学習の過程を記録として記事にまとめることにしました。
(とても初歩的な質問もしてますので、ご理解ください。)
ポリモーフィズムについて
僕が読んでいる「オブジェクト指向でなぜつくるのか 第3版」という本の中ではこう書いてありました。
<OOPの三大要素2 ─ ポリモーフィズム>
ポリモーフィズムはサブルーチンを呼び出す側のロジックを一本化する仕組み、すなわち「共通メインルーチン」を作る仕組みである。
ー平澤 章. オブジェクト指向でなぜつくるのか 第3版 知っておきたいOOP、設計、アジャイル開発の基礎知識 (pp. 110-111). (Function). Kindle Edition.
あまりに抽象的すぎるので、ChatGPTさんに解説してもらいました。
<プロンプト>
<OOPの三大要素2 ─ ポリモーフィズム>
ポリモーフィズムはサブルーチンを呼び出す側のロジックを一本化する仕組み、すなわち「共通メインルーチン」を作る仕組みである。
これは具体的にどういう事ですか?例を用いて教えて。
<ChatGPTの回答>
とてもよい質問です!
「ポリモーフィズムは共通メインルーチンを作る仕組み」というのは、異なるクラスのオブジェクトに対して、同じ“命令”で異なるふるまいをさせることで、呼び出す側のコードを共通化・簡略化できるという意味です。
🔧 具体例:動物が鳴くプログラム(ポリモーフィズムなし)
<?php
class Dog {
public function bark() {
echo "ワンワン\n";
}
}
class Cat {
public function meow() {
echo "ニャーニャー\n";
}
}
// 呼び出し側(メインルーチン)
function makeAnimalSound($animal) {
if ($animal instanceof Dog) {
$animal->bark();
} elseif ($animal instanceof Cat) {
$animal->meow();
}
}
makeAnimalSound(new Dog());
makeAnimalSound(new Cat());
👎 問題点:
呼び出し側(makeAnimalSound())がDogとCatの違いを知っていて、分岐処理をしている。
→ 動物の種類が増えるたびにif文が増えて、コードが複雑になる。
✅ ポリモーフィズムありの例(共通メインルーチン)
<?php
interface Animal {
public function makeSound();
}
class Dog implements Animal {
public function makeSound() {
echo "ワンワン\n";
}
}
class Cat implements Animal {
public function makeSound() {
echo "ニャーニャー\n";
}
}
// 共通メインルーチン
function makeAnimalSound(Animal $animal) {
$animal->makeSound();
}
makeAnimalSound(new Dog());
makeAnimalSound(new Cat());
👍 メリット:
• makeAnimalSound() は、Animal型(共通のインターフェース)だけを知っていればよい。
• 動物の種類が増えても makeSound() を実装すればよく、呼び出し側のコードは一切変える必要がない。
考察:
ポリモーフィズムなしとありのコードを比較してみると、ポリモーフィズムありのコードは見やすいコードと言われていますが、初心者の僕にとっては抽象クラスというわけのわからない概念が使われているのでこっちのコードの方が見にくいというのが正直のところです。逆に、なしの方だとif文が増える=コードが複雑になるので問題だとしているのですが、個人的には見やすい分岐だと思うので、それも腑に落ちないところです。ということでまた質問。
<プロンプト>
if文が増えるとコードが煩雑になるとありますが、個人的には見やすい分岐だと思うんですけど、どう思いますか?
また、”ポリモーフィズムなしの問題点:呼び出し側(makeAnimalSound())がDogとCatの違いを知っていて、分岐処理をしている。”
とありますが、「DogとCatの違いを知っている」とはどういうことを意味しているの?
<ChatGPTの回答>
とても良い視点です。「分岐の方が見やすく感じる」というのは初心者〜中級者ではよくある感覚で、実際、少数の条件分岐なら if文の方が直感的で理解しやすい場合もあります。
処理が多くなると読みづらい
たとえば現在はechoという一つ処理のみであるため見通しは良いように思われますが、10行、20行と増えた時、条件の羅列がノイズになり、全体の見通しが悪くなります。
if ($type === 'dog') { ... }
elseif ($type === 'cat') { ... }
elseif ($type === 'horse') { ... }
// 以下略
✅ 「DogとCatの違いを知っている」とは?
「DogとCatの違いを知っている」というのは、呼び出し側(makeAnimalSound()など)が、Dogはこう鳴く・Catはこう鳴く、という
それぞれの具体的な動作や条件を把握して、個別に対応しているということです。
🔍 もう少し具体的に言うと
例えばこんなコードがあるとします:
function makeAnimalSound($animalType) {
if ($animalType === 'dog') {
echo "ワン!";
} elseif ($animalType === 'cat') {
echo "ニャー!";
}
}
このとき、makeAnimalSound() は
• 「Dog は ‘ワン!’」
• 「Cat は ‘ニャー!’」
という具体的な知識(=違い)を知っていて、動作を分けて処理している。
💣 問題点
✅ 呼び出し側の責任が重くなる
• 呼び出し側が「全ての動物の振る舞い」を知ってる
• 言い換えると、呼び出し側が本来は知らなくていいことまで知ってる
• だからコードが硬くなるし、壊れやすくなる
✅ ポリモーフィズムを使った場合(改善)
interface Animal {
public function makeSound();
}
class Dog implements Animal {
public function makeSound() {
echo "ワン!";
}
}
class Cat implements Animal {
public function makeSound() {
echo "ニャー!";
}
}
function makeAnimalSound(Animal $animal) {
$animal->makeSound();
}
ここでは makeAnimalSound() は「鳴いて」とだけ言えばよくて、
• Dog なのか
• Cat なのか
• Horse なのか
→ それは呼び出し側の知るべきことじゃない!
考察:
ここで自分なりに以下のようにまとめました。
①複雑な処理をするWeb開発ではif文の羅列だと見通しが悪くなる。
②「dogとCatがそれぞれどうやって鳴くのかを呼び出す側が知っている」とはつまり、呼び出し側(function makeAnimalSound)つまり関数定義内でdogやcatが持つ具体的なメソッドbark();とmeow();を書いていること。それすなわち呼び出し側の処理することが多くなる。言い換えると責任が重くなる。そしてそれがなぜ望ましくないかと言うと以下の通り。
- 後から新しいオブジェクト(例えばbird鳥)を追加したいときにclass Birdを作り、呼び出し側function makeAnimalSoundの中身を編集しないといけない。もし(animal動物)が増えていくとif文の分岐が多くなり、煩雑なコードになる。
- ポリモーフィズムでは、抽象クラスAnimalにDogとCatの共通の鳴くという動詞をmakeSoundとして定義する。そうすると、呼び出し側は「鳴いて」と指示するだけで勝手にそれぞれのクラスを参照し異なる処理を実行できる。その結果、責任の分離と保守性(後から修正変更しやすくなること)が向上し、シンプルで見通しの良いコードになる。
総括
最後に、僕のまとめた内容をChatGPTに添削してもらったものを貼って、総括としたいと思います。
<プロンプト>
僕のまとめた内容を推敲して
<ChatGPTの回答>
✅ ポリモーフィズム導入の背景と意義(推敲版)
1. 複雑な処理を伴うWeb開発では、if文の羅列は見通しが悪くなりやすい。
2. 「DogとCatがそれぞれどうやって鳴くのかを呼び出し側が知っている」とは、
すなわち、呼び出し関数 makeAnimalSound() の中に bark() や meow() といった各動物の具体的なメソッドをベタ書きしている状態を意味する。
→ 結果として、呼び出し側が処理を抱え込み、責任が重くなる。
3. これがなぜ望ましくないのか:
• 新しい動物(例:Bird)を追加するたびに、class Bird だけでなく makeAnimalSound() の中身も編集が必要。
• 動物が増えるほど if文や分岐が多くなり、コードが煩雑になる。
• 呼び出し側が “全ての動物の鳴き方” を知っている状態は、保守性が低く、拡張性に乏しい。
4. ポリモーフィズムの導入:
• 抽象クラス Animal に、makeSound() という共通インターフェースを定義する。
• Dog や Cat はそれぞれ独自に makeSound() をオーバーライド。
• 呼び出し側は「鳴いて」とだけ指示すればよく、中身の違いを知らなくて済む。
5. 結果として:
• 責任が適切に分離され、変更・追加も各クラス内で完結できる。
• 保守性・拡張性が高まり、コードの見通しが格段によくなる。