抽象クラスとインターフェースの違い
免責
以前、未経験エンジニアさんにPHPによるOOPをお教えするお仕事をしたことがありました。その中で、抽象クラスとインターフェースの違いをテキストで説明したのですが、本稿はそのときのテキストをほぼそのまま掲載したものです。クラスの継承という概念はすでに(おおよそ)理解した上で、抽象クラスとインターフェースの違い、使い分けにフォーカスして解説した文章になります。
初学者向けに、おおまかな理解と納得感を得てもらうことを目的として書いた文章なので、厳密さを欠いている点にはどうぞ目を瞑ってください🙏
また、シニアOOPerに嫌われがちな(?)「犬猫」の例えを使った解説なので、その点もあらかじめご了承ください🐶🐱
では、以下本文です。
抽象クラス
抽象クラスとインターフェース、難しいですよね。自分も初心者の頃この辺りが一番苦手で、本当にさっぱり分かっていませんでした。
抽象クラスとインターフェースはとても似ていて、どちらも
- 通常のクラスと違って、それ単体では「実体」を作れない
- つまり何かしらの「概念」だけをまとめたものである
という共通点があります。
例えば、「犬」「猫」「豚」「牛」という4つのクラスを考えてみましょう。
すべて動物なので、「動物」という基底クラスを継承させることで構造化できそうですね。
しかしよく考えてみると、「犬」「猫」「豚」「牛」という動物はいるけど、「動物」という動物はいませんよね。
つまり、「動物」というのはあくまで「概念」なわけです。
「犬」がどう走るか、どう鳴くか、は定義することができますが、「動物」がどう走るか、どう鳴くか、は(動物によって違うので)定義できません。
これは、「動物」が実体を持てない「概念」だからです。
しかし一方で、すべての動物は必ず「死ぬ」という機能を持っていて、「死ぬ=心臓が停止する」という具体的な処理内容もすべての動物で共通です。(あくまで例なので、生物学的な厳密さは無視してください😅)
そう考えると、「動物」は以下のような抽象クラスとして定義することができそうです。
abstract class 動物 {
走る(); // 具体的な処理の定義はなくて、継承先の派生クラスで実装する
鳴く(); // 具体的な処理の定義はなくて、継承先の派生クラスで実装する
死ぬ() {
心臓を停止する; // このメソッドだけは具体的な処理まで定義する
}
}
class 犬 extends 動物 {
走る() {
100m進む;
}
鳴く() {
「ワン」と発声する;
}
// 死ぬ() は基底クラスで定義されているのでオーバーライドしなくていい
}
class 豚 extends 動物 {
走る() {
10m進む;
}
鳴く() {
「ブー」と発声する;
}
// 死ぬ() は基底クラスで定義されているのでオーバーライドしなくていい
}
こんなイメージですね。
「動物」抽象クラスで
- 走ることができる
- 鳴くことができる
- 死ぬことができて、具体的にこうやって死ぬ
という概念を定義しておき、これを継承した派生クラスで走り方や鳴き方をオーバーライドして具体的に定義する、という感じの使い方です。
インターフェース
インターフェースもこれとすごく似ています。
抽象クラスには概念だけでなく一部具体的な処理も定義しておくことができましたが、インターフェースには概念だけしか定義できません。これが最大の違いです。
「犬」「猫」「豚」「牛」という4つのクラスには「動物である」という共通点がありましたが、視点を変えて、「豚」と「牛」だけに注目すると、「(人が)食べることができる」という共通点もありますよね。
「動物である」という概念に対しては「これをベースに個別の動物を作る」という発想になりましたが、「食べることができる」という概念はそれとはちょっと違うと思いませんか?
「豚」も「牛」も「キャベツ」も「ホタテ」も「パン」も「豆腐」も、すべて「食べることができる」という性質を持っていますが、「何かをベースとした派生形である」ようには思えません。
このように、同じ「概念」でも、抽象クラスのようにクラスのベースにするのではなく、「クラスに『性質』を後付けする」ために使うのがインターフェースです。
この例で言えば、「食べることができる」というインターフェースを定義しておいて、「豚」クラスと「牛」クラスだけはそのインターフェースを「実装」(継承ではなく)する、という使い方をします。
コードのイメージは以下のような感じですね。
// これはさっきと一緒
abstract class 動物 {
走る(); // 具体的な処理の定義はなくて、継承先の派生クラスで実装する
鳴く(); // 具体的な処理の定義はなくて、継承先の派生クラスで実装する
死ぬ() {
心臓を停止する; // このメソッドだけは具体的な処理まで定義する
}
}
// こんな感じでインターフェースを定義しておく
interface 食べることができる {
料理される(); // 具体的な処理はインターフェースを実装したクラスに書く
}
// 犬は食べることができない
class 犬 extends 動物 {
走る() {
100m進む;
}
鳴く() {
「ワン」と発声する;
}
// 死ぬ() はスーパークラスで定義されているのでオーバーライドしなくていい
}
// 豚は食べることができる
class 豚 extends 動物 implements 食べることができる {
走る() {
10m進む;
}
鳴く() {
「ブー」と発声する;
}
// 死ぬ() はスーパークラスで定義されているのでオーバーライドしなくていい
料理される() {
焼き豚になる;
}
}
こんなふうに、「食べることができる」インターフェースを「実装(implements)」することで、『「豚」クラスは「食べることができる」という性質を持っているんだ』ということを明示することができます。
これの何が嬉しいかというと、例えば「料理人」という別のクラスを作るときに、
class 料理人 {
料理する(食べることができる 食材) {
return 食材->料理される();
}
}
という感じで、(読みづらいですが🙏)「食材」という引数の型として「食べることができる」インターフェースを指定することができて、これにより、「食べることができるもの」つまり「料理される()
メソッドを持っているクラス」が引数として渡されることを保証することができます。
そして、「食べることができる」インターフェースを実装しているクラスなら、「豚」だろうが「バッタ」だろうが「カブトムシ」だろうが何でもよくて、何が渡されても料理人は必ず適切に料理することができます(料理する処理の内容は食材自身が知っていて、料理人はそれを実行するだけなので)。
こんなふうに、ある「性質」を変数の「型」として表現したいときに、インターフェースを使います。
まとめ
というわけで、抽象クラスは「ベースとなる概念」、インターフェースは「性質」を定義するものと考えておけばほぼ間違いないです。
このような使い分けであるが故に、抽象クラスのクラス名は「名詞」、インターフェースの名前は「形容詞・形容動詞」が付けられることが多いです。
- 抽象クラスの例
AbstractAnimal
AbstractEngine
AbstractSpeaker
- インターフェースの例
-
EdibleInterface
(「食べることができる」という性質) -
IterableInterface
(「foreachなどに渡して反復処理できる」という性質) -
LoggableInterface
(「ログに出力できる」という性質)
-
例外も多々あります(名詞でもって「性質」を表すようなインターフェースの命名も全然よくあります)し、プログラミング言語や文化圏によっても慣習は様々なので一概には言えませんが、そういう傾向があるということを知っておくとコードを読むときに役立つかもしれません。
Discussion