ゴール
- 抽象クラスを作成することによる利点がわかる
- Javaによる基本的な書き方を知る
※ 制約事項
- インターフェース・ジェネリクスという概念を知らないものとする
- テストコードなし
- 知っていること(書き方・役割を知っている状態)
- クラス・インスタンス
- コンストラクタ
- メソッド・メンバー変数・クラス変数・不変オブジェクトなど
- アクセスキーワード(修飾子)(public, private, protected)
- スーパークラス・サブクラス
- クラスの継承・ポリモーフィズム。カプセル化
- 配列やJavaの基本的な文法
- クラス・インスタンス
抽象クラスを作ると良いこと
いくつかのクラスに共通するプロパティやメソッドがまとめられる
- 必要なプロパティやメソッドが一箇所にまとめられている状態になる
- 抽象クラスに定義したメソッドを子クラスに具体的な実装を義務付けられるため、全体的なプログラムの一貫性を持たせることができる(Aクラスには実装されているがBクラスには実装がない といった現象が減る)
異なる具体的な振る舞いを実現するための基盤となれる
- 抽象クラスで具体的な実装を持つことがないため、子クラスの方で自由に振る舞いの追加・変更をすることができる
- 共通処理部分に手をつける必要がなくなる
抽象クラスの特徴
- インスタンス化することはできないクラス
- 一部のメソッドまたはすべてのメソッドが具体的な実装を持たない
- 抽象クラスを継承した子クラスは抽象メソッドの具体的な実装が義務付けられる
- 他クラスから必ず継承を受けないといけないクラスであり、それだけでは効果を発揮できない
- 他クラスが継承する元になるクラスとなるため、スーパークラスになることが多い
RPGプログラムでの作成(ルールや世界観の定義)
- ゲームジャンル: ファンタジーRPG
- キャラクターの特徴:
- すべてのキャラクターは、名前(name)、HP(health)、MP(mana)を持つ
- キャラクターは「攻撃する(attack)」アクションをとる
- しかし、キャラクターごとに攻撃方法が異なる(例: 戦士は剣で攻撃、魔法使いは攻撃に魔法を使う)
- 一部のキャラクターは特有の行動(例: 回復魔法を使う)が可能
- 共通ルール:
- すべてのキャラクターには、基本的な能力や行動が共通しているが、詳細な行動はキャラクターによって異なる
共通化ないコード(クラスの継承を利用したコード)
// キャラクター共通クラス
class Character {
protected String name;
protected int health;
protected int mana;
public Character(String name, int health, int mana) {
this.name = name;
this.health = health;
this.mana = mana;
}
public void attack() {
System.out.println(name + " swings a sword!");
}
}
// 戦士クラス
class Warrior extends Character {
public Warrior(String name, int health, int mana) {
super(name, health, mana);
}
@Override
public void attack() {
super.attack();
}
}
// 魔法使いクラス
class Mage extends Character {
public Mage(String name, int health, int mana) {
super(name, health, mana);
}
@Override
public void attack() {
System.out.println(name + " casts a fireball!");
}
}
class Main {
public static void main(String[] args) {
Character warrior = new Warrior("Warrior", 100, 50);
Character mage = new Mage("Mage", 80, 100);
warrior.attack(); // Warrior swings a sword!
mage.attack(); // Mage casts a fireball!
}
}
処理としては、剣士は「剣」での攻撃。魔法は攻撃魔法「ファイアーボール」を利用しているため、それぞれのクラス独自の処理がされている。
何が問題なの?
共通のプロパティやメソッドは一箇所のクラスにまとまっている。継承を利用して、クラス独自の攻撃も実装できている。何が問題だというのか?
実装してほしいメソッドを義務付けられない(オーバーライドを忘れてしまう)
Charater
クラスの攻撃処理のデフォルトメソッドが剣用になっているため、上書き処理を忘れる可能性が出てきてしまう。書かれなかった場合でも、ビルドや動作はできてしまうため、気づける機会が減ってしまう
例: 実装を進める中で「キャラクター」に「弓使い」「回復役」「建築家」など、新たなキャラクターを入れたい という要望があった場合
今までのように Character
クラスを継承して実装を進めていけば、問題ないと思われるが、どこかのクラスで上書き処理を書かれなかった場合に、剣用の攻撃メソッドがよばれてしまう。(動作確認時に気がつけるかもしれないが、知らない既存機能が参照されていたら気が付かない可能性がある)
// 弓使い
class Bowyer extends Character {
public Bowyer(String name, int health, int mana) {
super(name, health, mana);
}
// attack メソッドが未実装だが、エラーにならない
}
共通クラスなのに、剣用のメソッドが入っているため、クラスの役割が曖昧になる
実装した本人やチームであれば、仕様を理解しているため関係ないかもしれないが、他の人が手を加えようとした際、「Character
という共通クラスの攻撃メソッドが剣用になっている。どうしてだろう?」と書かれたコードのを理解しようとしてしまい、実装が遅れてしまう可能性がある。
抽象クラスを利用すると、どうなるのか?
※ 用語の確認
- 抽象クラス(説明を割愛)
- 具象クラス(抽象クラスを継承したクラス)
// 抽象クラス
// abstract キーワードをつけることで抽象クラスとなる
abstract class Character {
protected String name;
protected int health;
protected int mana;
public Character(String name, int health, int mana) {
this.name = name;
this.health = health;
this.mana = mana;
}
// 抽象メソッド: 子クラスに具体的な攻撃方法を委ねる
public abstract void attack();
}
// 戦士クラス(具象クラス)
class Warrior extends Character {
public Warrior(String name, int health, int mana) {
super(name, health, mana);
}
@Override
public void attack() {
System.out.println(name + " swings a sword!");
}
}
// 魔法使いクラス
class Mage extends Character {
public Mage(String name, int health, int mana) {
super(name, health, mana);
}
@Override
public void attack() {
System.out.println(name + " casts a fireball!");
}
}
Charater
クラスの攻撃処理のメソッドを未実装にすることで、子クラスの方に具体的な攻撃方法を処理を任せる形になった
クラス図
抽象クラスを使うことによるメリット
メソッドの実装義務をつけることができる
Charater
を 抽象クラス
にしたことで、攻撃処理のメソッドを未実装にしました。これによって、子クラスの方では具体的な攻撃方法を処理を記載しないとコンパイルエラーになってしまうため、実装を書かせることができるようになりました。
// 戦士クラス
class Warrior extends Character {
public Warrior(String name, int health, int mana) {
super(name, health, mana);
}
// attackメソッドがないため、エラーとなる
}
具体的な処理を子クラスに任せることができる(ポリモーフィズムの実現)
- 抽象クラスにすることで、
Chracter
クラスで処理を固定せずに、子クラスの方で異なる実装を行えるようになった。これにより、キャラクターごとに異なる振る舞いを実現しながら共通化することができた
// 戦士クラス
class Warrior extends Character {
public Warrior(String name, int health, int mana) {
super(name, health, mana);
}
@Override
public void attack() {
System.out.println(name + " swings a sword!");
}
}
// 魔法使いクラス
class Mage extends Character {
public Mage(String name, int health, int mana) {
super(name, health, mana);
}
@Override
public void attack() {
System.out.println(name + " casts a fireball!");
}
}
基盤クラス という役割が明確になる
Character
クラスが「基盤」として共通プロパティと抽象メソッドのみを提供する設計に統一された状態となった。これにより、基盤クラスに手を加えずに新しいキャラクターを簡単に追加できるようになり、既存コードの変更をする必要が減り、既存機能がバグるリスクを軽減できた
// 弓使いクラス
class Archer extends Character {
public Archer(String name, int health, int mana) {
super(name, health, mana);
}
@Override
public void attack() {
System.out.println(name + " shoots an arrow!");
}
}
// 盗賊クラス
class Thief extends Character {
public Thief(String name, int health, int mana) {
super(name, health, mana);
}
@Override
public void attack() {
System.out.println(name + " swiftly strikes from the shadows!");
}
}
まとめ
抽象クラスを作成する利点
- メソッドの実装義務付け: 抽象メソッドにすることで、すべての子クラスで必ず具体的な実装を記述するよう強制できる。さらに、実装漏れによる不具合を未然に防ぐ
- 基盤クラスの役割が明確化: 抽象クラスにすることで、共通プロパティや抽象メソッドを提供する「基盤」としての役割が明確になる
継承の拡張がしやすくなる: 新しいキャラクタータイプを追加する際も、基盤設計に沿って実装を進められるため、コードの一貫性を保ちながら拡張可能