今回は、プログラム言語でのabstractクラスは、こんな風に使うと便利というお話です。
abstractクラスや抽象化については、文法上の解説や使いどころについて解説されたサイトなどは多くありますが、具体的にどのように使えばよいのか少し分かりにくい概念だと思います。知識としては知っていても、実際に使ったことはないという人も多いかもしれません。
この記事では、IT現場やチーム開発での使われ方といった視点から、abstractが具体的にどのように便利なのかを紹介しています。
特に、安全なクラスの設計を心がけるチームの中心的なエンジニアや、複数人で開発を進めるプログラマの人にはイメージしやすいかと思いますので、是非参考にしてみてください。
abstract(抽象)クラスとは
abstractクラスは日本語では抽象(化)クラスと呼ばれ、様々なプログラム言語で利用が可能な修飾子の一つです。
// 通常のクラス定義
class hoge {
}
// 抽象クラスの定義
abstract class hoge {
}
上記例ではhogeというクラスを、通常の方法と抽象化した方法で定義している例です。
プログラム言語によって定義の方法には差がありますので、詳しい文法などは各言語のリファレンスなどによって確認してください。JavaやC#,PHPといったプログラム言語では上記のように定義します。
ここではabstractを指定したクラスには、どのような特徴があるのかを簡単に紹介しています。以降のサンプルコードはPHPで紹介しています。他のプログラム言語を利用する場合は適宜読み替えてください。
インスタンス化できない
abstract指定したクラスは、インスタンス化できなくなります。
abstract class hoge {
}
// ... (中略)
$obj = new hoge(); // エラーとなる
上記例では、hogeという抽象クラスを定義した後、どこかでhogeのインスタンスを生成(new)しようとした場面です。残念ながらこれはエラーとなって実行することはできません。
つまり、abstractを指定するということは、そのクラスは継承して使うことが大前提ということになります。以下に継承の例を見てみましょう。
abstract class hoge {
}
// hogeを継承したクラスhogeExを定義
class hogeEx extends hoge {
}
// ... (中略)
$obj = new hogeEx(); // 派生すればインスタンス化できる
上記例では、抽象クラスhogeを定義した後に、hogeを継承(extends)した通常のクラスhogeExを定義しています。その後、派生先のhogeExのインスタンスを生成しています。これは通常のクラスと同じように、問題なく動作します。
イメージとしては、設計図から実体を生成できる通常のクラスを、abstract指定で一度概念的(抽象的)な物にします。概念からは実体を生成できません。そこから改めて概念を引き継いでクラス(設計図)を起こせば、通常通り実体を生成できるようになるといったイメージです。
継承先で実装を強制される
インスタンスを生成できなくなっただけだと、abstractにはほとんど意味がありません。抽象クラス内では変数(property)や関数(function)に対して、更にabstract修飾子を指定することができます。
もちろんabstract修飾子を付けない通常の変数や関数を定義することも可能で、それらは通常クラスと同じように動作します。
abstract class hoge {
// 通常のクラス関数showを定義
public function show() { /* ... 何かの処理 ... */ }
// 抽象化を指定した関数を定義 (関数の中身はナシ!!)
protected abstract function get_current_status();
}
class hogeEx extends hoge {
}
// ... (中略)
$obj = new hogeEx(); // 派生クラスのインスタンス生成
$obj->show(); // 派生元の関数showを呼び出し
上記例では、抽象クラスhogeで通常関数のshowと、抽象化された関数get_current_statusを定義しています。(後述しますが、上記プログラムでは派生先でabstract関数の実装がないためエラーとなります。)
派生クラスのインスタンスから、通常通りshowは呼び出すことができます。(アクセス修飾子protectedについては詳しく紹介しませんが、派生先からは利用できるけど派生先の外には非公開、とする指定です。publicとprivateの中間的な役割。)
ここで注意すべきなのはabstract functionの定義の行に { 関数の中身 } が存在しない点です。親クラスからすると、処理の実装を棚上げし、派生クラスに実装を任せる形です。上記サンプルをそのまま実行すると、以下のようなエラーが表示されます。
表示されるエラーメッセージ(例) :
'hogeEx' does not implement method 'get_current_status'
意味 : hogeExでは関数(method) get_current_statusが実装(implement)されていません
抽象化クラス内に、抽象化された変数や関数が定義されている場合、派生クラスでは「必ず」該当の関数などを実装する必要があります。
これは、クラス設計者からの「実装を強制させる」という効果といえるでしょう。IT現場などでは、設計が重視される親(基底)クラスを上司が、表面上の実処理である派生クラスを部下が実装するといった担当分けなどでも使える小技でもあります。
派生クラスでの実装は通常通りオーバーライドして行いますが、念のため実装例のサンプルも記載しておきます。
abstract class hoge {
// 通常のクラス関数showを定義
public function show() { /* ... 何かの処理 ... */ }
// 抽象化を指定した関数を定義 (関数の中身はナシ!!)
protected abstract function get_current_status();
}
class hogeEx extends hoge {
// 普通にオーバーライドして関数を定義
protected function get_current_status() {
// ここで関数の実体が初めて定義される
// ( ...中略...)
// 通常の戻り値(return)
// これは派生元の親クラスでも利用可能な値 (次項で解説)
return $child_status;
}
}
親クラスで派生クラスの関数を呼び出し
派生クラスで実装されるだけだと、名前を揃えるくらいしか意味がなさそうに思うかもしれません。
大事なのは、抽象化された関数などは、基底クラス内の処理でも利用可能という点です。基底クラスのコーディング中は、抽象化されていて実体のない関数を利用することになるので、少し変わった感覚になるでしょう。
abstract class hoge {
// 抽象化を指定した関数get_current_statusを定義
// 抽象化を指定しているので、関数の中身を定義することはできない
protected abstract function get_current_status();
// 通常のクラス関数showを定義
public function show() {
// 抽象化された関数を呼び出し - 実際は派生クラスの処理を呼び出し
$status_child = $this->get_current_status();
}
}
サンプルコードが複雑になってきましたが、上記例ではget_current_statusという抽象関数を定義した後、通常の関数showの中で、まだ実体が定義されていないget_current_statusを呼び出しています。
当然このままでは実行することはできないのですが、そもそもabstractを指定したクラスは「インスタンス化できない」ので、そのまま実行されることはなく、派生クラスを定義すると「抽象化指定されたものは実装を強制される」ため、実行時には必ず何らかの処理が存在するという仕掛けです。
派生クラスで実装された結果を$status_childという変数に保持するところまでを例に示していますが、このようにあらかじめ基底クラス側で派生先の処理に応じた処理を実装しておくことが可能というのが、abstractの大きな特徴の一つで、最大のメリットともいえるでしょう。
設計の選択肢の一つにabstractも含めよう
クラスの設計をする上での基底クラスは、共通処理をまとめておいて、派生クラスに「共通の機能を提供し、一元管理する」というのが大きな役割の一つですが、abstractクラスを使うことで、「派生クラスに機能を実装させて基底クラスへ提供」というまったく逆方向の効果を得ることが出来ます。
基底クラス定義時点では未確定だけど、派生先で共通的な処理が発生するため基底クラス側で吸収したい、といった悩みが生まれた際などに、是非abstractの事も思い出してみてください。