オブジェクト指向は、2000年代頃から、またたく間に世に広まった、プログラミング開発手法です。
なかでも「継承」という技術は、拡張性や保守性の決め手となる、画期的な手法として、もてはやされました。
それが今や評価は反転。継承は使わないほうがよい、とさえ言われます。
なぜ、ここまで評価が変わってしまったのでしょう?
現実的に起こる問題をもとに、継承という技術とどう向き合うべきなのか、を考えてみましょう。
「継承」は何をもたらしたのか?
継承は、オブジェクト指向の中でも、最も象徴的な仕掛けです。コードの再利用性や拡張性の向上が図れると、大きな期待が持たれました。
継承という技術が、いったい何をもたらしたのか?具体的な題材で見てみましょう。
実際のサンプルを見てみよう
継承を簡単に説明すると、こうなります。
- 既存のクラス(親クラスまたは基底クラス)の、特性や機能を
- 新しいクラス(子クラスまたは派生クラス)に、引き継ぐ仕組み
この親子関係を定義することで、コード再利用性や拡張性が向上する、と言われます。
実際のコードを見てみましょう。お絵描きアプリをJavaで実装します。
お絵かきアプリ クラス設計
- 基底クラスとして「図形クラス」を定義する。図形は「描画メソッド」を持つ
- 図形クラスを継承して、三角形クラス、正方形クラス、円クラス、の派生クラスを定義する
- 各図形が、描画メソッドをオーバーライドして描画処理を実装する
図形クラスが、継承のもととなる基底クラスです。描画メソッドを持っています。
// 図形クラス
class Shape {
// 描画メソッド (抽象メソッド)
abstract void draw();
}
三角形クラス、正方形クラス、円クラス。図形クラスを継承した、派生クラスです。描画メソッドをオーバーライドします。
// 三角形クラス
class Triangle extends Shape {
@Override
void draw() {
...
}
}
// 正方形クラス
class Square extends Shape {
@Override
void draw() {
...
}
}
// 円クラス
class Circle extends Shape {
@Override
void draw() {
...
}
}
これらのクラスを使う側は、複数の図形に対し、まとめて描画を指示します。
public class Main {
public static void main(String[] args) {
// 図形オブジェクトのコレクションを作成
List<Shape> shapes = new ArrayList<>();
// 三角形、正方形、円を生成してコレクションに格納
shapes.add(new Triangle());
shapes.add(new Square());
shapes.add(new Circle());
// コレクションの各要素について描画メソッドを呼び出し
for (Shape shape : shapes) {
shape.draw();
}
}
}
この仕掛けで、このようなことが実現できます。
- どの図形も、図形クラスの、共通の特徴を持っている
- 使う側は、図形が実際なにかを意識せずに、指示を出せる
三角形や円のオブジェクトが、「図形」という共通の特徴を、引き継いでいるわけですね。
継承がもたらすメリット
この図形クラスの定義がもたらすメリットは、次のとおり。
- 再利用性 三角形や正方形クラスは、図形クラスの動作をそのまま引き継げます
- インターフェース共通化 どの図形でも、図形共通のインターフェースで操作できます
- 拡張性 追加の図形も、容易に図形の一種として追加できます
特に、インターフェースを共通化できる仕組みのことを、多態性(ポリモーフィズム)といいます。
再利用性、多態性、拡張性の向上が、多大な恩恵をもたらすとして、継承という機構が歓迎されたわけです。
継承の利用で、実際に起こる問題
大きな期待が寄せられた、継承という仕掛け。
継承を使ったオブジェクト指向プログラム開発が、次々と行われました。
しかし、いざ開発が進むと、当初は想定していなかった問題が、表面化したのです。
問題1:際限なく膨張する基底クラス
この図形クラス定義を、実装することを考えます。
- システムのドローAPIを使って描画する
- ドローAPIで、初期化処理、ペン作成と開放、終了処理を行う
- 図形には、線の太さや色を指定できる
- 図形は一時的に保存しあとで復元できるようにする
さて、どこのクラスに、どう実装しましょうか?
- ドローAPIはどの図形でも使うよな。じゃあAPIの共通処理は、図形クラスに実装しよう
- 線の太さとか色も、それぞれの図形で共通だな。図形クラスに実装しよう
- 保存や復元の仕掛けも一緒にできるな。図形クラスに…
…なんだか、あらゆる処理が、図形クラスに組み込まれていきます。
でも方針としては、おかしいことは何も言っていない。まあそういうものなのか。
ここで追加仕様の発生です。
さらに「画像クラス」を加えることになりました。図形クラスから継承しましょう。
でも、あれ?
- 画像を描画するのに、ドローAPIとか使わないぞ?
- 線の太さとか色とかも、画像に関係ないぞ?
- 画像の描画は、全然別の方法でやらないといけないぞ?
いまの図形クラスの中身は、画像には殆ど関係ないものばかり。
それでも実装上、図形クラスから継承しないといけない。図形グループに加えて描画する必要あるからね。
しょうがない。
- 画像描画用の専用処理を、図形クラスに加えるか…
- 画像のサイズとかも必要だな。図形クラスに加えるか…
- 保存や復元も、画像専用の処理が必要だな。図形クラスに…
かくして、あらゆる処理が、ごった煮のように、図形クラスに実装されていきます。
図形クラスが、手に負えないほど肥大していくのです。
問題2:哲学的思想で混乱するクラス設計
さらに、追加仕様です。
「四角形」クラスと「楕円」クラスを追加することになりました。継承関係に、追加しましょう。
- 三角形は、図形である
- 正方形は、図形である
- 円は、図形である
- 四角形は、図形である
- 楕円は、図形である
…ここでふと、考えます。
「正方形と四角形」、「円と楕円」、これ継承関係あるんでないの?
- 四角形は、正方形であり、図形である…?
- 楕円は、円であり、図形である…?
いや待てよ。逆なのか?
- 正方形は、四角形であり、図形である…?
- 円は、楕円であり、図形である…?
どっちが正しいんだ?開発メンバー間でも意見が分かれます。けんけんがくがく。
これ実は、円―楕円問題と言われる問題で、理屈上の正解はあります。
ただここで言いたいのは、なにが正解か、ではなく、もっと根底にある問題。
それは、クラスの継承関係を定義することは、かくも難しい、という現実です。
加えて
- このような困難な定義を、最初にしないといけない
- 一度決めると、後戻りが難しい
ということが、混迷に拍車をかけるのです。
継承は使わないべきなのか?
かくして、継承という技術には、様々な問題があることも浮き彫りとなりました。
時を経るにつれ、こんな意見が出始めます。
継承は悪だ!
継承なんて使うな!
でも、当初期待された効果も、なにかしらあるんじゃないか。
継承という技術と、実際どう付き合っていくべきなのか?その根底の考え方を深堀りします。
「継承」の根底にある問題
際限なく膨張する基底クラス。混乱を招くクラス構造。
そういった継承技術の、現実的な問題をまとめると、こうなります。
- 継承関係を定義するのは、とても難しい
- そのような難しい定義を、設計の最初にしないといけない
- そして一回決めたあとは、変えにくい
結局、継承で期待された「再利用性」や「拡張性」といった効果は、現実的にはこうなります。
継承は「事前に想定」していた再利用や拡張に対しては、有効に機能する
その想定がハマれば、継承の効果は絶大です。
ただやっぱり、最初からそうそう、将来のことを読み切れない。これも実情です。
継承を考えるときのガイドライン
継承を使うことは、絶対的な悪とはいえません。でも、むやみやたらに使うのも考え物。
これを踏まえると、現場目線での、現実的な答えは、こうなるでしょう。
それでも継承を使おうと思うのなら…
先人の残した原理原則も参考にして、慎重に使いましょう。
継承を避けて進化するプログラミング言語
継承に期待される、主要な役割が、「多態性(ポリモーフィズム:Polymorphism)」です。
共通インターフェースを持たせようと、継承を使ったわけですが、様々な問題がつきまといます。
近年のプログラミング言語は、継承を使わずにポリモーフィズムを扱える仕掛けが、組み込まれています。
望ましいのは「インターフェースの継承」
Javaの発明者、James Gosling氏。かつての講演会における質疑応答で、このような言葉を残しています。
ある人が尋ねました。“If you could do Java over again, what would you change?” (Java をもう一度やり直せるとしたら、何を変えますか?)
彼は答えます。“I’d leave out classes,”(クラスを省きます)
笑いが収まった後、彼は続けます。
本当の問題はクラスそのものではなく、実装の継承(extends)にあります。インターフェースの継承(implements)が望ましいのです。実装の継承は可能な限り避けるべきです。
継承には、
- 実装の継承
- インターフェースの継承
の2つの側面があります。
このうち、実現したいのは「インターフェースの継承」、つまりポリモーフィズムだけ。基底とか派生とか複雑な構造はいらない。
共通したインターフェースを持っていることを、宣言さえできればいい、というわけです。
Javaだと、interfaceという定義で、このように置き換えられます。
// 図形インターフェース
interface Shape {
// 描画メソッド (抽象メソッド)
void draw();
}
// 三角形クラス
class Triangle implements Shape {
@Override
public void draw() {
...
}
}
// 正方形クラス
class Square implements Shape {
@Override
public void draw() {
...
}
}
// 円クラス
class Circle implements Shape {
@Override
public void draw() {
...
}
}
public class Main {
public static void main(String[] args) {
// 図形オブジェクトのコレクションを作成
List<Shape> shapes = new ArrayList<>();
// 三角形、正方形、円を生成してコレクションに格納
shapes.add(new Triangle());
shapes.add(new Square());
shapes.add(new Circle());
// コレクションの各要素について描画メソッドを呼び出し
for (Shape shape : shapes) {
shape.draw();
}
}
}
Shapeインターフェースは、あくまでメソッド定義だけ。実装を持てません。
実装継承を許容しないことで、問題を回避するのです。
様々な言語でのポリモーフィズム
近年の各種言語は、継承を使わずポリモーフィズムを実現するよう改良されています。
一例を紹介しましょう。
言語 | ポリモーフィズム実現手段 |
---|---|
Java | interface |
C# | interface |
Swift | protocol |
Go | type |
Rust | trait |
特に、GoやRustなど、近年登場した言語では、継承という仕掛け自体を、廃止しています。
プログラミング言語全体が、継承が持つ複雑性を回避し、より簡素化する方向に進んでいると言えるでしょう。
まとめ
オブジェクト指向が普及した当時は、「現実の世界をそのままクラスで表現する」なんて、崇高な理想がありました。
そして、継承という技術も、「世界はどのような構成であるべきか」なんて、壮大な議論がされがちでした。
いまとなっては、そのような神話めいた話には、非現実感が漂います。
いま大事なこと。それは、必要なことを、シンプルに、最小限に実現すること。
世界の有り様なんていう、壮大な問題を考える必要は、ないのですよ。
コメント