はじめに
抽象クラスの意味・定義の仕方は知っているけど、どんなときに使うのか・どんな良いことがあるのか。
同じ要件を抽象クラスを使って実装したプログラムと使わずに実装したプログラムを比較して確認してみます。
JAVAプログラム
プリンターの種類としてはインクジェットとレーザーがあるとします。それらを複数管理し、管理しているプリンターの情報を一覧表示するプログラムを作ります。
抽象クラスを使わないプログラム
InkjetPrinterクラスとLaserPrinterクラスがあるだけです。
それぞれモデル名(mModelName)と用紙残数(mPaper)をメンバ変数として持っています。また、プリンター情報を返してくれるgetPrinterInfo()関数を備えています。
違いは、InkjetPrinterはインク残量(mRemainingInk)を持っていて、LaserPrinterはトナー残量(mRemainingToner)を持っています。
抽象クラスを使ったプログラム
InkjetPrinterクラスとLaserPrinterクラスの共通部分(メンバ変数モデル名と用紙残量、プリンター情報を返してくれる関数)を汎化して抽象クラスAbstractPrinterを作りました。
InkjetPrinterクラスとLaserPrinterクラスは、AbstractPrinterを継承しています。
「抽象化」という観点で説明すると、InkjetPrinterクラスとLaserPrinterクラスをモデル名と用紙残量を持っていてプリンター情報を返すというプリンターとして抽象化した、となります。
実行結果
どちらも同じ結果が出力されます。
[インクジェット]Epson P1 印刷可能枚数:10
[インクジェット]Canon C1 印刷可能枚数:20
[レーザー]Xerox X1 印刷可能枚数:100
[レーザー]Richo R1 印刷可能枚数:50
[レーザー]Richo R2 印刷可能枚数:200
何が違うのか
2点あげられます。
まずはクラス構成。独立したクラスと抽象化したクラスです。抽象クラスの概念はオブジェクト指向を解説した書籍やサイトで詳しく解説されているのでここでは省きます。
もう一点は、管理している側からの使い方です。こちらについて詳しく見ていきます。
抽象クラスを使わない場合
public static void main(String[] args) throws Exception {
// 管理するインクジェットプリンターを登録する。
InkjetPrinter[] inkjets = {
new InkjetPrinter("Epson P1", 10),
new InkjetPrinter("Canon C1", 20),
};
// 管理するレーザプリンターを登録する。
LaserPrinter[] lasers = {
new LaserPrinter("Xerox X1", 100),
new LaserPrinter("Richo R1", 50),
new LaserPrinter("Richo R2", 200),
};
// 管理しているプリンターを一覧表示する。
// まずは、インクジェットプリンター。
for(int i = 0; i < inkjets.length; i++ ){
System.out.println(inkjets[i].getPrinterInfo());
}
// 次に、レーザープリンター。
for(int i = 0; i < lasers.length; i++ ){
System.out.println(lasers[i].getPrinterInfo());
}
}
InkjetPrinterクラスとLaserPrinterクラスとの間にはなんの関係性もないそれぞれ独立した「型」です。
InkjetPrinterクラスのオブジェクトはInkjetPrinter型の変数にしか格納できないし、LaserPrinterクラスのオブジェクトはLaserPrinter型の変数にしか格納できません。
よって、プリンターの種類ごとの型の変数(この場合は配列)を用意してプリンターオブジェクトを格納し、それぞれループさせてプリンター一覧を出力しています。
抽象クラスを使った場合
public static void main(String[] args) throws Exception {
// 管理するプリンターを登録する。
AbstractPrinter[] printers = {
new InkjetPrinter("Epson P1", 10),
new InkjetPrinter("Canon C1", 20),
new LaserPrinter("Xerox X1", 100),
new LaserPrinter("Richo R1", 50),
new LaserPrinter("Richo R2", 200),
};
// 管理しているプリンターを一覧表示する。
for(int i = 0; i < printers.length; i++ ){
System.out.println(printers[i].getPrinterInfo());
}
}
InkjetPrinterクラスとLaserPrinterクラスは「プリンター」を抽象化したクラス「AbstractPrinter」から派生しています。AbstractPrinterクラスが共通の親クラスとなっています。
変数はその型のオブジェクトとその型から派生したオブジェクトを格納することができます。
よって、AbstractPrinter型の変数には、InkjetPrinterクラスのオブジェクトとLaserPrinterクラスのオブジェクトを格納することができ、この変数に入っているオブジェクトをAbstractPrinterクラスとして扱うことができます。
これにより、プリンターを管理する変数がひとつになり、表示する処理もひとつになりました。
どんな良いことがあるのか
抽象クラスを導入することで管理する変数と表示処理がスマートになりました。
これだけでも良いことなのですが、機能拡張などの「変更に強くなる」というとても良いことがあります。
プリンターの種類にドットインパクトプリンターを追加して欲しいという改造要件が来たとしましょう。
抽象クラスを使わない場合は、以下のソースとなります。
「★追加★」とコメントした処理が追加した行です。
抽象クラスを使わない場合
public static void main(String[] args) throws Exception {
// 管理するインクジェットプリンターを登録する。
InkjetPrinter[] inkjets = {
new InkjetPrinter("Epson P1", 10),
new InkjetPrinter("Canon C1", 20),
};
// 管理するレーザプリンターを登録する。
LaserPrinter[] lasers = {
new LaserPrinter("Xerox X1", 100),
new LaserPrinter("Richo R1", 50),
new LaserPrinter("Richo R2", 200),
};
// ★追加★
// 管理するドットインパクトプリンターを登録する。
DotimpactPrinter[] dotimpacts = {
new DotimpactPrinter("NEC N1", 100),
new DotimpactPrinter("Oki O1", 50),
};
// 管理しているプリンターを一覧表示する。
// まずは、インクジェットプリンター。
for(int i = 0; i < inkjets.length; i++ ){
System.out.println(inkjets[i].getPrinterInfo());
}
// 次に、レーザープリンター。
for(int i = 0; i < lasers.length; i++ ){
System.out.println(lasers[i].getPrinterInfo());
}
// ★追加★
// 次に、ドットインパクトプリンター。
for(int i = 0; i < dotimpacts.length; i++ ){
System.out.println(dotimpacts[i].getPrinterInfo());
}
}
全ソース:https://paiza.io/projects/VL2hoWh-eWKiHF0lha_rXg
抽象クラスを使った場合
public static void main(String[] args) throws Exception {
// 管理するプリンターを登録する。
AbstractPrinter[] printers = {
new InkjetPrinter("Epson P1", 10),
new InkjetPrinter("Canon C1", 20),
new LaserPrinter("Xerox X1", 100),
new LaserPrinter("Richo R1", 50),
new LaserPrinter("Richo R2", 200),
new DotimpactPrinter("NEC N1", 100), // ★追加★
new DotimpactPrinter("Oki O1", 50), // ★追加★
};
// 管理しているプリンターを一覧表示する。
for(int i = 0; i < printers.length; i++ ){
System.out.println(printers[i].getPrinterInfo());
}
}
全ソース:https://paiza.io/projects/UHysfz9Nx6LPENUOuTnY6w
抽象クラスを使わない方は、ドットインパクトプリンターを管理する変数とそれを表示するループ処理を追加しました。
似たような処理が増えましたね。冗長です。
一方、抽象クラスを使った方はプリンターを管理するprinters変数にドットインパクトプリンターのオブジェクトを追加するだけ済み、表示する処理に変更は入りませんでした。プログラムの改造量が少なくて済みますね。
#まとめ
実はこの利点は抽象クラスの効果というよりも、クラスの汎化・継承による利点です。
抽象クラスをつかうということは、抽象化+汎化・継承するということなので、汎化・継承の恩恵を受けられるわけです。