【実際どうなの?】オブジェクト指向の”継承”は悪なのか?

is-inheritance-evil

オブジェクト指向は、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:哲学的思想で混乱するクラス設計

さらに、追加仕様です。

「四角形」クラスと「楕円」クラスを追加することになりました。継承関係に、追加しましょう。

  • 三角形は、図形である
  • 正方形は、図形である
  • 円は、図形である
  • 四角形は、図形である
  • 楕円は、図形である

…ここでふと、考えます。

正方形四角形「円と楕円、これ継承関係あるんでないの?

  • 四角形は、正方形であり、図形である…?
  • 楕円は、円であり、図形である…?

いや待てよ。逆なのか?

  • 正方形は、四角形であり、図形である…?
  • 円は、楕円であり、図形である…?

どっちが正しいんだ?開発メンバー間でも意見が分かれます。けんけんがくがく。

これ実は、円―楕円問題と言われる問題で、理屈上の正解はあります。

ただここで言いたいのは、なにが正解か、ではなく、もっと根底にある問題。

それは、クラスの継承関係を定義することは、かくも難しい、という現実です。

加えて

  • このような困難な定義を、最初にしないといけない
  • 一度決めると、後戻りが難しい

ということが、混迷に拍車をかけるのです。

問題:設計の根幹となる「継承関係」を、最初に定義しないといけない(そして後戻りが難しい)

継承は使わないべきなのか?

かくして、継承という技術には、様々な問題があることも浮き彫りとなりました。

時を経るにつれ、こんな意見が出始めます。

継承は悪だ!

継承なんて使うな!

でも、当初期待された効果も、なにかしらあるんじゃないか。

継承という技術と、実際どう付き合っていくべきなのか?その根底の考え方を深堀りします。

「継承」の根底にある問題

際限なく膨張する基底クラス。混乱を招くクラス構造。

そういった継承技術の、現実的な問題をまとめると、こうなります。

  • 継承関係を定義するのは、とても難しい
  • そのような難しい定義を、設計の最初にしないといけない
  • そして一回決めたあとは、変えにくい

結局、継承で期待された「再利用性」「拡張性」といった効果は、現実的にはこうなります。

継承は「事前に想定」していた再利用や拡張に対しては、有効に機能する

その想定がハマれば、継承の効果は絶大です。

ただやっぱり、最初からそうそう、将来のことを読み切れない。これも実情です。

継承を考えるときのガイドライン

継承を使うことは、絶対的な悪とはいえません。でも、むやみやたらに使うのも考え物。

これを踏まえると、現場目線での、現実的な答えは、こうなるでしょう。

再利用や拡張の用途が、限定できるのなら、継承は有効な手段たりうる

そうでなければ、継承はできれば使わないほうがいい

それでも継承を使おうと思うのなら…

先人の残した原理原則も参考にして、慎重に使いましょう。

継承を使うときのチェック項目
  • SOLID原則に違反していませんか? オブジェクト指向を適切に使うための指針です
  • コード再利用のためだけに継承を使っていませんか? 基本は「継承より委譲」です
  • 多態性を他の手段で実現できませんか? Javaのインターフェースなど、継承よりも安全な手段があります

継承を避けて進化するプログラミング言語

継承に期待される、主要な役割が、「多態性(ポリモーフィズム: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インターフェースは、あくまでメソッド定義だけ。実装を持てません。

実装継承を許容しないことで、問題を回避するのです。

様々な言語でのポリモーフィズム

近年の各種言語は、継承を使わずポリモーフィズムを実現するよう改良されています。

一例を紹介しましょう。

言語ポリモーフィズム実現手段
Javainterface
C#interface
Swiftprotocol
Gotype
Rusttrait

特に、GoやRustなど、近年登場した言語では、継承という仕掛け自体を、廃止しています。

プログラミング言語全体が、継承が持つ複雑性を回避し、より簡素化する方向に進んでいると言えるでしょう。

まとめ

オブジェクト指向が普及した当時は、「現実の世界をそのままクラスで表現する」なんて、崇高な理想がありました。

そして、継承という技術も、「世界はどのような構成であるべきか」なんて、壮大な議論がされがちでした。

いまとなっては、そのような神話めいた話には、非現実感が漂います。

いま大事なこと。それは、必要なことを、シンプルに、最小限に実現すること

世界の有り様なんていう、壮大な問題を考える必要は、ないのですよ。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次