この記事では、プログラミングにおける「クラス」を、わかりやすく解説します。
「他のサイトの記事や本を読んでもわからなかった」という方も安心してください。
ここでは、抽象的な説明は控えめにして、サンプルコードを使った具体的な例を使って説明します。
クラスとは?
クラスとは「オブジェクト指向プログラミング」における「オブジェクトの設計図」です。
いきなりこんなことをいわれてもピンときませんよね。
クラスを理解するにはオブジェクト指向プログラミングを理解するのが近道です。
オブジェクト指向は抽象的で難しい考え方なので、順を追って丁寧に説明します。
わかりやすく説明するので、安心してついてきてください。
オブジェクト指向プログラミングとは?
プログラミングには、いくつかの考え方があります。
方法論といってもいいでしょう。
システムを開発するときに「こういう方法で作ったら作りやすいよね」という考え方がいくつかあるのです。
これを「プログラミングパラダイム」といいます。
「オブジェクト指向プログラミング」もプログラミングパラダイムのひとつです。
「オブジェクト指向プログラミング」は「オブジェクト」という概念を使って、プログラムをわかりやすく整理しようという考え方です。
※ちなみに、英語では「Object Oriented Programming(OOP)」で「オブジェクトによって整理されたプログラミング」という意味です。
オブジェクトとは?
ここでキーワードとなるのが「オブジェクト」です。
プログラミングで「オブジェクト」とは「変数と関数を持ったデータ構造」のことです。
- オブジェクト
- 変数
- 関数
オブジェクト指向プログラミングでは、変数や関数を「オブジェクト」というひとつの構造にまとめます。
たとえば、ロボットの「歩く機能」をプログラミングするとします。
オブジェクト指向プログラミングでロボットの歩行をプログラミングするには、次のようなロボットオブジェクトをつくります。
- ロボットオブジェクト
- <変数> distance … 歩く距離
- <変数> direction … 歩く方向
- <変数> currentPosition … 現在の位置
- [関数] walk() … 方向の手続き(歩かせる処理)
オブジェクトは、このように「変数や関数をひとつのデータ構造としてまとめたもの」です。
変数や関数をオブジェクトにまとめて整理しながらプログラミングします。
手続き型プログラミングとの比較
あなたがオブジェクト指向プログラミングを経験したことがないとしたら、これまで、以下のようなプログラムを書いてきたのではないでしょうか?
以下はロボットを歩かせるプログラムの例です。
// 変数
var distance = 10;
var direction = "x";
var robot = {
currenPposition: {
x: 10,
y: 10,
}
};
// 関数
function walk(distance, direction, robot) {
// ロボットを動かす処理
if (direction == "x") {
robot.currentPosition.x += distance;
}
// ... (以下略)
}
// 関数呼び出し
walk(distance, direction, robot);
このように、実行順にコードを書き連ねる方法を「手続き型プログラミング」といいます。
手続き型プログラミングは、オブジェクト指向プログラミングと比較して「簡単に書ける」というメリットがありますが、コードが整理されず、乱雑になりがちです。
一方、オブジェクト指向プログラミングでは「オブジェクト」を使ってコードを整理します。
上記のサンプルコードを、オブジェクト指向プログラミングで整理すると以下のようになります。
// ロボットクラスを定義
class Robot {
// ロボットオブジェクトを生成する関数
constructor(initPositionX, initPositionY) {
this.currentPosition = {
x: 0,
y: 0
};
this.currentPosition.x = initPositionX;
this.currentPosition.y = initPositionY;
}
// 現在位置を取得する関数(メソッド)
get currentPosition() {
return this.currentPosition;
}
// 歩行関数(メソッド)
walk(distance, direction) {
if (direction == "x") {
this.currentPosition.x += distance;
}
if (direction == "y") {
this.currentPosition.y += distance;
}
}
}
const robot = new Robot(10, 10); // ロボットオブジェクトを生成
robot.walk(10, "x");
見慣れない表記がたくさんあると思いますが、いまは気にしないでください。
ここで重要なのはコードの書き方の違いです。
手続き型プログラミングでは命令を実行順に書き連ねるだけですが、オブジェクト指向プログラミングでは、①まず「オブジェクトの設計図(クラス)」を書きます。
それから、②設計図(クラス)を使ってオブジェクトを作り、③オブジェクトの関数を呼び出します。
このようにオブジェクト指向プログラミングでは、オブジェクトを作って変数や関数を持たせます。
手続き型プログラミングとオブジェクト指向プログラミングの書き方の違いが、なんとなくイメージできたでしょうか。
ここでは、なんとなくイメージできていればOKです。
いまは以下のことだけ覚えておいてください。
- オブジェクトは変数と関数を持つデータ構造
- オブジェクト指向では、オブジェクトの「設計図」からオブジェクトを作る
さて、ここでいうオブジェクトの「設計図」が、今回のテーマである「クラス」です。
クラスはオブジェクトの設計図
クラスとは、オブジェクト指向プログラミングで「オブジェクトの設計図」に相当するものです。
オブジェクトとは「変数と関数を持つデータ構造」のことでした。
- オブジェクト
- 変数
- 関数
オブジェクトはクラスから生成します。
ひとつのクラス(設計図)から、いくつものオブジェクトを作ることができます。
これは「たい焼き」と「たい焼き器」の関係にたとえるとわかりやすいです。
「たい焼き器」が「クラス(設計図)」です。
それをもとに作られる「たい焼き」たちが「オブジェクト」です。
1つの「たい焼き器」があれば、たくさんの「たい焼き」を作ることができます。
同じように1つの「クラス」があれば、たくさんの「オブジェクト」を作ることができます。
クラスとインスタンス
クラスから作成したオブジェクトを「インスタンス」と呼ぶこともあります。
インスタンスは英語で「実体」を意味する言葉です。
「たい焼き器」は「たい焼き」を作る設計図ですが、焼いてはじめて「たい焼き」という実体ができますね。
オブジェクト指向プログラミングでは、クラスから「インスタンス」を作るということを覚えておきましょう。
この時点ではイメージだけ持っていればOKです。
後ほど、具体例を使って説明します。
ここでのイメージと具体例が結びつけば、オブジェクト指向プログラミングは完璧に理解できます。
オブジェクト指向プログラミングのメリット
ここまで「オブジェクト指向プログラミング」と「クラス」について説明しました。
「手続き型プログラミングの方が簡単なのに、どうしてオブジェクト指向プログラミングなんて使うの?」と思われた方もいるかもしれません。
たしかに、オブジェクト指向はコードを書く立場からするとちょっと面倒です。
しかし、オブジェクト指向は、現在のシステム開発では主流の考え方になっています。
それだけのメリットがあるからです。
オブジェクト指向に従って、変数や関数をひとつのオブジェクトにまとめると、以下のような嬉しいことがあります。
- プログラムを再利用しやすくなる
- 他の作業者がプログラムを利用しやすくなる
- 機能を追加・修正しやすくなる
これらのメリットについて、具体例を用いて簡単に説明します。
1. プログラムを再利用しやすくなる
オブジェクト指向プログラミングでは、プログラムを簡単に再利用できます。
前述のロボットを歩かせる例で、手続き型プログラミングのコードは、以下のようになっていました。
// 変数定義
var distance = 10;
var direction = "x";
// ロボットの状態
var robot = {
currentPosition: {
x: 10,
y: 10
}
};
// ロボットを動かす処理
function walk(distance, direction, robot) { (...略) }
// 関数呼び出し
walk(distance, direction, robot);
ここで、ロボットの数を増やし、3台のロボットを動かすにはどうすればいいでしょうか?
手続き型プログラミングでは、ロボットの状態を扱う変数(robot)を増やして対応します。
// 変数定義
var distance = 10;
var direction = "x";
// ロボット(1台目)の状態
var robot1 = {
currentPosition: {
x: 10,
y: 10
}
};
// ロボット(2台目)の状態
var robot2 = {
currentPosition: {
x: 10,
y: 10
}
};
// ロボット(3台目)の状態
var robot3 = {
currentPosition: {
x: 10,
y: 10
}
};
// ロボットを動かす処理
function walk(distance, direction, robot) {}
// 関数呼び出し
walk(distance, direction, robot1); // 1台目のロボットを動かす
walk(distance, direction, robot2); // 2台目のロボットを動かす
walk(distance, direction, robot3); // 2台目のロボットを動かす
コードが長くなってしまいましたね。
このように手続き型プログラミングでは、コードの再利用が難しいのです。
では、オブジェクト指向プログラミングではどうでしょうか?
オブジェクト指向プログラミングのコードは、以下のようになっていました。
// ロボットクラスを定義
class Robot {
// ロボットオブジェクトを生成する関数
constructor(initPositionX, initPositionY) {
this.currentPosition = {
x: 0,
y: 0
};
this.currentPosition.x = initPositionX;
this.currentPosition.y = initPositionY;
}
// 現在位置を取得する関数(メソッド)
get currentPosition() {
return this.currentPosition;
}
// ロボットを動かす処理
walk(distance, direction) { (...略)}
}
// ロボットオブジェクトを生成
const robot = new Robot(10, 10);
robot.walk(10, "x");
ここでロボットの台数を3台に増やしてみましょう。
オブジェクト指向プログラミングでは、最後の2行にある「ロボットオブジェクトの生成」を繰り返すだけです。
// ロボットクラスを定義
class Robot {
... (略)
}
// ロボットオブジェクトを生成
const robot = new Robot(10, 10);
robot.walk(10, "x");
// ↓↓↓ 以下を付け足すだけ ↓↓↓
const robot2 = new Robot(10, 10);
robot2.walk(10, "x");
const robot3 = new Robot(10, 10);
robot3.walk(10, "x");
ロボットを簡単に増やすことができました。
オブジェクト指向では、「クラス」という設計図があるため、このような複製が簡単なのです。
このように「プログラムを再利用しやすい」ところがオブジェクト指向のメリットのひとつです。
2. 他の作業者がプログラムを利用しやすくなる
オブジェクト指向で開発すると、他の作業者がプログラムを使いやすくなります。
これは後述する「カプセル化」という考え方にも関係していますが、ここではロボットの歩行プログラムの例を使って、使いやすくなる理由を説明しましょう。
以下は、手続き型プログラミングで書いたロボットの歩行プログラムです。
// 変数定義
var distance = 10;
var direction = "x";
// ロボットの状態
var robot = {
currentPosition: {
x: 10,
y: 10
}
};
// ロボットを動かす処理
function walk(distance, direction, robot) { (...略) }
// 関数呼び出し
walk(distance, direction, robot);
このプログラムをエンジニアのAさんが使いたいとします。
ただし、Aさんはロボットの初期位置や、移動距離、移動方向を変えて使いたいと考えています。
あなたがコードを共有した後、Aさんはコピー&ペーストして、一部を書き換えて使います。
// 変数定義
var distance = 20;
var direction = "y";
// Aさんのロボットの状態
var robotOfA = {
currentPosition: {
x: 5,
y: 3
}
};
// ロボットを動かす処理
function walk(distance, direction, robot) { (...略) }
// 関数呼び出し
walk(distance, direction, robotOfA);
Aさんはあなたが書いたコードを良く読んで理解しないと、使えません。
これは、ちょっと面倒ですね。
では、オブジェクト指向の場合はどうでしょうか。
オブジェクト指向の場合、Aさんはコードの中身を理解する必要がありません。
コードの使い方だけを理解すればいいのです。
【ロボットクラスの使い方】
①ロボットオブジェクトを作る
var robot = new Robot(初期位置x, 初期位置x)
②ロボットを歩かせる
robot.walk(移動距離, 移動方向)
この情報さえあれば、Aさんは以下のようにロボットオブジェクトを使えます。
import { Robot } from '..'
var robotOfA = new Robot(5, 3);
robotOfA.walk(20, "y");
ロボットオブジェクトには「ロボットクラス(Robot)」という設計図が用意されているので、Aさんはそれを流用するだけでいいのです。
これがオブジェクト指向で他の作業者がプログラムを使いやすくなる理由です。
以上のように、オブジェクト指向を使うとプログラムを使いやすくなります。
特に大規模なシステム開発では、他のエンジニアが書いたプログラムを使うことが多いため、オブジェクト指向プログラミングが重宝されるのです。
3. 機能を追加・修正しやすくなる
オブジェクト指向で、変数や関数をオブジェクトにまとめると機能の追加・修正がやりやすくなります。
これまで使ってきたサンプルコードを使って、ロボットに「飛ぶ」機能を追加してみましょう。
飛ぶには新たに「飛行高度」の情報が必要です。
手続き型プログラミングでは、以下のように複数のロボットの変数に、新しい情報を追加する必要があります。
// 変数定義
var distance = 10;
var direction = "x";
var altitude = 20; // 飛行高度
// ロボット(1台目)の状態
var robot1 = {
currentPosition: {
x: 10,
y: 10,
altitude: 10 // ← 飛行高度の情報を追加
}
};
// ロボット(2台目)の状態
var robot2 = {
currentPosition: {
x: 10,
y: 10,
altitude: 10 // ← 飛行高度の情報を追加
}
};
// ロボット(3台目)の状態
var robot3 = {
currentPosition: {
x: 10,
y: 10,
altitude: 10 // ← 飛行高度の情報を追加
}
};
// ロボットを動かす処理
function walk(distance, direction, robot) {}
function fly(altitude, robot) {}
// 関数呼び出し
walk(distance, direction, robot1); // 1台目のロボットを動かす
fly(altitude, robot1) {} // 新しい関数呼び出し
walk(distance, direction, robot2); // 2台目のロボットを動かす
fly(altitude, robot2) {} // 新しい関数呼び出し
walk(distance, direction, robot3); // 2台目のロボットを動かす
fly(altitude, robot3) {} // 新しい関数呼び出し
3台のロボットだけでも修正が大変ですね。
修正漏れもあるかもしれません。
これをオブジェクト指向プログラミングでやってみましょう。
オブジェクト指向なら、オブジェクトの設計図であるクラスを変更するだけです。
// ロボットクラスを定義
class Robot {
constructor(initPositionX, initPositionY, initAltitude) {
this.currentPosition = {
x: 0,
y: 0,
altitude: 0 // 飛行高度を初期化
};
this.currentPosition.x = initPositionX;
this.currentPosition.y = initPositionY;
this.currentPosition.initAltitude = initAltitude; // 飛行高度の初期値を設定
}
// 現在位置を取得する関数(メソッド)
get currentPosition() {
return this.currentPosition;
}
// ロボットを動かす処理
walk(distance, direction) { (...略)}
fly(altitude) {(...略)}
}
// ロボットオブジェクトを生成
const robot = new Robot(10, 10);
robot.walk(10, "x");
robot.fly(10); // 新しい関数(メソッド)呼び出し
const robot2 = new Robot(10, 10);
robot2.walk(10, "x");
robot2.fly(10); // 新しい関数(メソッド)呼び出し
const robot3 = new Robot(10, 10);
robot3.walk(10, "x");
robot3.fly(10); // 新しい関数(メソッド)呼び出し
新しい変数の初期化のために2行を追加し、あとは飛行のメソッドを追加するだけです。
手続き型プログラミングよりも変更箇所が少なく、修正漏れも起きにくくなっています。
このように、オブジェクト指向では機能の追加・修正が簡単になります。
クラスの基礎
クラスの「構造」と「作り方」について、さらに詳しく説明します。
重要な言葉がたくさん出てくるので、ここでしっかり覚えましょう。
ここではJavaScriptで書いた「Personクラス」を例にクラスの構造を説明します。
なお、JavaScriptのクラスについてはMDNの公式ドキュメントを参考にしてください。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
walk() {
console.log(`${this.name}は歩いています`);
}
talk() {
console.log(`${this.name}は話しています`);
}
}
const person = new Person("Alice", 25);
person.walk();
person.talk();
上記の例では、Personクラスを作成して、オブジェクト(インスタンス)をつくり、オブジェクトの関数を呼び出しています。
この例に沿ってクラスの作り方を説明します。
1. クラスを定義する
クラスはclassキーワードを使って定義します。
class Person {
// ... (クラスの中身)
}
classキーワードの後に、クラスの名前を書きます。
クラスの名前は先頭を大文字(パスカルケース)で書きます。
クラス名の後に、波括弧({})でブロックを作ります。括弧内にクラスの中身を記述します。
2. コンストラクタを定義する
次にコンストラクタを定義します。
コンストラクタ(constructor)とは「クラスからオブジェクト(インスタンス)を作成するときに実行する関数」です。
コンストラクタは英語で「建設するやつ」というような意味ですね。
まさにクラス(設計図)からインスタンス(実体)を作るやつなのです。
class Person {
constructor() { } // コンストラクタを定義する
}
クラスは、以下のようにnewキーワードを用いてインスタンスを作成することではじめて使えるようになります。
このように、newキーワードを使ってインスタンスを作成することを「インスタンス化」といいます。
const person = new Person();
newキーワードを使うことで、設計図であるクラスから実体であるインスタンスを作るというわけです。
「たい焼き器」を使ってnew「たい焼き」を作るということですね。
コンストラクタとは、まさにこの「インスタンス化」のタイミングで自動的に実行される関数です。
new Person()
のようにクラスをインスタンス化するタイミングで、Personクラスのconstructor関数
が自動的に実行されます。
3. インスタンスを初期化する
コンストラクタを定義したら、コンストラクタの中に、インスタンスの初期化処理を実装します。
インスタンスの初期化処理としては、インスタンスの変数に初期値を設定するなど、初期化時に済ませておきたい処理を設定します。
プロパティとは?
オブジェクトは「変数」と「関数」を持つデータ構造であると説明しました。
インスタンス(=オブジェクト)は、そのインスタンスに固有の変数を持ちます。
たとえば、人には「名前」や「年齢」がありますよね。
Pesronクラスから作られるPersonインスタンスにも、「名前」や「年齢」などのデータを「変数」として持たせることができます。
このようにインスタンスに持たせる「変数」のことを「プロパティ(property)」といいます。
- オブジェクト(インスタンス)
- 変数(プロパティ)
- 関数(メソッド)
プロパティは、英語で「所有物」のことです。
まさにインスタンスが所有するデータということです。
プロパティの初期値を設定する
コンストラクタで、プロパティの「初期値」を設定できます。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
JavaScriptのクラスでは、プロパティの先頭に「this.」をつけます。
thisはそのインスタンス自体を指しています。
「this.」の後にプロパティ名(変数名)を書きます。
this.name
で「このインスタンス(this)の名前(name)」というように書けるわけです。
また、コンストラクタは引数を持つこともできます。
ここではnameとageの2つの引数を持たせています。
class Person {
constructor(name, age) { // コンストラクタの引数
...
こうすると、クラスをインスタンス化するときにコンストラクタの引数に値を渡すことができます。
const person = new Person("Alice", 20); // 引数nameに"Alice"が、引数ageに20が渡される
こうすれば、コンストラクタの引数から値を受け取り、プロパティの初期値を設定できるのです。
class Person {
constructor(name, age) { // 引数に受け取った値を
this.name = name; // プロパティ「this.name」に設定する
this.age = age; // プロパティ「this.age」に設定する
}
}
4. メソッドを実装する
次に、Personクラスにメソッドを実装します。
メソッドとは?
オブジェクトは「変数(プロパティ)」と「関数」を持つことができるデータ構造でした。
オブジェクト指向プログラミングで、オブジェクトが持つ関数を「メソッド」といいます。
- オブジェクト(インスタンス)
- 変数(プロパティ)
- 関数(メソッド)
特定のオブジェクトに属さない「変数」や「関数」は、そのまま「変数」や「関数」と呼びます。
一方、クラスやインスタンスが持つ「変数」は「プロパティ」、「関数」は「メソッド」と呼んで区別します。
- オブジェクトに属する「変数」→「プロパティ」
- オブジェクトに属する「関数」→「メソッド」
日本語では、数学で使う「f(x)」のようなものを「関数」といいますね。一方で、「プロのメソッド」のように特定の個人が持っている方法論を「メソッド」と呼んだりします。
そのように、オブジェクトに属していないものは「関数」、オブジェクトに属しているものは「メソッド」と呼ぶ、と覚えておきましょう。
メソッドを実装する
さて、メソッドを実装します。
JavaScriptでクラスにメソッドを実装するのは簡単です。
クラスの定義の中で、普通に関数を定義するだけです。
class Person {
constructor(name, age) {
this.name = name; // プロパティ「this.name」に設定する
this.age = age; // プロパティ「this.age」に設定する
}
// walkメソッドを実装する
walk() {
console.log($`{this.name}は歩いています`);
}
// talkメソッドを実装する
talk() {
console.log(`${this.name}は話しています`);
}
}
ここではwalkメソッドとtalkメソッドを実装しました。
メソッドの中で「this.name」のようにインスタンスのプロパティを参照できます。
メソッドの実装は簡単ですね。
ここで定義したメソッドは、インスタンスから呼び出すことができます。
const person = new Person("Alice", 20);
person.walk(); // Aliceは歩いています
person.talk(); // Aliceは話しています
5. 完成
これでクラスの実装は完了です。
簡単ですね。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
walk() {
console.log(`${this.name}は歩いています`);
}
talk() {
console.log(`${this.name}は話しています`);
}
}
const person = new Person("Alice", 25);
person.walk();
person.talk();
練習問題
クラスの構造をよく理解するために練習問題に挑戦しましょう。
- Personクラスに「favoriteFood」プロパティを追加し、コンストラクタで「favoriteFood」プロパティの初期値を設定できるようにしてください
- Personクラスに「eat」メソッドを追加してください
- 「eat」メソッドが「(name)の好きな食べ物は(favoriteFood)です」と標準出力できるように実装してください
- Personクラスをインスタンス化して、eatメソッドを呼び出してください
クラスの継承
クラスの継承を使うと、コードの再利用性が高まります。
継承とは?
継承とは、あるクラスからプロパティやメソッドを受け継いで別のクラスでも使えるようにすることです。
たとえば「乗用車クラス」と「パトカークラス」の2つのクラスがあるとします。
これらのクラスは両方とも車です。そのため、共通するプロパティとメソッドを持っています。
たとえば、「速度」「走行距離」「燃料残量」などのプロパティを持ち、「走る」「止まる」「燃料を補給する」などのメソッドを持ちます。
ここで、新しいクラスとして「タクシークラス」を作ることにしました。
タクシークラスにも、乗用車クラスやパトカークラスが共通して持っている機能を実装したいです。
しかし、これらの機能をイチから作り直すのは面倒です。
そこで乗用車クラスとパトカークラスの共通部分を「車クラス」として一本化することを考えます。
- 車クラス
- (継承)→乗用車クラス
- (継承)→パトカークラス
- (継承)→タクシークラス
3つのクラスの共通する機能を持った「車クラス」を作り、車クラスを3つのクラスに継承すれば、同じ機能を何度も実装しなくて済みます。
このように、あるクラスの機能を他のクラスに引き継ぐ形で使うことを「継承」といいます。
共通する機能を1つのクラスにまとめることで、コードの再利用を高めることができます。
スーパークラスとサブクラス
継承元のクラスを「スーパークラス(親クラス)」、継承先のクラスを「サブクラス(小クラス)」といいます。
先ほどの例では、「車クラス」がスーパークラスで、「乗用車クラス」「パトカークラス」「タクシークラス」がサブクラスです。
ここで「スーパー(super-)」は「上位の」、「サブ(sub-)」は「下位の」という意味です。
クラスの継承を実装するには?
クラスの継承を実装してみましょう。
以下はJavaScriptでクラスの継承を実装するサンプルコードです。
class Car {
constructor(speed, gas, mileage) {
this.speed = speed;
this.gas = gas;
this.mileage = mileage;
}
run() {
console.log("走る");
}
stop() {
console.log("止まる");
}
chargeGas() {
console.log("燃料を補給する");
}
}
// TaxiクラスにCarクラスを継承させる
class Taxi extends Car {
pickUp() {
// タクシー固有のメソッド
console.log("お客さんを迎えに行く");
}
}
// Carクラスのコンストラクタを使える
const taxi = new Taxi(50, 100, 1000);
// Carクラスで定義したプロパティを参照できる
console.log(`speed: ${taxi.speed}, gas: ${taxi.gas}, mileage: ${taxi.mileage}`);
// Carクラスのメソッドを呼び出せる
taxi.run();
// Taxiクラス固有のメソッドを呼び出せる
taxi.pickUp();
JavaScriptは次にようにextendsキーワードを使って、クラスを継承できます。
class SubClass extends SuperClass {
}
ここではCarクラスを継承してTaxiクラスを作るので以下のようにします。
class Taxi extends Car {
}
また、Carクラスを継承したTaxiクラスでは、改めてコンストラクタを書かなくても、Carクラスのコンストラクタを使えるようになっています。
// TaxiクラスにCarクラスを継承させる
class Taxi extends Car {
// Taxiクラスにはコンストラクタを定義していない
}
const taxi = new Taxi(50, 100, 1000); // Carクラスのコンストラクタを使える
さらに、TaxiクラスのインスタンスではCarクラスで定義したプロパティやメソッドが使えます。
// Carクラスのコンストラクタを使える
const taxi = new Taxi(50, 100, 1000);
// Carクラスで定義したプロパティを参照できる
console.log(`speed: ${taxi.speed}, gas: ${taxi.gas}, mileage: ${taxi.mileage}`);
// Carクラスのメソッドを呼び出せる
taxi.run();
Taxiクラスに固有の機能を実装すればと 、Taxiクラスのインスタンスで呼び出すことができます。
class Taxi extends Car {
pickUp() {
// タクシー固有のメソッド
console.log("お客さんを迎えに行く");
}
}
// Taxiクラス固有のメソッドを呼び出せる
taxi.pickUp();
ただし、サブクラスであるTaxiクラスで定義したメソッドを、スーパークラスであるCarクラスのインスタンスから呼び出すことはできません。
class Taxi extends Car {
pickUp() {
// タクシー固有のメソッド
console.log("お客さんを迎えに行く");
}
}
const car = new Car(30, 100, 2000);
// スーパークラスのインスタンスからサブクラスのメソッドは呼び出せない
car.pickUp(); // TypeError: car.pickUp is not a function
オーバーライド
クラスの継承によって、スーパークラスの機能をサブクラスでも使えるようになりました。
また、サブクラスではスーパークラスにはないメソッドを新たに追加できます。
それでは、サブクラスでスーパークラスのメソッドと同名のメソッドを定義するとどうなるのでしょうか。
class SuperClass {
superMethod() {
console.log("This is method of SuperClass");
}
}
class SubClass extends SuperClass {
// スーパークラスのメソッドと同名のメソッドをサブクラスで定義する
superMethod() {
console.log("This is method of SubClass"); // メソッドの処理内容を変更する
}
}
const sub = new SubClass();
sub.superMethod(); // どちらが呼び出される??
結論からいうと、サブクラスのメソッドが呼び出されます。
const sub = new SubClass();
sub.superMethod(); // This is method of SubClass
このように、サブクラス側でスーパークラスのメソッドを上書きすることを「オーバーライド」といいます。
オーバーライドはどんなときに使う?
オーバーライドにはどんな使いみちがあるのでしょうか?
オーバーライドは、サブクラス同士で振る舞いを変えたいときに使います。
たとえば、同じ日本人でも出身地域によって「ありがとう」の言い方が違いますよね。
こういうときにメソッドのオーバーライドが使えます。
class Japanese {
thank() {
console.log("ありがとう");
}
}
class Tochigian extends Japanese {
thank() {
// スーパークラスは同じだが「あいさつ」の振る舞いが変わる
console.log("あんがとね");
}
}
class Chibanian extends Japanese {
thank() {
// スーパークラスは同じだが「あいさつ」の振る舞いが変わる
console.log("あんがとう");
}
}
const tochigian = new Tochigian();
const chibanian = new Chibanian();
tochigian.thank(); // あんがとね
chibanian.thank(); // あんがとう
スーパークラスのメソッドを呼び出す
メソッドをオーバーライドしても、superキーワードを使ってスーパークラスのメソッドを呼び出すことができます。
superキーワードは次のように使います。
super.スーパークラスのメソッド()
実際にsuperキーワードを使って、サブクラスからスーパークラスのメソッドを呼び出してみましょう。
class SuperClass {
method() {
console.log("スーパークラスのメソッドを呼び出しました。");
}
}
class SubClass extends SuperClass {
method() {
super.method(); // superキーワードでスーパークラスのメソッドを呼び出す
console.log("サブクラスのメソッドを呼び出しました。");
}
}
const sub = new SubClass();
sub.method();
// スーパークラスのメソッドを呼び出しました。
// サブクラスのメソッドを呼び出しました。
サブクラスでメソッドをオーバーライドしても、スーパークラスのメソッドが呼び出せていますね。
カプセル化
オブジェクト指向プログラミングの重要な考え方のひとつに「カプセル化」があります。
カプセル化とは「クラス内のプロパティやメソッドに対する外部からのアクセスを制限すること」をいいます。
なぜプロパティやメソッドへのアクセスを制限するのか?
クラスのプロパティやメソッドについて、外部のプログラムからは変更してほしくない場合があります。
たとえば、円の面積を計算するMathクラスに円周率を定義しているプロパティ(this.pi)があるとします。
class Math {
constructor() {
this.pi = 3.141592; // 円周率プロパティを定義
}
// 円の面積を計算するメソッド。
// 引数に半径を渡すと面積を計算して返す
calcCircleArea(radius) {
return radius * radius * this.pi;
}
}
Mathクラスを他のプログラムから呼び出したときに、呼び出し側で円周率の値を変えられてしまうと円の面積の計算が破綻します。
const math = new Math();
const correctAnswer = math.calcCircleArea(3);
console.log(`正しい計算結果: 3 x 3 = ${correctAnswer} ∵ math.pi=${math.pi}`);
// 正しい計算結果: 3 x 3 = 28.274328 ∵ math.pi=3.141592
math.pi = 42; // クラスを使う人が勝手に円周率を書き換える
const wrongAnswer = math.calcCircleArea(3);
console.log(`間違った計算結果: 3 x 3 = ${wrongAnswer} ∵ math.pi=${math.pi}`);
// 間違った計算結果: 3 x 3 = 378 ∵ math.pi=42
このくらいシンプルなクラスで、プロパティが円周率とわかっていれば、間違って変えてしまうことはないかもしれません。
しかし、大規模なシステム開発でひとつのクラスにいろいろなプロパティがつくようになると、クラスを使う側は、そのプロパティを変更していいのか悪いのか、判断できなくなります。
そこで「カプセル化」という機能が必要になるのです。
プロパティやメソッドを外部から隠すには?
カプセル化を使うと、特定のプロパティやメソッドを外部から読み取れないように、アクセスを制限できます。
カプセル化の方法としては「privateやprotectedなどのアクセス修飾子をつける方法」や「ゲッター(getter)やセッター(setter)と呼ばれるメソッドを定義してアクセス方法を限定する方法」が一般的です。
ここではJavaScriptを例にプロパティをカプセル化して、変数を外部から変更できないようにしましょう。
サンプルコードとして、先ほどのMathクラスを使います。
class Math {
constructor() {
this.pi = 3.141592; // 円周率プロパティを定義
}
// 円の面積を計算するメソッド。
// 引数に半径を渡すと面積を計算して返す
calcCircleArea(radius) {
return radius * radius * this.pi;
}
}
ここでは、Mathのプロパティである「this.pi」を変更できないようにします。
JavaScriptでは、プロパティ名の先頭に「#」をつけることでプロパティをプライベートにすることができます。
プロパティがプライベートになると、そのプロパティにアクセスできるのはクラス内部だけになります。
class Math {
#pi; // #をつけてプライベートなプロパティとして宣言する
constructor() {
this.#pi = 3.141592; // クラス内部では「this.#pi」として参照できる
}
// 円の面積を計算するメソッド。
// 引数に半径を渡すと面積を計算して返す
calcCircleArea(radius) {
return radius * radius * this.#pi; // クラス内部では「this.#pi」として参照できる
}
}
プライベートなプロパティには、クラスの外部からアクセスできなくなります。
よって、クラス内部のプロパティ「#pi」を外部から変更することはできなくなります。
const math = new Math();
const beforeAnswer = math.calcCircleArea(3);
console.log(`計算結果: 3 x 3 = ${beforeAnswer}`);
// 計算結果: 3 x 3 = 28.274328
math.pi = 42; // ここではクラス内部のプロパティ「#pi」を書き換えることはできない
const afterAnswer = math.calcCircleArea(3);
console.log(`計算結果: 3 x 3 = ${afterAnswer}`);
// 計算結果: 3 x 3 = 28.274328
math.#pi = 42; // 「#」をつけて変更を試みる
// SyntaxError: Private field '#pi' must be declared in an enclosing class
「math.#pi = 42
」のように「#」をつけて変更しようとした場合も「SyntaxError: Private field ‘#pi’ must be declared in an enclosing class」のように文法エラーが出て、変更できません。
このように、カプセル化を使うことで、変更されたくないプロパティやメソッドを外部から隠蔽できます。
【言語別】クラスの書き方
言語別にクラスの実装方法を紹介します。
C言語のクラス
C言語には「クラス」がありません。
C言語はオブジェクト指向プログラミングに対応していないためです。
クラスに相当するものを作るには、C言語のstruct(構造体)が使えます。
Pythonのクラス
Pythonはオブジェクト指向プログラミングに対応しています。
Pythonのクラスは、classキーワードを使って、以下のように定義できます。
class Hello:
def __init__(self, name):
self.name = name
def hello(self):
print(f'Hello, {self.name}!')
my_class = Hello('Python')
my_class.hello();
__init__
という名称でメソッドを定義するとコンストラクタを作成できます。
また、メソッドの最初の引数(self)がそのクラス自身を指します(thisに相当します)。
Pythonのクラスの詳細については公式ドキュメントを参照してください。
JavaScriptのクラス
JavaScriptはオブジェクト指向プログラミングに対応しています。
JavaScriptのクラスは、classキーワードを使って、以下のように定義できます。
class Hello {
constructor (name) {
this.name = name;
}
hello() {
console.log(`Hello, ${this.name}!`);
}
}
myClass = new Hello();
myClass.hello();
constructorという名称でメソッドを定義するとコンストラクタが作成できます。
また、thisがそのクラス自信を指します。
JavaScriptのクラスについて詳しくはMDNの公式ドキュメントを参照してください。
Rubyのクラス
Rubyはオブジェクト指向プログラミングに対応しています。
Rubyのクラスは、以下のように定義できます。
class Hello
def initialize(name)
@name = name
end
def hello()
puts "Hello, #{@name}!"
end
end
myClass = Hello.new("Ruby")
myClass.hello()
Rubyでは「@変数名」のように@をつけた変数がプロパティ(インスタンス変数)になります。
initializeと命名したメソッドがコンストラクタとして認識されます。
また、クラス名.new()
のようにnewメソッドを呼び出すことでインスタンスを生成します。
Rubyのクラスについて詳しくは公式ドキュメントを参照してください。
PHPのクラス
PHPはオブジェクト指向プログラミングに対応しています。
PHPのクラスは、以下のように定義できます。
<?php
class Hello
{
public string $name;
function __construct(string $name) {
$this->name = $name;
}
public function hello() {
echo "Hello, ".$this->name."!";
}
}
$myClass = new Hello("PHP");
$myClass->hello();
?>
PHPでは変数「$this」がクラスのインスタンスを示します。`$this->name`のようにしてプロパティ(インスタンス変数)にアクセスできます。
また、__construct
と命名したメソッドがコンストラクタとして認識されます。
PHPのクラスについて詳しくは公式ドキュメントを参照してください。
Javaのクラス
Javaはオブジェクト指向のプログラミング言語です。
Javaでクラスは、以下のように定義できます。
import java.util.*;
class Hello {
String name;
Hello(String name) {
this.name = name;
}
public void hello() {
System.out.println("Hello, "+ this.name + "!");
}
}
public class Main {
public static void main(String[] args) throws Exception {
Hello myClass = new Hello("Java");
myClass.hello();
}
}
Javaでは「this」がクラスのインスタンスを示します。
`this.name`のようにしてプロパティ(インスタンス変数)にアクセスできます。
また、クラス名と同名のメソッドがコンストラクタとして認識されます。
Javaのクラスについて詳しくはオラクルのドキュメントを参照してください。
C#のクラス
C#はオブジェクト指向のプログラミング言語です。
C#でクラスは、以下のように定義できます。
public class Hello{
private string name;
public Hello(string name)
{
this.name = name;
}
public void hello()
{
System.Console.WriteLine($"Hello, {this.name}!");
}
}
class MainClass
{
static void Main() {
var myClass = new Hello("C#");
myClass.hello();
}
}
C#では「this」がクラスのインスタンスを示します。
this.name
のようにしてクラスのプロパティを参照できます。
また、クラス名と同名のメソッドがコンストラクタとして認識されます。
C#のクラスについて詳しくは公式ドキュメントを参照してください。
まとめ
この記事では、プログラミングの「クラス」を解説しました。
- クラスは「オブジェクト指向プログラミング」で使う
- オブジェクトとは「変数と関数」を持つデータ構造
- クラスはオブジェクトの設計図
- クラスの持つ変数を「プロパティ」、クラスの持つ関数を「メソッド」という
- クラスの継承を使うと他のクラスに機能を受け継ぐことができる
- 継承元のクラスを「スーパークラス」、継承先のクラスを「サブクラス」という
- 継承先のクラスでメソッドを書き換えることを「オーバーライド」という
- カプセル化によってプロパティやメソッドへの外部からのアクセスを制限できる
オブジェクト指向やクラスについて学びはじめると、見慣れない言葉がたくさん出てきて大変だと思います。
混乱したときはシンプルなクラスを自分で書いて、動作を確かめながら学習してみましょう。
言葉で聞くと難しいことも、使いながら覚えれば意外と簡単です。
ITエンジニアの開発現場では、たとえ「プロパティ」「継承」「サブクラス」などの言葉を忘れていたとしても、コードを見て全体の構造を理解できていれば大した問題にはなりません。
まずは使いながら覚えて、あとから言葉を覚えるようにしてみてください。そうすれば、学習が進みやすくなります。ぜひ、試してみてください。