こんにちは!独立を目指して日々奮闘中の「六条すずや」です!
この記事では、オブジェクト指向プログラミングで頻出の概念「クラスの継承」と「オーバーライド」について、ものすごく噛み砕いて分かりやすく解説します!
目次
継承は「オブジェクト指向の三大要素」のひとつ!
突然ですが、「オブジェクト指向の三大要素」はご存知ですか?
オブジェクト指向言語には、次に示す3つの特徴があります。
これらを理解せずにオブジェクト指向の概念を理解することは不可能です!
- カプセル化
- 継承
- ポリモーフィズム
この記事で解説する「継承」は、オブジェクト指向の三大要素のひとつなんですね。
「オーバーライド」も、オブジェクト指向の三大要素のひとつである「ポリモーフィズム」と深い関係があります。
つまり、この記事の内容をしっかり理解すれば、オブジェクト指向の概念自体も理解がしやすくなるはずです。
では、さっそく見ていきましょう!
「継承」とは
継承とは、「あるクラスに定義された変数(フィールド)やプロパティ、メソッドを引き継いで、新しいクラスを作ること」です!
…いきなりプログラミング用語がたくさん出てきてしまったので、一つずつ説明します。
前提知識
クラス
「クラス」とは、オブジェクト指向言語特有の概念で、「物」の設計図のようなものです。
何を言っているかわからないかもしれませんが、本当に「物」を表すのに使用します。
例えば、レースゲームを開発するときに、コンピュータに「車とはこういうものだ!」と分からせるのは大変ですよね?
そこで、車クラスを作成することで、コンピュータに「車の設計図」を渡すことができます。
その設計図を表すのに必要なものが、「変数(フィールド)」「プロパティ」「メソッド」です。
そもそもオブジェクト指向とは、「現実世界に存在する物を、そのままプログラムに落とし込むことを目的として作られた」と主張する人もいるほどです。
変数(フィールド)
変数とは、そのクラス(物)の様々な状態を記憶しておく部分のことです。
例えば、車で考えると、「ガソリンの残量」「今の走行速度」「エンジンの排気量」などがそれに当たりますね!
class Car {
private float m_gas; //ガソリン残量
private int m_speed; //走行速度
private float m_engineDisplacement; //エンジンの排気量
}
ここで注意すべき点は、これらの値はユーザー(運転者)に好き勝手にいじってもらっては困るということです。
勝手にガソリンの残量を0リットルにしたり100リットルにしたり、ましては-1リットルにしてみたりと、好き勝手に変数の値をいじられたら車は壊れます(バグります)ね。
なので、変数(フィールド)は、ユーザーに勝手にアクセスされないよう、基本的にprivate指定にします。
private指定にすると、そのクラスの内部(つまり車自身)からしかその変数の値をいじったり、参照したりすることができなくなるので、ガソリンの残量を好き勝手にいじられなくてすみます。
とは言っても、ガソリンが少なくなったら、ユーザーがガソリン残量を増やさなければなりませんよね?
どうやってユーザーは車にガソリンを入れるのでしょうか?
これには、次の「プロパティ」を使用します。
プロパティ
プロパティとは、通常外部からアクセスできない「変数(フィールド)」のうち、外部に公開したい部分を定義したものです。
先ほどの例でいくと、「ガソリンの量」はユーザーがアクセスできなければ(外部に公開されていなければ)残量を確認することもできませんし、ガソリンを入れることもできません。
したがって、「ガソリンの量」は外部に公開したいですね。
ということで、プロパティにしましょう!
プロパティにすることで、ユーザーがアクセスできるようになります。
class Car {
private float m_gas; //ガソリン残量
private int m_speed; //走行速度
private float m_engineDisplacement; //エンジンの排気量
//プロパティ
public float Gas {
get {
return m_gas;
}
set {
if(value < 0) {
Debug.WriteLine("ガソリン残量をマイナスにすることはできません。");
} else {
m_gas = value;
}
}
}
}
getというのは、その名の通りプロパティの値をゲットするという意味です。
ここでは、そのままm_gasの値をreturnしていますね。
つまり、privateのため外部からアクセスできないはずのm_gasの値をそのまま取得することができるようになっています。
対してsetというのは、そのままプロパティに値をセットするという意味です。
ここで注目ですが、setの中でifによる条件分岐を行なっています。
value(セットされた値)がマイナスの場合、エラーメッセージを表示するようになっていますね。
マイナスでない場合のみ、m_gasに値を代入するようになっています。
つまり、変数(フィールド)に想定外の値が代入されないようになっているのですね。
このように、変数(フィールド)をそのまま公開せずに、プロパティを介して公開すると、入力された値が想定外の値でないかをチェックし、適切な値のみを代入させることができます。
これによって、外部から変な値を無理やり入力されて思わぬバグが発生することを防ぐことができます。
メソッド
メソッドは、そのクラスの振る舞いを記述したものです。
Carクラスの場合はどのようなメソッド(振る舞い)を記述すべきでしょうか?
車が持つ振る舞いとして最も基本的なものは、「進む」「曲がる」「止まる」ですね。
class Car {
private float m_gas; //ガソリン残量
private int m_speed; //走行速度
private float m_engineDisplacement; //エンジンの排気量
//プロパティ
public float Gas {
get {
return m_gas;
}
set {
if(value < 0) {
Debug.WriteLine("ガソリン残量をマイナスにすることはできません。");
} else {
m_gas = value;
}
}
}
//メソッド
public void Forward(){
//進む処理
}
public void Turn(){
//曲がる処理
}
public void Stop(){
//止まる処理
}
}
Carクラスの振る舞いとして、3つのメソッドを追加しました。
振る舞いはユーザーからのアクションによって実行されるものが多いので、メソッドのアクセス識別子は基本的にpublicを指定することが多いです。(もちろん場合によります)
publicにしたので、ユーザーは車を自由に走らせたり曲がらせたり止まらせたりすることができますね。
再び「継承」とは
お疲れ様でした。ここまでが「継承」を理解するための前提知識となります。
では、再び「継承」の意味を見てみます。継承とは、
「あるクラスに定義された変数(フィールド)やプロパティ、メソッドを引き継いで、新しいクラスを作ること」
でしたね。
「引き継ぐ」とは具体的にどういうことかというと、継承するクラスに書かれた内容を記述しなくても、内部的に記述したことになっています。
なんの役に立つのか?
例えば、レースゲームに普通の車のみならず、トラックやバイクも登場させたいとします。
トラックは体当たりして他の車を攻撃できる機能をもち、またバイクは4輪車と比べてハンドリングに癖をもたせるものとします。
それでは、そのような機能を実装したTrackクラスやBikeクラスを作成しましょう、となるわけですが、継承を知らないと次のようなことになります。
class Track {
private float m_gas; //ガソリン残量
private int m_speed; //走行速度
private float m_engineDisplacement; //エンジンの排気量
//プロパティ
public float Gas {
get {
return m_gas;
}
set {
if(value < 0) {
Debug.WriteLine("ガソリン残量をマイナスにすることはできません。");
} else {
m_gas = value;
}
}
}
//メソッド
public void Forward(){
//進む処理
}
public void Turn(){
//曲がる処理
}
public void Stop(){
//止まる処理
}
public void Attack(){
//体当たりする処理
}
}
class Bike {
private float m_gas; //ガソリン残量
private int m_speed; //走行速度
private float m_engineDisplacement; //エンジンの排気量
//プロパティ
public float Gas {
get {
return m_gas;
}
set {
if(value < 0) {
Debug.WriteLine("ガソリン残量をマイナスにすることはできません。");
} else {
m_gas = value;
}
}
}
//メソッド
public void Forward(){
//進む処理
}
public void Turn(){
//癖のあるハンドリング処理
}
public void Stop(){
//止まる処理
}
}
どちらのクラスも、大半のコードがCarクラスのコピーとなってしまいました。
これの何がいけないのかというと、例えば後から「全体的にもう少し車の加速を大きくしたい」などとなったときに、すべてのクラスのForward()メソッドの中身を同じように書き換えなければならなくなります。
とても面倒くさいですよね。それに、見落としがあり、ひとつのクラスだけ加速が鈍いままになってしまうかもしれません。
そこで「継承」の出番です。
共通部分をすべて「乗り物」を表すVehicleクラスに定義してしまい、Car, Track, BikeはVehicleクラスを継承するようにしましょう!
class Vehicle {
private float m_gas; //ガソリン残量
private int m_speed; //走行速度
private float m_engineDisplacement; //エンジンの排気量
//プロパティ
public float Gas {
get {
return m_gas;
}
set {
if(value < 0) {
Debug.WriteLine("ガソリン残量をマイナスにすることはできません。");
} else {
m_gas = value;
}
}
}
//メソッド
public void Forward(){
//進む処理
}
public virtual void Turn(){
//曲がる処理
}
public void Stop(){
//止まる処理
}
}
※Turnメソッドにvirtualという文字が追加されていますが、あとで解説するのでひとまず気にしないでください。
これで、「乗り物」の共通部分をまとめた「Vehicleスーパークラス」が完成しました!
あとは、Vehicleクラスを継承したCar, Track, Bikeクラスを作成しましょう。
共通部分はVehicleクラスにあるので、そのクラスの特別な部分のみ記述すれば良いですね!
なお、継承される側のクラス(今回の場合はVehicleクラス)のことを「スーパークラス」、継承する側のクラス(今回の場合はCar,Track,Bikeクラス)のことを「サブクラス」と言います。
この言葉は非常によく使うので、覚えておいてください!
【サブクラス】 あるクラスを継承したクラス。
ではさっそく、Vehicleクラスを継承したCarクラスを作成します。
Vehicleスーパークラスと比較して、Carクラスの特別な部分のみを記述すれば良いわけですが、今思えば特別な部分はないですね。
つまり、Vehicleクラスを継承さえすれば、何も記述しなくて良いことになります。
class Car : Vehicle {
}
たったこれだけで済んでしまいました。
次はTrackクラスを作成します。Trackクラスの特徴は、体当たりができることでしたね。
class Track : Vehicle {
public void Attack(){
//体当たりする処理
}
}
Trackクラスも非常にすっきりと書くことができました!
さて、問題はBikeクラスです。
Bikeクラスの特徴は、ハンドリングに癖を持たせるのでした。
では、ハンドリングに癖を持たせたTurnメソッドを記述すれば良いでしょうか?
しかし、考えてみてください。
継承とは「スーパークラスに記述されたフィールド、プロパティ、メソッドをすべて引き継ぐ」のでしたね。
つまり、通常のTurnメソッドも引き継がれてしまいます!
では、Bikeクラスには特別なBikeTurnメソッドを用意して、そこにバイクの特徴である「癖のあるハンドリング処理」を記述すればいいですね!
class Bike : Vehicle {
public void BikeTurn(){
//癖のあるハンドリング処理
}
}
お気付きの方もいるかもしれませんが、これには問題があります。
BikeクラスはVehicleスーパークラスのすべてを引き継ぐので、TurnメソッドとBikeTurnメソッドが共存してしまいますね。
「別に、バイクではTurnを使わずBikeTurnだけを使えばいいじゃないか!」と思うかもしれませんが、Bikeクラスの内容を全く知らない共同開発者が、曲がる処理を書こうと思ったとき、間違えてTurnメソッドを使ってしまうかもしれません。
自分一人だけで開発する場合はまだ良いかもしれませんが、チームで開発するときにこのようなクラスがあればバグの元になります。
もちろん、一人で開発するときでも、人間であればミスはするので、できる限りこのような設計はなくすべきですね。
ではどのようにすれば良いのかというと、ここで「オーバーライド」の出番です!!
「オーバーライド」とは?
オーバーライドとは、「スーパークラスで定義されたプロパティとメソッドを、サブクラスで上書きすることができる機能」です。
通常、継承をするとスーパークラスのプロパティやメソッドがすべてそのまま引き継がれちゃいます。
でも、Bikeクラスのように、「外から見たら『曲がる』という処理は同じだけど、中身の処理を少し変えたい!」という場合に、この「オーバーライド」がとても役に立ちます。
オーバーライドをするためには、元のクラス(スーパークラス)で、メソッドがオーバーライドされることを許可するvirturlキーワードを追加しておく必要があります。
class Vehicle {
//省略
public virtual void Turn(){
//曲がる処理
}
//省略
}
virturl指定されたプロパティやメソッドは、サブクラス側でoverrideキーワードを付与するだけで、中身を上書きすることができます。
class Bike : Vehicle {
public override void Turn(){
//癖のあるハンドリング処理
}
}
ここで特に注目して欲しい点は、「外側(使う側)から見たらどちらもTurnメソッドである」という点です。
オーバーライドを使用しない設計では、TurnメソッドとBikeTurnメソッドが共存してしまい、使用者側はどちらのメソッドを使えば良いか混乱してしまう可能性がありました。
しかし、オーバーライドを使用した設計では、Turnメソッドしかありませんので、とりあえずTurnメソッドを呼んでおけば、そのクラスに合ったTurnの処理をしてくれます。
つまり、「Carだから通常の曲がる処理を呼ぼう」とか、「Bikeだから特別な曲がる処理を呼ぼう」とか、そんなことを考える必要が一切無くなります。
とても楽ですよね。しかも、意図しないメソッドが呼ばれてしまうことによるバグが発生しにくくなります。
最後に
実は、オーバーライドの本領発揮はまだまだこれからなのですが、ひとまず「継承」と「オーバーライド」の基礎についてご理解いただけましたでしょうか。
本領発揮については、また別記事で書かせていただきますので、ぜひご覧ください。
以上、六条すずやでした。最後までありがとうございました!