継承って何?まずは基本を理解しよう
継承(Inheritance)は、オブジェクト指向プログラミング(OOP)の中心的な概念の一つです。
一言で言うと 「既にあるクラスの機能や性質を引き継いで、新しいクラスを作る仕組み」です。
プログラミング初心者の方には難しく聞こえるかもしれませんが、実は私たちの身の回りにも同じような考え方がたくさんあります。
日常生活で理解する継承の考え方
例1:生物の分類
動物(最も基本的な分類)
- 共通の特徴:生きている、動ける、食べる、呼吸する
哺乳類(動物の特徴を引き継ぐ)
- 動物の特徴を全て持っている
- さらに追加の特徴:体温を保つ、母乳で子を育てる、毛がある
犬(哺乳類の特徴を引き継ぐ)
- 哺乳類の特徴を全て持っている
- さらに独自の特徴:吠える、尻尾を振る、嗅覚が鋭い
このように、上位の特徴を受け継ぎながら、さらに独自の特徴を追加していく。これが継承の基本的な考え方です。
例2:家電製品
電化製品(基本カテゴリー)
- 共通機能:電源ON/OFF、電力を使う、コンセントに繋ぐ
テレビ(電化製品の機能を引き継ぐ)
- 電化製品の機能を全て持つ
- 追加機能:映像を映す、音を出す、チャンネルを変える
スマートテレビ(テレビの機能を引き継ぐ)
- テレビの機能を全て持つ
- 追加機能:インターネットに接続、アプリを使う、動画配信を見る
このように階層的に機能が追加されていく仕組みが、プログラミングの継承なのです。
なぜ継承が必要なの?
プログラミングでは、似たような機能を持つクラスをたくさん作ることがあります。
継承を使わない場合の問題
// 犬のクラス
class Dog {
constructor(name) {
this.name = name;
}
eat() { console.log('食事をする'); }
sleep() { console.log('眠る'); }
bark() { console.log('ワンワン'); }
}
// 猫のクラス
class Cat {
constructor(name) {
this.name = name;
}
eat() { console.log('食事をする'); } // 重複!
sleep() { console.log('眠る'); } // 重複!
meow() { console.log('ニャー'); }
}
// 鳥のクラス
class Bird {
constructor(name) {
this.name = name;
}
eat() { console.log('食事をする'); } // 重複!
sleep() { console.log('眠る'); } // 重複!
fly() { console.log('飛ぶ'); }
}
全てのクラスで eat() と sleep() を何度も書いています。これは非効率ですね。
継承を使った解決方法
// 親クラス:共通機能をまとめる
class Animal {
constructor(name) {
this.name = name;
}
eat() { console.log(`${this.name}が食事をする`); }
sleep() { console.log(`${this.name}が眠る`); }
}
// 子クラス:独自の機能だけを追加
class Dog extends Animal {
bark() { console.log('ワンワン'); }
}
class Cat extends Animal {
meow() { console.log('ニャー'); }
}
class Bird extends Animal {
fly() { console.log('飛ぶ'); }
}
// 使い方
const myDog = new Dog('ポチ');
myDog.eat(); // ポチが食事をする(親から継承)
myDog.sleep(); // ポチが眠る(親から継承)
myDog.bark(); // ワンワン(独自のメソッド)
共通部分は一度だけ書けばOK。コードがすっきりしました!
継承の基本用語を覚えよう
プログラミングの継承を理解するために、重要な用語を押さえましょう。
親クラス(スーパークラス、基底クラス)
継承元となるクラスのこと。共通の機能をまとめたクラスです。
別名
- スーパークラス(Superclass)
- 基底クラス(Base class)
- 基本クラス
子クラス(サブクラス、派生クラス)
親クラスを継承して作られるクラス。親の機能を引き継ぎ、独自の機能を追加できます。
別名
- サブクラス(Subclass)
- 派生クラス(Derived class)
extends キーワード
「〜を継承する」という意味のキーワード。多くの言語で使われます。
class Dog extends Animal {
// DogはAnimalを継承する
}
super キーワード
親クラスのメソッドやコンストラクタを呼び出すためのキーワード。
class Dog extends Animal {
constructor(name, breed) {
super(name); // 親のコンストラクタを呼ぶ
this.breed = breed;
}
}
オーバーライド(上書き)
親クラスのメソッドを、子クラスで書き換えること。
class Animal {
speak() {
console.log('何か音を出す');
}
}
class Dog extends Animal {
speak() { // 親のメソッドを上書き
console.log('ワンワン');
}
}
実践的なコード例で理解を深めよう
例1:従業員管理システム
// 親クラス:従業員の共通情報
class Employee {
constructor(name, id, baseSalary) {
this.name = name;
this.id = id;
this.baseSalary = baseSalary;
}
displayInfo() {
console.log(`名前: ${this.name}`);
console.log(`社員ID: ${this.id}`);
}
calculateSalary() {
return this.baseSalary;
}
}
// 子クラス1:正社員
class FullTimeEmployee extends Employee {
constructor(name, id, baseSalary, bonus) {
super(name, id, baseSalary);
this.bonus = bonus;
}
// 給与計算をオーバーライド
calculateSalary() {
return this.baseSalary + this.bonus;
}
}
// 子クラス2:契約社員
class ContractEmployee extends Employee {
constructor(name, id, hourlyRate, hoursWorked) {
super(name, id, 0);
this.hourlyRate = hourlyRate;
this.hoursWorked = hoursWorked;
}
// 給与計算をオーバーライド
calculateSalary() {
return this.hourlyRate * this.hoursWorked;
}
}
// 使い方
const fullTimer = new FullTimeEmployee('田中太郎', 1, 300000, 50000);
console.log(fullTimer.calculateSalary()); // 350000
const contractor = new ContractEmployee('佐藤花子', 2, 2000, 160);
console.log(contractor.calculateSalary()); // 320000
例2:図形の面積計算
// 親クラス:図形の共通機能
class Shape {
constructor(color) {
this.color = color;
}
displayColor() {
console.log(`色: ${this.color}`);
}
// 子クラスで必ず実装してもらうメソッド
calculateArea() {
throw new Error('このメソッドは子クラスで実装してください');
}
}
// 子クラス1:円
class Circle extends Shape {
constructor(color, radius) {
super(color);
this.radius = radius;
}
calculateArea() {
return Math.PI * this.radius * this.radius;
}
}
// 子クラス2:長方形
class Rectangle extends Shape {
constructor(color, width, height) {
super(color);
this.width = width;
this.height = height;
}
calculateArea() {
return this.width * this.height;
}
}
// 使い方
const circle = new Circle('赤', 5);
console.log(circle.calculateArea()); // 78.54...
const rectangle = new Rectangle('青', 10, 20);
console.log(rectangle.calculateArea()); // 200
継承のメリット:なぜ使うべきなのか
メリット1:コードの重複を減らせる
共通の機能を親クラスに一度だけ書けば、複数の子クラスで使えます。
数字で見る効果
- 継承なし:3つのクラスで同じコードを3回書く → 100行 × 3 = 300行
- 継承あり:親クラス1つ + 子クラス3つ → 100行 + (30行 × 3) = 190行
約36%のコード削減!
メリット2:保守性が向上する
共通機能を修正するとき、親クラスだけを変更すれば、全ての子クラスに反映されます。
具体例
// 親クラスの eat メソッドを修正
class Animal {
eat() {
console.log('食事を始めます'); // 変更
console.log('もぐもぐ'); // 追加
}
}
// 全ての子クラス(Dog、Cat、Bird)に自動的に反映される!
メリット3:現実世界の関係を表現できる
「is-a関係」(〜は〜である)を自然に表現できます。
- 犬は動物である(Dog is an Animal)
- 正社員は従業員である(FullTimeEmployee is an Employee)
- 円は図形である(Circle is a Shape)
この関係性をコードで表現することで、プログラムが理解しやすくなります。
メリット4:拡張性が高い
新しい機能を追加したいとき、既存のコードを変更せずに、新しい子クラスを作るだけでOK。
// 新しい動物を追加するのも簡単
class Rabbit extends Animal {
hop() {
console.log('ぴょんぴょん跳ねる');
}
}
// 既存のAnimal、Dog、Catクラスには一切触れない
メリット5:ポリモーフィズムが実現できる
異なる種類のオブジェクトを、統一的に扱えます。
const animals = [
new Dog('ポチ'),
new Cat('タマ'),
new Bird('ピー')
];
// 全ての動物に対して同じメソッドを呼べる
animals.forEach(animal => {
animal.eat(); // それぞれ親クラスのメソッドが実行される
animal.sleep(); // 種類を気にせず統一的に扱える
});
継承のデメリットと注意点
継承は便利ですが、使いすぎると問題が起きることもあります。
デメリット1:親子の結びつきが強くなる(密結合)
親クラスを変更すると、意図せず子クラスが壊れることがあります。
class Parent {
method1() {
console.log('処理A');
}
}
class Child extends Parent {
method2() {
this.method1(); // 親のメソッドに依存
}
}
// 親クラスを変更すると...
class Parent {
method1() {
throw new Error('廃止されました'); // 子クラスが壊れる!
}
}
デメリット2:継承階層が深くなりすぎる
階層が深くなると、コードの全体像が把握しづらくなります。
生物
└ 動物
└ 脊椎動物
└ 哺乳類
└ 肉食動物
└ イヌ科
└ オオカミ属
└ イヌ
このように深すぎる階層は避けるべきです。一般的には、3〜4階層程度が適切とされています。
デメリット3:不要な機能まで継承してしまう
親クラスの全てのメソッドが子クラスに引き継がれるため、不要な機能まで持つことになります。
class Bird extends Animal {
// Animalクラスの swim() メソッドも継承してしまう
// でも全ての鳥が泳げるわけではない...
}
デメリット4:柔軟性に欠ける
一度継承関係を決めると、後から変更するのが難しくなります。
継承 vs コンポジション:どちらを選ぶ?
現代のプログラミングでは、「継承よりコンポジション(委譲)を優先すべき」という考え方が主流です。
継承を使うべき場合(is-a関係)
「〜は〜である」という関係が明確な場合に使います。
// 犬は動物である
class Dog extends Animal {
// 継承が適切
}
コンポジションを使うべき場合(has-a関係)
「〜は〜を持つ」という関係の場合に使います。
// 車はエンジンを持つ(車はエンジンではない)
class Car {
constructor() {
this.engine = new Engine(); // コンポジションが適切
this.wheels = new Wheels();
}
start() {
this.engine.start();
}
}
判断基準の例
| 関係 | 適切な方法 | 理由 |
|---|---|---|
| 犬と動物 | 継承 | 犬は動物である(is-a) |
| 車とエンジン | コンポジション | 車はエンジンを持つ(has-a) |
| 正社員と従業員 | 継承 | 正社員は従業員である(is-a) |
| 人と住所 | コンポジション | 人は住所を持つ(has-a) |
アクセス修飾子と継承の関係
継承では、どのメンバー(プロパティやメソッド)を子クラスに公開するかを制御できます。
public:どこからでもアクセス可能
class Parent {
publicMethod() {
// 子クラスからも外部からもアクセスできる
}
}
protected:子クラスからアクセス可能
class Parent {
protected void protectedMethod() {
// 子クラスからはアクセスできる
// 外部からはアクセスできない
}
}
private:親クラス内部のみ
class Parent {
#privateMethod() {
// 子クラスからもアクセスできない
// 親クラス内部でのみ使用
}
}
実用例
class BankAccount {
#balance = 0; // private: 外部から直接変更できない
deposit(amount) { // public: 誰でも使える
this.#balance += amount;
}
getBalance() { // public: 残高を確認できる
return this.#balance;
}
}
class SavingsAccount extends BankAccount {
addInterest() {
// this.#balance にはアクセスできない
// getBalance() を使って取得する
const balance = this.getBalance();
this.deposit(balance * 0.01); // 1%の利息
}
}
主要プログラミング言語での継承
JavaScript / TypeScript
class Parent {
constructor(name) {
this.name = name;
}
}
class Child extends Parent {
constructor(name, age) {
super(name);
this.age = age;
}
}
Java
public class Parent {
protected String name;
public Parent(String name) {
this.name = name;
}
}
public class Child extends Parent {
private int age;
public Child(String name, int age) {
super(name);
this.age = age;
}
}
Python
class Parent:
def __init__(self, name):
self.name = name
class Child(Parent):
def __init__(self, name, age):
super().__init__(name)
self.age = age
C#
public class Parent {
protected string name;
public Parent(string name) {
this.name = name;
}
}
public class Child : Parent {
private int age;
public Child(string name, int age) : base(name) {
this.age = age;
}
}
よくある質問(FAQ)
Q1:多重継承はできますか?
A:言語によります
- できる言語:C++、Python
- できない言語:Java、C#、JavaScript
多くの言語では、複雑さを避けるために単一継承のみをサポートしています。代わりにインターフェースを使います。
Q2:継承の階層はどのくらいが適切?
A:3〜4階層程度が推奨されます
深すぎる継承は理解が難しくなります。5階層を超える場合は、設計を見直すことをおすすめします。
Q3:継承とインターフェースの違いは?
A:実装を持つかどうかが主な違いです
- 継承:実装(メソッドの中身)を引き継ぐ
- インターフェース:契約(何を実装すべきか)だけを定義
Q4:private なメンバーは継承されますか?
A:継承されますが、アクセスできません
子クラスにも存在しますが、直接アクセスはできません。親クラスの public または protected メソッドを通してのみ利用できます。
まとめ:継承を正しく使ってコードの質を高めよう
継承は、オブジェクト指向プログラミングの重要な概念です。
重要ポイントまとめ
- 継承は既存クラスの機能を引き継ぐ仕組み
- コードの重複を減らし、保守性を高める
- is-a関係(〜は〜である)を表現する
- オーバーライドで動作をカスタマイズできる
- 深すぎる継承階層は避ける
- 現代では「継承よりコンポジション」が推奨される
- 適切な場面で使うことが重要
初心者へのアドバイス
- まずはシンプルな親子関係から始める
- 実際にコードを書いて試してみる
- 本当に継承が必要か、常に考える
- 最初は2〜3階層程度に抑える
- 慣れてきたらコンポジションも学ぶ
継承を適切に使うことで、より保守しやすく、拡張性の高いプログラムを作ることができます。ただし、万能な解決策ではないので、状況に応じて他の設計手法と組み合わせて使いましょう。