
継承という考え方は、オブジェクト指向で登場する重要な考え方のひとつです。
手続きや関数を中心にプログラムを整理する構造化プログラミングでは、あまり意識する機会がないため、最初は少しなじみにくいかもしれません。
継承では、「親クラス」と「子クラス」という関係を使って、親クラスに共通する情報や処理をまとめます。
この記事では、RPGに登場する「勇者」と「スライム」を例にして、Javaの継承の使用例を解説します。
最初から完璧な親クラスを作るのではなく、プログラムを書いていく中で、
「これは勇者にもスライムにも必要だな」
「同じ処理を2回書いているな」
「それなら親クラスにまとめた方がよさそうだな」
と気づいたタイミングで整理していきます。
前回までの状態
前回の記事では、RPGに登場するキャラクターを表すために、親クラスとして Chara クラスを作りました。
Chara クラスには、キャラクターに共通する情報として name と hp を持たせています。
public class Chara {
String name;
int hp;
}
なお、Javaには java.lang.Character という標準クラスがあります。
そのため、このシリーズでは混同を避けるために、RPGのキャラクターを表すクラス名を Character ではなく Chara としています。
そして、勇者を表す Hero クラスと、スライムを表す Slime クラスは、この Chara クラスを継承する形にしました。
public class Hero extends Chara {
}
public class Slime extends Chara {
}
Hero も Slime も、どちらもキャラクターの一種です。
そのため、名前やHPのような共通する情報は、親クラスである Chara にまとめています。
今回はこの続きとして、攻撃する処理やダメージを受ける処理を、どのクラスに書くべきか考えていきます。
継承は「共通するもの」が見えてきたときに使いやすい
継承というと、最初から親クラスをきれいに設計しなければいけないように感じるかもしれません。
しかし、最初から完璧なクラス設計を考えるのは難しいです。
RPGを作る場合でも、最初から勇者、スライム、武器、魔法、アイテム、レベルアップなどをすべて正確に設計するのは大変です。
そこで、まずは必要になったものから作っていきます。
そして、複数のクラスに同じような情報や処理が出てきたら、
これは親クラスにまとめた方がよさそうだ
と考えます。
これが、継承を理解するうえで大切な感覚です。
今回も、最初から Chara クラスにすべてを入れるのではなく、勇者とスライムを作っていく中で、共通する処理を親クラスに移していきます。
まずはHeroクラスに攻撃処理を追加してみる
RPGでは、勇者が敵を攻撃します。
そこで、まずは Hero クラスに attack() メソッドを追加してみます。
public class Hero extends Chara {
public void attack() {
System.out.println(name + "の攻撃!");
}
}
attack() は、勇者が攻撃する動作を表すメソッドです。
ここで使っている name は、Hero クラスの中には書かれていません。
System.out.println(name + "の攻撃!");
Hero は Chara を継承しているので、Chara にある name を参照できます。
このように、子クラスでは親クラスから受け継いだフィールドを利用できます。
ここまでは自然です。
勇者が攻撃するので、Hero クラスに attack() を追加しました。
Slimeにも攻撃処理が必要になる
しかし、RPGでは攻撃するのは勇者だけではありません。
スライムも勇者に攻撃してきます。
そこで、Slime クラスにも attack() メソッドを追加してみます。
public class Slime extends Chara {
public void attack() {
System.out.println(name + "の攻撃!");
}
}
これで、スライムも攻撃できるようになりました。
しかし、ここで Hero クラスと Slime クラスを見比べると、同じ処理を書いていることに気づきます。
public void attack() {
System.out.println(name + "の攻撃!");
}
勇者にもスライムにも、まったく同じ attack() メソッドがあります。
このように、複数のクラスに同じ処理が出てきたら、共通化を考えるタイミングです。
attack()は親クラスに移動した方が自然
attack() は勇者だけの処理でしょうか。
そうではありません。
スライムも攻撃します。
ゴブリンも攻撃するかもしれません。
ドラゴンも攻撃するかもしれません。
つまり、attack() は勇者だけの機能ではなく、キャラクター共通の機能と考えられます。
そこで、attack() メソッドを親クラスである Chara に移動します。
public class Chara {
String name;
int hp;
public void attack() {
System.out.println(name + "の攻撃!");
}
}
すると、Hero クラスと Slime クラスは次のようにシンプルになります。
public class Hero extends Chara {
}
public class Slime extends Chara {
}
Hero と Slime の中には attack() を書いていません。
それでも、どちらも Chara を継承しているので、attack() を使うことができます。
これが、Javaの継承の基本的な使用例のひとつです。
共通する処理を親クラスにまとめることで、子クラスに同じコードを何度も書かなくて済みます。
ダメージを受ける処理も考えてみる
攻撃する処理があるなら、ダメージを受ける処理も必要になります。
まずは、勇者がスライムに攻撃すると、スライムがダメージを受けるます。
その処理を Slime クラスに書いてみます。
public class Slime extends Chara {
public void damage(int point) {
hp -= point;
if (hp < 0) {
hp = 0;
}
}
}
damage() メソッドは、引数で受け取ったダメージ量を hp から引く処理です。
hp -= point;
ただし、そのままだとHPがマイナスになる可能性があります。
そこで、HPが0より小さくなった場合は、0に戻しています。
if (hp < 0) {
hp = 0;
}
これで、スライムが大きなダメージを受けても、HPがマイナスにならないようにできます。
勇者にもdamage()が必要になる
しかし、ダメージを受けるのはスライムだけではありません。
勇者もスライムから攻撃されれば、ダメージを受けます。
そこで、Hero クラスにも damage() メソッドを書いてみます。
public class Hero extends Chara {
public void damage(int point) {
hp -= point;
if (hp < 0) {
hp = 0;
}
}
}
これも Slime クラスに書いたものと同じです。
勇者もスライムも、HPを持っています。
そして、ダメージを受けるとHPが減ります。
この処理も、勇者だけのものではなく、キャラクター共通の処理と考えられます。
damage()も親クラスに追加する
damage() もキャラクター共通の処理なので、親クラスである Chara に移動します。
public class Chara {
String name;
int hp;
public void attack() {
System.out.println(name + "の攻撃!");
}
public void damage(int point) {
hp -= point;
if (hp < 0) {
hp = 0;
}
}
}
これで、Hero も Slime も、Chara から attack() と damage() を受け継ぐことができます。
public class Hero extends Chara {
}
public class Slime extends Chara {
}
子クラスに何も書いていないので、不安に感じるかもしれません。
しかし、Hero も Slime も Chara を継承しています。
そのため、Chara にある name、hp、attack()、damage() を利用できます。
必要になったものだけを親クラスに追加する
ここで大事なのは、最初から何でも親クラスに入れないことです。
RPGには、他にもいろいろな処理が考えられます。
たとえば、次のようなメソッドです。
public void recover() {
}
public void useMagic() {
}
public void useItem() {
}
public void levelUp() {
}
RPGらしく考えると、どれも必要になりそうです。
しかし、まだ回復、魔法、アイテム、レベルアップの仕組みを作っていないなら、今は追加しなくて構いません。
「いつか使うかもしれない」と思って先に作ってしまうと、親クラスがどんどん大きくなります。
親クラスが大きくなりすぎると、子クラスにとって不要な機能まで受け継ぐことになります。
その結果、クラスの役割が分かりにくくなります。
そのため、今回は実際に必要になった attack() と damage() だけを Chara クラスに追加しました。
必要になったら追加する。
共通することが分かったら親クラスにまとめる。
この流れで進めると、クラス設計が分かりやすくなります。
継承はコードの重複を減らすためだけではない
ここまでの説明を見ると、継承は同じコードをまとめるための仕組みに見えるかもしれません。
たしかに、コードの重複を減らせることは継承のメリットです。
しかし、継承の意味はそれだけではありません。
大事なのは、
HeroもSlimeも、RPGに登場するキャラクターの一種である
という関係を表せることです。
勇者はキャラクターです。
スライムもキャラクターです。
このように「AはBの一種である」と言える関係では、継承を使うと自然に表現できます。
今のクラス図で整理する
ここまでで作ったクラスを、簡単なクラス図で整理してみます。
+----------------+
| Chara |
+----------------+
| name |
| hp |
+----------------+
| attack() |
| damage(point) |
+----------------+
▲
|
+-------------------------+
| |
+----------------+ +----------------+
| Hero | | Slime |
+----------------+ +----------------+
| | | |
+----------------+ +----------------+
| | | |
+----------------+ +----------------+
Chara クラスには、キャラクターに共通する情報と処理があります。
- name
- hp
- attack()
- damage(point)
Hero と Slime は Chara を継承しています。
そのため、Hero と Slime の中に同じフィールドやメソッドを書かなくても、Chara にあるものを使えます。
このように図で整理すると、何を親クラスに置き、何を子クラスに置くのかが分かりやすくなります。
親クラスに入れるか、子クラスに入れるかの考え方
メソッドやフィールドを追加するときは、次のように考えると整理しやすくなります。
勇者にもスライムにも必要なら、親クラスである Chara に書きます。
勇者だけに必要な動作が出てきたら、Hero クラスに書きます。
たとえば、将来、プレイヤーが操作する勇者だけに「調べる」という行動が必要になった場合です。
public class Hero extends Chara {
public void search() {
System.out.println(name + "はあたりを調べた。");
}
}
ただし、この search() も、必要になってから追加すれば十分です。
まだ「調べる」という仕様がないので今は作りません。
スライムについても同じです。
スライムだけに必要な動作が出てきたら、そのときに Slime クラスへ追加します。
今はスライム専用の動作が必要ないなら、Slime クラスは空のままで構いません。
public class Slime extends Chara {
}
このように考えると、どのクラスに何を書くべきか判断しやすくなります。
大事なのは、最初から全部を決めようとしないことです。
必要になったものを追加し、共通することが分かったら親クラスに移す。
この流れで少しずつ整理していけば大丈夫です。
まとめ:継承は共通する情報と処理を親クラスにまとめるときに使う
今回は、RPGの勇者とスライムを例にして、Javaの継承の使用例を見てきました。
最初は、勇者に attack() を追加しました。
しかし、スライムにも同じ attack() が必要になりました。
そこで、attack() は勇者だけの処理ではなく、キャラクター共通の処理だと考え、親クラスである Chara に移動しました。
同じように、damage() も勇者とスライムの両方に必要だったので、Chara に追加しました。
最終的に、Chara クラスは次のようになりました。
public class Chara {
String name;
int hp;
public void attack() {
System.out.println(name + "の攻撃!");
}
public void damage(int point) {
hp -= point;
if (hp < 0) {
hp = 0;
}
}
}
Hero と Slime は、この Chara クラスを継承します。
public class Hero extends Chara {
}
public class Slime extends Chara {
}
継承は、最初から完璧な親クラスを作るためのものではありません。
プログラムを書いていく中で、
複数のクラスに同じ情報がある
複数のクラスに同じ処理がある
それらが同じ種類のものとして考えられる
と分かったときに、親クラスにまとめると自然です。
今回の例では、勇者もスライムもキャラクターの一種です。
そのため、Hero と Slime が Chara を継承するのは自然です。
まずは必要なものだけを作る。
同じものが出てきたら共通化する。
共通する情報や処理は親クラスにまとめる。
この流れで考えると、Javaの継承はかなり理解しやすくなります。
次回は、Java Swingを使って、RPGの画面を作っていきます。
`JFrame`、`JLabel`、`ImageIcon` を使い、勇者の画像を画面に表示するところから始めます。
