Java超|抽象クラスとインターフェイスで設計するクラス階層

抽象クラスは戦士の土台を整え、インターフェイスは能力の約束を分ける。強いクラス階層は、共通点と個性をきれいに整理する設計図です。

Javaのプログラムが大きくなってくると、クラスを1つずつ個別に作るだけでは、だんだん整理が難しくなります。

最初は、SuperSaiyanWarrior クラス、NamekianHealer クラス、FlameFighter クラスのように、必要なクラスを順番に作っていけば動くかもしれません。

しかし、クラスが増えてくると、次のような問題が出てきます。

困りやすいこと内容
共通部分が重複する名前や速度など、似たフィールドを何度も書いてしまう
役割が分かりにくくなるどのクラスが何を担当しているのか見えにくい
新しいクラスを追加しにくいどこに組み込めばよいか迷う
同じ処理でまとめにくいクラスごとに別々の扱いが必要になる

このようなときに役立つのが、抽象クラスとインターフェイスを組み合わせたクラス階層の設計です。

ドラゴンボールの世界観でたとえると、戦士たちが増えてきたときに、全員をバラバラに考えるのではなく、次のように整理するイメージです。

整理したいものドラゴンボール風のイメージ
共通の土台戦士なら名前や速度を持つ
共通の能力移動できる、気を使える、支援できる
個別の特徴サイヤ人戦士、ナメック星の治療戦士、炎の気を使う戦士としての個性

抽象クラスは、共通の状態や処理をまとめる土台として使います。
インターフェイスは、何ができるかという能力の約束を整理するために使います。

つまり、クラス階層を設計するとは、単に親子関係を作ることではありません。

共通部分をどこに置き、能力の約束をどこで決め、個性をどのクラスに任せるかを整理することです。

クラス階層を設計するとはどういうことか

クラス階層を設計するとは、似た役割を持つクラス同士の関係を整理することです。

オブジェクト指向では、クラスをただ横に並べるだけではなく、共通するものを上位にまとめ、違う部分を下位のクラスへ分けていきます。

ドラゴンボール風に考えると、次のような整理ができます。

分類役割
共通の土台Warriorすべての戦士に共通する名前や速度を持つ
能力の約束iMovable移動できることを表す
能力の約束iKiUser気を使えることを表す
具体クラスSuperSaiyanWarriorサイヤ人として気を使って戦う
具体クラスFlameFighter炎の気を使って戦う
具体クラスNamekianHealer治療術で仲間を支援する

ここで大切なのは、「何を親クラスにするか」だけではありません。

どの情報は全戦士に共通なのか。
どの能力はクラスをまたいで使う約束なのか。
どの処理は具体クラスごとに変わるのか。

このような役割分担を考えることが、クラス階層の設計です。

図:抽象クラスとインターフェイスで作るクラス階層

この図が示していること

この図では、抽象クラス、インターフェイス、具体クラスの役割分担を表しています。

上部中央の Warrior 抽象クラスは、戦士としての共通の土台です。
name や speed のような共通状態、setSpeed() や showBaseInfo() のような共通処理を持っています。

左上の iMovable は、移動できるという能力の約束です。
右上の iKiUser は、気を使えるという能力の約束です。

下部の SuperSaiyanWarrior、FlameFighter、NamekianHealer は具体クラスです。
それぞれ Warrior を継承し、必要なインターフェイスを実装しています。

要素役割
抽象クラス共通の土台をまとめる
インターフェイス能力の約束を整理する
具体クラス実際の個性と処理を表す

この図から分かることは、抽象クラスとインターフェイスを組み合わせると、共通部分、能力、個性を分けて整理できるということです。

抽象クラスは共通の土台を作るためのもの

抽象クラスは、複数のクラスに共通する状態や処理をまとめるために使います。

ドラゴンボール風に、Warrior という抽象クラスを考えてみます。

戦士なら、共通して次のような情報を持っていそうです。

共通要素内容
name戦士の名前
speed移動速度
setSpeed()速度を設定する処理
showBaseInfo()共通情報を表示する処理

このような「戦士なら共通して持つもの」は、抽象クラスにまとめると整理しやすくなります。

ただし、すべてを抽象クラスで完成させる必要はありません。

たとえば、show() のような情報表示メソッドは、戦士ごとに内容が変わります。

具体クラスshow() で表示したい内容
SuperSaiyanWarriorサイヤ人戦士の名前、速度、気の型
FlameFighter炎の戦士の名前、速度、炎の技
NamekianHealer治療戦士の名前、速度、治療術

このように、共通の枠だけを抽象クラスに置き、具体的な表示内容は子クラスで完成させると、クラスの役割がきれいに分かれます。

抽象クラスは、共通の状態と共通の考え方をまとめる土台だと考えると分かりやすいです。

インターフェイスは能力や約束を整理するためのもの

インターフェイスは、クラスの種類ではなく、何ができるかを表すために使います。

ドラゴンボール風に考えると、戦士たちはそれぞれ違う存在ですが、能力としては共通点を持つことがあります。

能力インターフェイスの例意味
移動できるiMovablemove() を持つ
気を使えるiKiUseruseKi() を持つ
情報を表示できるshow()抽象クラス側で抽象メソッドとして用意
支援できるiSupportablesupport() を持つ

SuperSaiyanWarrior はサイヤ人型の具体クラスです。
NamekianHealer はナメック星の治療戦士という具体クラスです。

この2つは「何者か」で見ると違います。
しかし、どちらも移動できるなら、iMovable を実装できます。

クラス何者か何ができるか
SuperSaiyanWarriorサイヤ人戦士表示できる、移動できる、気を使える
NamekianHealerナメック星の治療戦士表示できる、移動できる、治療できる
FlameFighter炎の気を使う戦士表示できる、移動できる、気を使える

この「何ができるか」を共通ルールとして切り出すのがインターフェイスです。

抽象クラスが「共通の土台」を作るものなら、インターフェイスは「共通の能力の約束」を作るものです。

抽象クラスとインターフェイスを組み合わせると設計しやすい

実際の設計では、抽象クラスかインターフェイスのどちらか一方だけで考えるより、両方を組み合わせたほうが自然なことが多いです。

ドラゴンボール風に整理すると、次のようになります。

役割向いているしくみ
戦士としての共通の土台抽象クラスWarrior
移動できる約束インターフェイスiMovable
気を使える約束インターフェイスiKiUser
支援できる約束インターフェイスiSupportable
実際の戦士具体クラスSuperSaiyanWarrior、FlameFighter、NamekianHealer

このように分けると、役割がはっきりします。

置き場所置く内容
抽象クラス共通の状態、共通の処理、共通の土台
インターフェイス共通の能力、共通の約束、必ず持つメソッド名
具体クラス実際の処理内容、個別の特徴、戦士らしさ

たとえば、Warrior 抽象クラスで名前や速度を持たせます。
iMovable で移動できることを表します。
iKiUser で気を使えることを表します。
SuperSaiyanWarrior や FlameFighter で、実際の戦士の個性を表します。

この形にすると、共通部分、能力の約束、個性が混ざりにくくなります。

ファイル名:Sample6.java

abstract class Warrior
{
    protected String name;
    protected int speed;

    public Warrior(String n)
    {
        name = n;
        speed = 0;
        System.out.println(name + "を戦士として登録しました。");
    }

    public void setSpeed(int s)
    {
        speed = s;
        System.out.println(name + "の速度を" + speed + "にしました。");
    }

    public void showBaseInfo()
    {
        System.out.println("戦士の名前は" + name + "です。");
        System.out.println("速度は" + speed + "です。");
    }

    public abstract void show();
}

interface iMovable
{
    void move();
}

interface iKiUser
{
    void useKi();
}

class SuperSaiyanWarrior extends Warrior implements iMovable, iKiUser
{
    private String kiStyle;

    public SuperSaiyanWarrior(String n, String ks)
    {
        super(n);
        kiStyle = ks;
        System.out.println(name + " スーパーサイヤ人戦士を作成しました。");
    }

    public void move()
    {
        System.out.println(name + "は舞空術で空中を素早く移動します。");
    }

    public void useKi()
    {
        System.out.println(name + "は" + kiStyle + "の気を解放します。");
    }

    public void show()
    {
        showBaseInfo();
        System.out.println("役割はスーパーサイヤ人戦士です。");
        System.out.println("気の型は" + kiStyle + "です。");
    }
}

class NamekianHealer extends Warrior implements iMovable
{
    private String healingSkill;

    public NamekianHealer(String n, String hs)
    {
        super(n);
        healingSkill = hs;
        System.out.println(name + " ナメック星の治療戦士を作成しました。");
    }

    public void move()
    {
        System.out.println(name + "は仲間の近くへ素早く移動します。");
    }

    public void show()
    {
        showBaseInfo();
        System.out.println("役割はナメック星の治療戦士です。");
        System.out.println("治療術は" + healingSkill + "です。");
    }
}

class Sample6
{
    public static void main(String[] args)
    {
        Warrior[] warriors = new Warrior[2];

        warriors[0] = new SuperSaiyanWarrior("蒼月", "蒼天気功");
        warriors[0].setSpeed(80);

        warriors[1] = new NamekianHealer("ネイロン", "光の再生術");
        warriors[1].setSpeed(60);

        for(int i = 0; i < warriors.length; i++){
            warriors[i].show();
        }

        iMovable[] movableMembers = new iMovable[2];

        movableMembers[0] = (iMovable)warriors[0];
        movableMembers[1] = (iMovable)warriors[1];

        for(int i = 0; i < movableMembers.length; i++){
            movableMembers[i].move();
        }
    }
}

実行結果

蒼月を戦士として登録しました。
蒼月 スーパーサイヤ人戦士を作成しました。
蒼月の速度を80にしました。
ネイロンを戦士として登録しました。
ネイロン ナメック星の治療戦士を作成しました。
ネイロンの速度を60にしました。
戦士の名前は蒼月です。
速度は80です。
役割はスーパーサイヤ人戦士です。
気の型は蒼天気功です。
戦士の名前はネイロンです。
速度は60です。
役割はナメック星の治療戦士です。
治療術は光の再生術です。
蒼月は舞空術で空中を素早く移動します。
ネイロンは仲間の近くへ素早く移動します。

Sample6.java で使っている設計

このプログラムでは、抽象クラスとインターフェイスを組み合わせています。

まず、Warrior 抽象クラスがあります。

abstract class Warrior

Warrior は、戦士としての共通の土台です。

Warrior にあるもの役割
name戦士の名前
speed戦士の速度
setSpeed()速度を設定する共通処理
showBaseInfo()名前と速度を表示する共通処理
show()子クラスで完成させる抽象メソッド

つまり、Warrior はすべての戦士に共通する情報をまとめています。

一方で、show() は abstract になっています。

public abstract void show();

これは、戦士なら自分の情報を表示できるべきだけれど、具体的な表示内容は戦士ごとに違うためです。

SuperSaiyanWarrior なら気の型を表示します。
NamekianHealer なら治療術を表示します。

このように、共通の土台は抽象クラスにまとめ、個別の表示内容は具体クラスに任せています。

インターフェイスで能力を分けている

このプログラムでは、iMovable と iKiUser という2つのインターフェイスも使っています。

interface iMovable
{
    void move();
}

iMovable は、移動できることを表す約束です。

interface iKiUser
{
    void useKi();
}

iKiUser は、気を使えることを表す約束です。

ここで大事なのは、どちらも「何者か」ではなく「何ができるか」を表している点です。

インターフェイス表している能力
iMovable移動できる
iKiUser気を使える

SuperSaiyanWarrior は、Warrior を継承しながら、iMovable と iKiUser を実装しています。

class SuperSaiyanWarrior extends Warrior implements iMovable, iKiUser

これは、SuperSaiyanWarrior が戦士としての共通土台を持ち、さらに移動できる能力と気を使える能力を持つことを表しています。

一方、NamekianHealer は iMovable だけを実装しています。

class NamekianHealer extends Warrior implements iMovable

これは、NamekianHealer が戦士としての共通土台を持ち、移動できる能力も持つことを表しています。

クラス抽象クラスの土台実装する能力
SuperSaiyanWarriorWarrioriMovable、iKiUser
NamekianHealerWarrioriMovable

このように、抽象クラスで土台を作り、インターフェイスで能力を追加する形にすると、設計がかなり見やすくなります。

図:共通の型でまとめて扱う設計

この図が示していること

この図では、抽象クラス型とインターフェイス型で、異なる具体クラスをまとめて扱う様子を表しています。

左側の Warrior[] warriors では、SuperSaiyanWarrior と NamekianHealer を戦士としてまとめています。
そのため、共通して show() を呼び出せます。

右側の iMovable[] movableMembers では、移動できるクラスをまとめています。
そのため、共通して move() を呼び出せます。

まとめ方呼び出せるメソッド見方
戦士としてまとめるWarrior[]show()共通の土台で見る
移動できる者としてまとめるiMovable[]move()共通の能力で見る

この図から分かることは、抽象クラス型では「何者か」でまとめ、インターフェイス型では「何ができるか」でまとめられるということです。

多くのクラスをまとめて扱えることが大きな利点

抽象クラスやインターフェイスを使う大きな利点は、複数のクラスを共通の型でまとめて扱えることです。

Sample6.java では、Warrior[] 配列に SuperSaiyanWarrior と NamekianHealer を入れています。

Warrior[] warriors = new Warrior[2];

warriors[0] = new SuperSaiyanWarrior("蒼月", "蒼天気功");
warriors[1] = new NamekianHealer("ネイロン", "光の再生術");

SuperSaiyanWarrior も NamekianHealer も Warrior を継承しているため、Warrior[] にまとめられます。

配列要素変数としての型実体
warriors[0]WarriorSuperSaiyanWarrior
warriors[1]WarriorNamekianHealer

そして、次のように同じ show() を呼び出せます。

for(int i = 0; i < warriors.length; i++){
    warriors[i].show();
}

呼び出し方は同じです。
しかし、実際に動く show() は、実体ごとに変わります。

実体動く show()
SuperSaiyanWarriorスーパーサイヤ人戦士としての情報を表示する
NamekianHealerナメック星の治療戦士としての情報を表示する

これが、抽象クラスを使った多態性です。

さらに、iMovable[] 配列も使っています。

iMovable[] movableMembers = new iMovable[2];

movableMembers[0] = (iMovable)warriors[0];
movableMembers[1] = (iMovable)warriors[1];

iMovable を実装しているクラスなら、クラスの種類が違っていても同じ配列に入れられます。

配列要素実体共通して使えるメソッド
movableMembers[0]SuperSaiyanWarriormove()
movableMembers[1]NamekianHealermove()

このように、抽象クラスでは「戦士としてまとめる」、インターフェイスでは「移動できる者としてまとめる」という使い方ができます。

新しいクラスの追加や差し替えがしやすくなる

クラス階層をきちんと設計しておくと、新しいクラスを追加しやすくなります。

たとえば、あとから FlameFighter クラスを追加したいとします。

FlameFighter が Warrior を継承し、iMovable と iKiUser を実装するなら、既存の設計に自然に組み込めます。

class FlameFighter extends Warrior implements iMovable, iKiUser
{
    private String flameTechnique;

    public FlameFighter(String n, String ft)
    {
        super(n);
        flameTechnique = ft;
        System.out.println(name + " 炎の気を使う戦士を作成しました。");
    }

    public void move()
    {
        System.out.println(name + "は炎の勢いで前線へ踏み込みます。");
    }

    public void useKi()
    {
        System.out.println(name + "は" + flameTechnique + "を放ちます。");
    }

    public void show()
    {
        showBaseInfo();
        System.out.println("役割は炎の気を使う戦士です。");
        System.out.println("炎の技は" + flameTechnique + "です。");
    }
}

この FlameFighter は、既存の設計にそのまま乗せられます。

追加クラス継承する抽象クラス実装するインターフェイス
FlameFighterWarrioriMovable、iKiUser

このようにしておけば、使う側のコードは「このクラス名は何か」よりも、「Warrior として扱えるか」「iMovable として扱えるか」を見ればよくなります。

つまり、クラス階層を整えておくと、追加や差し替えがしやすくなります。

インターフェイスは多重継承の考え方も支える

Javaでは、クラスの多重継承はできません。

つまり、1つのクラスが複数の親クラスを同時に継承することはできません。

class SuperSaiyanWarrior extends Warrior, KiMaster
{
}

これはできません。

しかし、インターフェイスなら複数実装できます。

class SuperSaiyanWarrior extends Warrior implements iMovable, iKiUser

これは、SuperSaiyanWarrior が Warrior という1つの土台を持ちながら、iMovable と iKiUser という複数の能力の約束を持つことを表しています。

しくみJavaでの扱い
クラスの多重継承できない
インターフェイスの複数実装できる
共通の土台抽象クラスで表す
複数の能力インターフェイスで表す

ドラゴンボール風にたとえると、1人の戦士が複数の血筋や所属を同時に親として持つのではありません。

その代わりに、「移動できる」「気を使える」「支援できる」といった複数の能力札を持つイメージです。

この考え方によって、クラス構造をシンプルに保ちながら、能力は柔軟に追加できます。

図:新しいクラスを既存の階層へ追加する

この図が示していること

この図では、既存のクラス階層に FlameFighter という新しいクラスを追加する流れを表しています。

Warrior 抽象クラスが共通の土台を持ち、iMovable と iKiUser が能力の約束を表しています。

FlameFighter は、その既存のルールに合わせて、Warrior を継承し、iMovable と iKiUser を実装します。

追加クラス土台能力
FlameFighterWarrioriMovable、iKiUser

この図から分かることは、抽象クラスとインターフェイスで役割を分けておくと、新しいクラスを追加するときも既存の構造に組み込みやすいということです。

標準クラスライブラリを見るときにも階層を意識する

Javaの標準クラスライブラリにも、クラス階層があります。

自分でクラスを作るときだけでなく、標準クラスを使うときにも、次のような視点で見ると理解しやすくなります。

確認したいこと意味
どのクラスを継承しているかどんな土台を持つか
どのインターフェイスを実装しているかどんな能力や約束を持つか
どのメソッドをオーバーライドしているかどんな振る舞いを自分用に変えているか

ドラゴンボール風にたとえると、新しい戦士が登場したときに、どの種族の系譜に属しているのか、どんな能力札を持っているのかを確認するようなものです。

この見方ができるようになると、自分でクラスを設計するときにもかなり役立ちます。

ドラゴンボール風にクラス階層の設計を整理する

ドラゴンボール風に考えると、クラス階層の設計は、戦士たちの世界を整理する作業です。

戦士たちは、それぞれ違う個性を持っています。

具体クラス個性
SuperSaiyanWarrior気を解放して戦う
FlameFighter炎の気を使う
NamekianHealer仲間を治療する

しかし、全員に共通する土台もあります。

共通の土台内容
name名前
speed速度
showBaseInfo()基本情報の表示

さらに、クラスをまたいで共通する能力もあります。

能力の約束内容
iMovable移動できる
iKiUser気を使える
iSupportable支援できる

このように整理すると、抽象クラスとインターフェイスの役割がはっきりします。

設計要素ドラゴンボール風のイメージ
抽象クラス戦士としての共通の土台
インターフェイス能力の約束
具体クラス実際の戦士
クラス階層共通ルール、能力、個性を整理した構造

抽象クラスとインターフェイスを使い分けることで、クラスが増えても整理された構造を保ちやすくなります。

この内容で押さえておきたいポイント

ポイント内容
クラス階層の設計共通部分、能力、個性をどこに置くか整理すること
抽象クラス共通の状態や処理をまとめる土台
インターフェイス何ができるかという能力の約束
具体クラス実際の処理内容と個別の特徴を表す
抽象クラス型複数の具体クラスを共通の土台でまとめられる
インターフェイス型複数の具体クラスを共通の能力でまとめられる
追加しやすさ新しいクラスを既存の階層に組み込みやすくなる
多重継承の考え方クラスの親は1つ、インターフェイスは複数実装できる

抽象クラスとインターフェイスは、どちらも大きなプログラムを整理するための設計道具です。

抽象クラスは、戦士としての共通の土台を作ります。
インターフェイスは、移動できる、気を使える、支援できるといった能力の約束を作ります。
具体クラスは、それらを組み合わせて、それぞれの戦士らしさを表します。

この役割分担が見えると、クラス階層はただの親子関係ではなく、プログラム全体を見通しよく組み立てるための設計図として理解できるようになります。