多態性(ポリモーフィズム)って何?基本を理解しよう
多態性(Polymorphism / ポリモーフィズム)は、オブジェクト指向プログラミング(OOP)の三大要素の一つです。
一言で言うと 「同じ名前のメソッドを使って、異なるオブジェクトに対して異なる動作をさせる仕組み」です。
「ポリ(poly)」は「多くの」、「モーフィズム(morphism)」は「形態」という意味で、直訳すると「多様な形態」となります。難しく聞こえますが、実は私たちの日常生活にも同じような考え方がたくさんあります。
日常生活で理解する多態性の考え方
プログラミングの概念を理解する前に、身の回りの例から考えてみましょう。
例1:リモコンの「再生ボタン」
同じ「再生」ボタンでも、機器によって動作が異なる
- テレビのリモコン:録画した番組が再生される
- オーディオのリモコン:音楽CDが再生される
- ゲーム機のリモコン:ゲームが起動する
- DVDプレーヤー:映画が再生される
全て「再生」という同じ操作ですが、対象によって実際の動作は全く異なります。これが多態性の基本的な考え方です。
例2:電源ボタン
「電源を入れる」という同じ操作
- パソコン:OSが起動し、デスクトップが表示される
- 電子レンジ:表示パネルが点灯し、操作可能になる
- エアコン:運転を開始し、室温調整が始まる
- 照明:ライトが点く
全て「電源を入れる」という同じインターフェース(ボタンを押す)ですが、それぞれが独自の方法で動作します。
例3:動物の「鳴く」という行動
同じ「鳴く」という行動でも、動物によって違う
- 犬:ワンワン
- 猫:ニャーニャー
- 鳥:ピーピー
- 牛:モーモー
- ライオン:ガオー
全ての動物が「鳴く」という共通の行動を持っていますが、実際の鳴き方(実装)は動物ごとに異なります。
この「同じ操作・同じ名前で、対象によって異なる動作をする」というのが、多態性の本質です。
なぜ多態性が必要なの?
多態性がないプログラミングでは、どんな問題が起きるのか見てみましょう。
多態性を使わない場合の問題
// 多態性を使わない例
function makeAnimalSound(animal) {
if (animal.type === 'dog') {
console.log('ワンワン');
} else if (animal.type === 'cat') {
console.log('ニャーニャー');
} else if (animal.type === 'bird') {
console.log('ピーピー');
} else if (animal.type === 'cow') {
console.log('モーモー');
}
// 新しい動物を追加するたびに、ここにif文を追加...
}
const animals = [
{ type: 'dog', name: 'ポチ' },
{ type: 'cat', name: 'タマ' },
{ type: 'bird', name: 'ピー' }
];
animals.forEach(animal => makeAnimalSound(animal));
この方法の問題点
- 新しい動物を追加するたびに関数を修正する必要がある
- if文が増え続ける:コードが読みにくくなる
- 修正ミスのリスク:既存のコードを変更するので、バグが入りやすい
- 保守性が低い:機能追加のたびに複数箇所を変更
多態性を使った解決方法
// 多態性を使った例
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name}が鳴いています`);
}
}
class Dog extends Animal {
speak() {
console.log(`${this.name}がワンワン鳴いています`);
}
}
class Cat extends Animal {
speak() {
console.log(`${this.name}がニャーニャー鳴いています`);
}
}
class Bird extends Animal {
speak() {
console.log(`${this.name}がピーピー鳴いています`);
}
}
// ここがポイント!同じメソッド名で呼び出せる
const animals = [
new Dog('ポチ'),
new Cat('タマ'),
new Bird('ピー')
];
// どの動物も同じ方法で扱える
animals.forEach(animal => {
animal.speak(); // それぞれ異なる鳴き方をする
});
// 出力:
// ポチがワンワン鳴いています
// タマがニャーニャー鳴いています
// ピーがピーピー鳴いています
多態性を使う利点
- 新しい動物を追加してもif文を書く必要がない
- コードがシンプルで読みやすい
- 既存のコードを変更せずに機能追加できる
- バグが入りにくい
多態性の基本的な仕組み
多態性は主に3つの要素で成り立っています。
要素1:継承
親クラスから子クラスへ、共通の特徴を引き継ぎます。
class Animal {
constructor(name) {
this.name = name;
}
// 共通のメソッド
eat() {
console.log(`${this.name}が食事をしています`);
}
speak() {
console.log(`${this.name}が鳴いています`);
}
}
// Dogは、Animalの特徴を継承
class Dog extends Animal {
// eat()はそのまま使える
// speak()は上書き(オーバーライド)
}
要素2:メソッドのオーバーライド
親クラスのメソッドを、子クラスで書き換えます。
class Animal {
speak() {
console.log('何か音を出す');
}
}
class Dog extends Animal {
speak() { // 親のメソッドを上書き
console.log('ワンワン');
}
}
class Cat extends Animal {
speak() { // 親のメソッドを上書き
console.log('ニャーニャー');
}
}
要素3:統一されたインターフェース
全てのオブジェクトが同じメソッド名で呼び出せます。
// どのオブジェクトも speak() で呼び出せる
const dog = new Dog('ポチ');
const cat = new Cat('タマ');
dog.speak(); // ワンワン
cat.speak(); // ニャーニャー
多態性の3つの種類
多態性には、主に3つの種類があります。
種類1:サブタイプ多態性(オーバーライド)
最も一般的な多態性。継承を使って実現します。
class Shape {
constructor(color) {
this.color = color;
}
getArea() {
return 0; // 基本実装
}
describe() {
console.log(`${this.color}の図形、面積:${this.getArea()}`);
}
}
class Circle extends Shape {
constructor(color, radius) {
super(color);
this.radius = radius;
}
// 面積計算をオーバーライド
getArea() {
return Math.PI * this.radius * this.radius;
}
}
class Rectangle extends Shape {
constructor(color, width, height) {
super(color);
this.width = width;
this.height = height;
}
// 面積計算をオーバーライド
getArea() {
return this.width * this.height;
}
}
class Triangle extends Shape {
constructor(color, base, height) {
super(color);
this.base = base;
this.height = height;
}
// 面積計算をオーバーライド
getArea() {
return (this.base * this.height) / 2;
}
}
// 使い方
const shapes = [
new Circle('赤', 5),
new Rectangle('青', 10, 20),
new Triangle('緑', 8, 6)
];
shapes.forEach(shape => {
shape.describe();
});
// 出力:
// 赤の図形、面積:78.53981633974483
// 青の図形、面積:200
// 緑の図形、面積:24
種類2:パラメータ多態性(オーバーロード)
同じメソッド名でも、引数の型や数によって異なる処理を行います。
// Javaの例(JavaScriptには真のオーバーロードはない)
class Calculator {
// 2つの整数を加算
public int add(int a, int b) {
return a + b;
}
// 2つの小数を加算
public double add(double a, double b) {
return a + b;
}
// 3つの整数を加算
public int add(int a, int b, int c) {
return a + b + c;
}
}
JavaScriptでは、可変長引数やデフォルト引数で似たことができます。
class Calculator {
// 引数の数に応じて動作が変わる
add(...numbers) {
return numbers.reduce((sum, num) => sum + num, 0);
}
}
const calc = new Calculator();
console.log(calc.add(1, 2)); // 3
console.log(calc.add(1, 2, 3)); // 6
console.log(calc.add(1, 2, 3, 4)); // 10
種類3:アドホック多態性(演算子の多態性)
同じ演算子でも、データ型によって異なる動作をします。
// + 演算子の多態性
console.log(5 + 3); // 8(数値の加算)
console.log('Hello' + ' World'); // "Hello World"(文字列の結合)
console.log([1, 2] + [3, 4]); // "1,23,4"(配列の文字列化と結合)
// * 演算子
console.log(5 * 3); // 15(数値の乗算)
console.log('abc' * 3); // NaN(文字列と数値の乗算は不可)
実践的な多態性の活用例
例1:支払いシステム
// 支払いの基底クラス
class Payment {
constructor(amount) {
this.amount = amount;
}
process() {
throw new Error('このメソッドは子クラスで実装してください');
}
// 共通の処理
logTransaction() {
console.log(`取引金額: ${this.amount}円`);
}
}
// クレジットカード決済
class CreditCardPayment extends Payment {
constructor(amount, cardNumber, cvv) {
super(amount);
this.cardNumber = cardNumber;
this.cvv = cvv;
}
process() {
console.log('=== クレジットカード決済 ===');
console.log(`カード番号(末尾): ${this.cardNumber.slice(-4)}`);
console.log('カード情報を検証中...');
console.log('決済処理中...');
this.logTransaction();
console.log('決済完了!');
return { success: true, method: 'credit_card' };
}
}
// 銀行振込決済
class BankTransferPayment extends Payment {
constructor(amount, accountNumber, bankName) {
super(amount);
this.accountNumber = accountNumber;
this.bankName = bankName;
}
process() {
console.log('=== 銀行振込決済 ===');
console.log(`銀行名: ${this.bankName}`);
console.log(`口座番号: ${this.accountNumber}`);
console.log('振込処理中...');
this.logTransaction();
console.log('振込完了!');
return { success: true, method: 'bank_transfer' };
}
}
// 電子マネー決済
class DigitalWalletPayment extends Payment {
constructor(amount, walletId, provider) {
super(amount);
this.walletId = walletId;
this.provider = provider;
}
process() {
console.log('=== 電子マネー決済 ===');
console.log(`決済サービス: ${this.provider}`);
console.log(`ウォレットID: ${this.walletId}`);
console.log('残高確認中...');
console.log('決済処理中...');
this.logTransaction();
console.log('決済完了!');
return { success: true, method: 'digital_wallet' };
}
}
// 決済処理システム
class PaymentProcessor {
processPayment(payment) {
// どの支払い方法でも同じように扱える!
console.log('決済を開始します...\n');
const result = payment.process();
console.log('\n決済結果:', result);
return result;
}
processMultiplePayments(payments) {
payments.forEach((payment, index) => {
console.log(`\n【決済 ${index + 1}】`);
this.processPayment(payment);
});
}
}
// 使い方
const processor = new PaymentProcessor();
const payments = [
new CreditCardPayment(15000, '1234567890123456', '123'),
new BankTransferPayment(30000, '1234567', 'みずほ銀行'),
new DigitalWalletPayment(8000, 'user@paypay.com', 'PayPay')
];
processor.processMultiplePayments(payments);
例2:通知システム
// 通知の基底クラス
class Notification {
constructor(recipient, message) {
this.recipient = recipient;
this.message = message;
this.timestamp = new Date();
}
send() {
throw new Error('このメソッドは子クラスで実装してください');
}
// 共通処理
log() {
console.log(`[${this.timestamp.toLocaleString()}] 通知送信完了`);
}
}
// メール通知
class EmailNotification extends Notification {
constructor(recipient, message, subject) {
super(recipient, message);
this.subject = subject;
}
send() {
console.log('--- メール通知 ---');
console.log(`宛先: ${this.recipient}`);
console.log(`件名: ${this.subject}`);
console.log(`本文: ${this.message}`);
console.log('メール送信中...');
this.log();
}
}
// SMS通知
class SMSNotification extends Notification {
constructor(recipient, message) {
super(recipient, message);
}
send() {
console.log('--- SMS通知 ---');
console.log(`電話番号: ${this.recipient}`);
console.log(`メッセージ: ${this.message}`);
console.log('SMS送信中...');
this.log();
}
}
// プッシュ通知
class PushNotification extends Notification {
constructor(recipient, message, appName) {
super(recipient, message);
this.appName = appName;
}
send() {
console.log('--- プッシュ通知 ---');
console.log(`アプリ: ${this.appName}`);
console.log(`デバイストークン: ${this.recipient}`);
console.log(`メッセージ: ${this.message}`);
console.log('プッシュ通知送信中...');
this.log();
}
}
// Slack通知
class SlackNotification extends Notification {
constructor(recipient, message, channel) {
super(recipient, message);
this.channel = channel;
}
send() {
console.log('--- Slack通知 ---');
console.log(`チャンネル: ${this.channel}`);
console.log(`宛先: ${this.recipient}`);
console.log(`メッセージ: ${this.message}`);
console.log('Slack通知送信中...');
this.log();
}
}
// 通知マネージャー
class NotificationManager {
sendNotification(notification) {
notification.send();
}
sendBulkNotifications(notifications) {
console.log(`${notifications.length}件の通知を送信します\n`);
notifications.forEach((notification, index) => {
console.log(`【通知 ${index + 1}/${notifications.length}】`);
this.sendNotification(notification);
console.log('');
});
console.log('全ての通知送信が完了しました');
}
}
// 使い方
const manager = new NotificationManager();
const notifications = [
new EmailNotification(
'user@example.com',
'ご注文を承りました',
'注文確認'
),
new SMSNotification(
'090-1234-5678',
'認証コード: 123456'
),
new PushNotification(
'device-token-abc123',
'新しいメッセージがあります',
'ChatApp'
),
new SlackNotification(
'@john',
'コードレビューをお願いします',
'#dev-team'
)
];
manager.sendBulkNotifications(notifications);
例3:ファイル処理システム
// ファイルの基底クラス
class File {
constructor(filename) {
this.filename = filename;
}
open() {
throw new Error('このメソッドは子クラスで実装してください');
}
read() {
throw new Error('このメソッドは子クラスで実装してください');
}
close() {
console.log(`${this.filename}をクローズしました`);
}
}
// テキストファイル
class TextFile extends File {
open() {
console.log(`テキストファイル ${this.filename} を開きました`);
}
read() {
console.log('テキストデータを読み込み中...');
return 'これはテキストファイルの内容です';
}
}
// PDFファイル
class PDFFile extends File {
open() {
console.log(`PDFファイル ${this.filename} を開きました`);
console.log('PDFリーダーを初期化中...');
}
read() {
console.log('PDFをパース中...');
console.log('テキストを抽出中...');
return 'これはPDFから抽出したテキストです';
}
}
// Excelファイル
class ExcelFile extends File {
open() {
console.log(`Excelファイル ${this.filename} を開きました`);
console.log('ワークブックを読み込み中...');
}
read() {
console.log('シートを解析中...');
console.log('セルデータを読み込み中...');
return { sheet1: [[1, 2, 3], [4, 5, 6]] };
}
}
// 画像ファイル
class ImageFile extends File {
open() {
console.log(`画像ファイル ${this.filename} を開きました`);
}
read() {
console.log('画像データを読み込み中...');
console.log('メタデータを解析中...');
return { width: 1920, height: 1080, format: 'JPEG' };
}
}
// ファイル処理システム
class FileProcessor {
processFile(file) {
console.log('\n--- ファイル処理開始 ---');
file.open();
const data = file.read();
console.log('読み込んだデータ:', data);
file.close();
console.log('--- ファイル処理完了 ---\n');
}
processMultipleFiles(files) {
console.log(`${files.length}個のファイルを処理します`);
files.forEach(file => {
this.processFile(file);
});
console.log('全てのファイル処理が完了しました');
}
}
// 使い方
const processor = new FileProcessor();
const files = [
new TextFile('document.txt'),
new PDFFile('report.pdf'),
new ExcelFile('data.xlsx'),
new ImageFile('photo.jpg')
];
processor.processMultipleFiles(files);
インターフェースによる多態性
多くの言語では、インターフェースを使って多態性を実現できます。
// TypeScriptの例
interface Drawable {
draw(): void;
getColor(): string;
}
class Circle implements Drawable {
constructor(private radius: number, private color: string) {}
draw(): void {
console.log(`半径${this.radius}の${this.color}い円を描画`);
}
getColor(): string {
return this.color;
}
}
class Square implements Drawable {
constructor(private side: number, private color: string) {}
draw(): void {
console.log(`一辺${this.side}の${this.color}い正方形を描画`);
}
getColor(): string {
return this.color;
}
}
class Triangle implements Drawable {
constructor(
private base: number,
private height: number,
private color: string
) {}
draw(): void {
console.log(`底辺${this.base}、高さ${this.height}の${this.color}い三角形を描画`);
}
getColor(): string {
return this.color;
}
}
// インターフェースを使った統一的な処理
function drawAll(shapes: Drawable[]): void {
shapes.forEach(shape => {
shape.draw();
});
}
const shapes: Drawable[] = [
new Circle(5, '赤'),
new Square(10, '青'),
new Triangle(8, 6, '緑')
];
drawAll(shapes);
多態性のメリット:なぜ重要なのか
メリット1:コードの柔軟性が劇的に向上
新しい型を追加しても、既存のコードを変更する必要がありません。
// 新しい動物を追加
class Rabbit extends Animal {
speak() {
console.log(`${this.name}がピョンピョン鳴いています`);
}
}
// 既存のコードはそのまま使える!
const animals = [
new Dog('ポチ'),
new Cat('タマ'),
new Rabbit('ミミ') // 新しい型を追加
];
animals.forEach(animal => animal.speak()); // 変更不要
開放閉鎖の原則(Open/Closed Principle)
- 拡張に対して開いている(新機能を追加しやすい)
- 修正に対して閉じている(既存コードを変更しない)
メリット2:コードの再利用性が高まる
同じコードで、さまざまな型のオブジェクトを処理できます。
class ReportGenerator {
generateReports(reporters) {
reporters.forEach(reporter => {
const report = reporter.generate();
console.log(report);
});
}
}
// PDFレポート、Excelレポート、HTMLレポートなど
// 全て同じ方法で生成できる
メリット3:保守性が大幅に向上
機能追加や変更が簡単で、既存コードへの影響が最小限になります。
// 新しい支払い方法を追加
class PayPalPayment extends Payment {
process() {
console.log('PayPalで支払いました');
return { success: true, method: 'paypal' };
}
}
// PaymentProcessorクラスは一切変更不要
メリット4:テストがしやすくなる
モック(テスト用の偽オブジェクト)を簡単に作成できます。
// 本物のデータベース接続
class RealDatabase {
query(sql) {
// 実際のデータベースに接続
return executeRealQuery(sql);
}
}
// テスト用のモック
class MockDatabase {
query(sql) {
// テスト用の固定データを返す
return [
{ id: 1, name: 'Test User 1' },
{ id: 2, name: 'Test User 2' }
];
}
}
// 同じインターフェースなので、切り替えが簡単
function getUsers(db) {
return db.query('SELECT * FROM users');
}
// 本番環境
const users = getUsers(new RealDatabase());
// テスト環境
const testUsers = getUsers(new MockDatabase());
メリット5:チーム開発が効率的になる
インターフェースを決めれば、並行して開発できます。
// まずインターフェースだけ決める
class PaymentProcessor {
process(payment) {
return payment.process(); // payment.process()があれば動く
}
}
// Aさん:クレジットカード決済を実装
class CreditCardPayment {
process() { /* 実装 */ }
}
// Bさん:銀行振込を実装
class BankTransferPayment {
process() { /* 実装 */ }
}
// それぞれ独立して作業できる
ダックタイピング:継承なしの多態性
JavaScriptやPythonなどの動的型付け言語では、「ダックタイピング」という形式の多態性があります。
「アヒルのように歩き、アヒルのように鳴くなら、それはアヒルである」
継承関係がなくても、同じメソッドを持っていれば同じように扱えます。
// 継承関係は一切ない
class Dog {
speak() {
console.log('ワンワン');
}
}
class Robot {
speak() {
console.log('ピピピ、マスター');
}
}
class Person {
speak() {
console.log('こんにちは');
}
}
class MusicPlayer {
speak() {
console.log('♪〜音楽再生中〜♪');
}
}
// 全て継承関係はないが、speak()があれば動作する
const speakers = [
new Dog(),
new Robot(),
new Person(),
new MusicPlayer()
];
speakers.forEach(speaker => speaker.speak());
// 出力:
// ワンワン
// ピピピ、マスター
// こんにちは
// ♪〜音楽再生中〜♪
ダックタイピングの注意点
- 実行時エラーのリスクがある
- IDEの補完が効きにくい
- メソッド名のタイポに気づきにくい
多態性を使う際の注意点
多態性は便利ですが、使いすぎると問題が起きることもあります。
注意点1:過度な抽象化を避ける
必要以上に複雑にしないことが大切です。
// 過度な抽象化(悪い例)
class AnimalFactory {
createAnimal(type) {
switch(type) {
case 'dog':
return new Dog();
case 'cat':
return new Cat();
case 'bird':
return new Bird();
default:
throw new Error('不明な動物タイプ');
}
}
}
class AnimalManager {
constructor() {
this.factory = new AnimalFactory();
this.animals = [];
}
addAnimal(type) {
this.animals.push(this.factory.createAnimal(type));
}
}
// シンプルな方が良い場合も(良い例)
const animals = [
new Dog('ポチ'),
new Cat('タマ')
];
判断基準
- プロジェクトが小規模なら、シンプルに
- 将来的に多くの型が追加される予定なら、抽象化を検討
注意点2:パフォーマンスへの影響
多態性を使うと、わずかにパフォーマンスが低下する場合があります。
// 直接呼び出し(高速)
function calculateDirectly(x, y) {
return x * y;
}
// 多態性を使った呼び出し(わずかに遅い)
class Calculator {
calculate(x, y) {
return x * y;
}
}
const calc = new Calculator();
calc.calculate(x, y);
注意すべき場面
- ゲームのメインループなど、超高速処理が必要な場合
- 数百万回のループ内で呼び出される場合
ただし、ほとんどの場合、この差は無視できるほど小さいです。
注意点3:デバッグが難しくなることがある
実行時にどのメソッドが呼ばれるか分かりにくい場合があります。
function processItem(item) {
item.process(); // どのクラスのprocess()?
}
// 実行時まで分からない
processItem(new TypeA()); // TypeA.process()
processItem(new TypeB()); // TypeB.process()
対策
- 適切な命名を心がける
- ログを出力して確認する
- デバッガーを活用する
注意点4:継承階層が深くなりすぎない
階層が深すぎると、コードが追いにくくなります。
// 深すぎる階層(避けるべき)
class Vehicle {}
class LandVehicle extends Vehicle {}
class Car extends LandVehicle {}
class Sedan extends Car {}
class LuxurySedan extends Sedan {}
class ElectricLuxurySedan extends LuxurySedan {} // 深すぎ!
// 適切な階層(推奨)
class Vehicle {}
class Car extends Vehicle {}
class ElectricCar extends Car {} // 3階層程度まで
主要プログラミング言語での多態性
JavaScript
class Animal {
speak() {
console.log('音を出す');
}
}
class Dog extends Animal {
speak() {
console.log('ワンワン');
}
}
const dog = new Dog();
dog.speak(); // ワンワン
TypeScript
interface Speaker {
speak(): void;
}
class Dog implements Speaker {
speak(): void {
console.log('ワンワン');
}
}
class Robot implements Speaker {
speak(): void {
console.log('ピピピ');
}
}
Java
abstract class Animal {
abstract void speak();
}
class Dog extends Animal {
@Override
void speak() {
System.out.println("ワンワン");
}
}
class Cat extends Animal {
@Override
void speak() {
System.out.println("ニャーニャー");
}
}
Python
class Animal:
def speak(self):
pass
class Dog(Animal):
def speak(self):
print("ワンワン")
class Cat(Animal):
def speak(self):
print("ニャーニャー")
# 使い方
animals = [Dog(), Cat()]
for animal in animals:
animal.speak()
C#
abstract class Animal {
public abstract void Speak();
}
class Dog : Animal {
public override void Speak() {
Console.WriteLine("ワンワン");
}
}
class Cat : Animal {
public override void Speak() {
Console.WriteLine("ニャーニャー");
}
}
多態性と他のOOP概念の関係
多態性は、他のオブジェクト指向の概念と密接に関係しています。
多態性 × 継承
継承を使って多態性を実現します。
class Vehicle {
start() {
console.log('エンジンを始動');
}
}
class Car extends Vehicle {
start() {
console.log('キーを回してエンジン始動');
}
}
class ElectricCar extends Vehicle {
start() {
console.log('ボタンを押してモーター起動');
}
}
多態性 × カプセル化
カプセル化と組み合わせることで、安全な多態性を実現します。
class BankAccount {
#balance = 0;
deposit(amount) {
if (amount > 0) {
this.#balance += amount;
}
}
getBalance() {
return this.#balance;
}
}
class SavingsAccount extends BankAccount {
#interestRate = 0.01;
addInterest() {
const interest = this.getBalance() * this.#interestRate;
this.deposit(interest);
}
}
SOLID原則との関係
多態性は、SOLID原則と深く関係しています。
開放閉鎖の原則(Open/Closed Principle)
- 拡張に対して開いている
- 修正に対して閉じている
// 新しい図形を追加しても、既存コードを変更しない
class Pentagon extends Shape {
getArea() {
// 五角形の面積計算
}
}
リスコフの置換原則(Liskov Substitution Principle)
- 子クラスは親クラスと置き換え可能であるべき
function calculateTotalArea(shapes) {
return shapes.reduce((sum, shape) => sum + shape.getArea(), 0);
}
// どの図形でも正しく動作する
const shapes = [new Circle(5), new Rectangle(10, 20)];
よくある質問(FAQ)
Q1:多態性と継承の違いは?
A:継承は仕組み、多態性は結果です
- 継承:親クラスの特徴を子クラスが引き継ぐ仕組み
- 多態性:継承を使って、同じメソッド名で異なる動作を実現する結果
継承は多態性を実現するための手段の一つです。
Q2:多態性とオーバーライドの違いは?
A:オーバーライドは多態性を実現する技術です
- オーバーライド:親クラスのメソッドを子クラスで書き換える技術
- 多態性:オーバーライドなどを使って実現される、より広い概念
Q3:いつ多態性を使うべきですか?
A:以下の条件に当てはまる場合に検討しましょう
✅ 使うべき場面
- 複数の似たようなクラスがある
- 将来的に新しい型が追加される可能性がある
- 同じ操作を異なるオブジェクトに適用したい
- コードの柔軟性を高めたい
❌ 使わなくても良い場面
- シンプルな処理で十分
- 型が増える予定がない
- パフォーマンスが最優先
Q4:抽象クラスとインターフェースの違いは?
A:実装を持つかどうかが主な違いです
特徴 | 抽象クラス | インターフェース |
---|---|---|
実装 | 一部持てる | 持てない(原則) |
継承数 | 1つのみ | 複数可能 |
用途 | 共通実装の共有 | 契約の定義 |
Q5:多態性を使うとコードが遅くなりますか?
A:わずかに遅くなりますが、通常は問題ありません
現代のプログラミングでは、保守性や拡張性の方が重要です。パフォーマンスが極めて重要な場合(ゲームエンジンなど)を除き、多態性による速度低下は無視できます。
Q6:JavaScriptに抽象クラスはありますか?
A:ES6の文法では直接サポートされていませんが、実装できます
class AbstractClass {
constructor() {
if (new.target === AbstractClass) {
throw new Error('抽象クラスは直接インスタンス化できません');
}
}
abstractMethod() {
throw new Error('このメソッドは子クラスで実装してください');
}
}
TypeScriptを使えば、より明確に定義できます。
実務での多態性のベストプラクティス
プラクティス1:意味のある名前を付ける
// 悪い例
class A extends B {
doIt() { /* ... */ }
}
// 良い例
class EmailNotification extends Notification {
send() { /* ... */ }
}
プラクティス2:インターフェースを小さく保つ
// 悪い例:インターフェースが大きすぎる
interface Worker {
work(): void;
eat(): void;
sleep(): void;
code(): void;
meeting(): void;
}
// 良い例:小さなインターフェースに分割
interface Workable {
work(): void;
}
interface Eatable {
eat(): void;
}
interface Sleepable {
sleep(): void;
}
プラクティス3:継承よりコンポジションを検討
// 継承(is-a関係)
class Car extends Vehicle {
// 車は乗り物である
}
// コンポジション(has-a関係)
class Car {
constructor() {
this.engine = new Engine(); // 車はエンジンを持つ
this.wheels = new Wheels(); // 車は車輪を持つ
}
}
プラクティス4:リスコフの置換原則を守る
子クラスは親クラスと置き換え可能であるべきです。
// 悪い例:親クラスの契約を破っている
class Rectangle {
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
setWidth(width) {
this.width = width;
this.height = width; // 正方形は幅と高さが同じ
}
setHeight(height) {
this.width = height;
this.height = height;
}
}
// この場合、Squareは本当にRectangleの代わりにならない
プラクティス5:nullオブジェクトパターンを活用
// nullチェックを避けるパターン
class RealUser {
getName() {
return this.name;
}
isNull() {
return false;
}
}
class NullUser {
getName() {
return 'ゲスト';
}
isNull() {
return true;
}
}
function displayUserName(user) {
// nullチェック不要
console.log(user.getName());
}
まとめ:多態性でより良いコードを書こう
多態性(ポリモーフィズム)は、同じインターフェースで異なる実装を提供する強力な仕組みです。
重要ポイントまとめ
- 同じメソッド名で、オブジェクトごとに異なる動作を実現
- 継承とメソッドのオーバーライドで実現
- インターフェースを使っても実現可能
- コードの柔軟性、再利用性、保守性が劇的に向上
- 新しい型の追加が非常に簡単
- ダックタイピングという考え方もある
- 過度な抽象化は避け、適切なバランスを保つ
初心者へのアドバイス
- まずは簡単な動物の例から始める
- 継承とオーバーライドに慣れる
- 実際のプロジェクトで少しずつ使ってみる
- 「本当に多態性が必要か?」を常に考える
- SOLID原則も一緒に学ぶ
学習の次のステップ
- デザインパターン(Strategy、Factory、Observerなど)を学ぶ
- SOLID原則を深く理解する
- 依存性注入(DI)を学ぶ
- リファクタリングの技術を磨く
実務での活用シーン
- 決済システム(複数の支払い方法)
- 通知システム(メール、SMS、プッシュ通知)
- ファイル処理(PDF、Excel、画像など)
- データベースアクセス層(MySQL、PostgreSQL、MongoDB)
- レポート生成システム(PDF、Excel、HTMLレポート)
多態性を適切に使うことで、変更に強く、拡張しやすい、美しいコードが書けるようになります。最初は少し難しく感じるかもしれませんが、実際に使ってみることで、その便利さと強力さが実感できるはずです。
オブジェクト指向プログラミングの核心概念である多態性をマスターして、プロフェッショナルなコードを書けるエンジニアを目指しましょう!