オブジェクト指向プログラミング(5) 静的分析(3) 第7回 ソフトウエアの再利用 オブジェクト指向プログラミング(5) 静的分析(3)
継承とは(インヘリタンス、Inheritance) ①クラス定義の共通部分を別クラスにまとめ ることで、コードの重複を排除する仕組み である。 ②継承はオブジェクト指向におけるもっとも 改革的な発明といっても過言ではない。 ③継承を活用すれば、私たちは生産性、保 守性、拡張性を大幅に向上することがで きる。
メカニズムとしての継承 定義:クラスがインスタンスを生成する際に、他のク ラスの属性/操作を借りてきて、自分と合成 定義:クラスがインスタンスを生成する際に、他のク ラスの属性/操作を借りてきて、自分と合成 して1つのインスタンスを生成する仕組みで ある。 次ページの図にて説明
メカニズムとしての継承 通常、オブジェクト指向では、1つのインスタンスは1つのクラスから生成されます。(右図) 一方、クラスが別のクラスを継承すると、そのクラスは自分のインスタンスを生成する際に継承しているクラスの属性や操作を自分と合成して1つのインスタンスとして生成する。(左図) これがオブジェクト指向独特で、かつ重要な概念である「継承」のメカニズム(仕組み)です。 では、いったいこの仕組みがどういうときに役に立つのでしょうか。
継承の目的 ①従来の手続き中心のアプローチでは、サブルーチンによる手続きの共通化というのは欠かせない概念です。これを「サブルーチンによる処理の共通化」といいます。 ②オブジェクト指向のようなクラス中心のアプローチでも、「クラスの一部を共通化する」という方法が存在します。これが継承の(プログラム的な面での)機能です。 継承の目的から考えて見たいと思います ①従来の手続き中心のアプローチでは、サブルーチンによる手続きの共通化というのは欠かせない概念でした。これは同じような処理を複数書くのは面倒だけではなく、その処理内容自体に変更があったときにはすべての場所を変更しないといけないという問題点があります。これはまったくもって非効率です。 それを解決するために考え出されたのが「サブルーチンによる共通化」です。 ②このように手続き中心のアプローチに手続きの一部を共通化するという方法があるように、オブジェクト指向のようなクラス中心のアプローチでも、「クラスの一部を共通化」するという方法が存在します。これがプログラム的な面での継承の機能です。
継承の目的 次に、分析的な視点から、継承という考え た場合、継承を使用する局面には、2種類 の場合があります。 ①「クラスの汎化」 次に、分析的な視点から、継承という考え た場合、継承を使用する局面には、2種類 の場合があります。 ①「クラスの汎化」 ②「クラスの特化」 これまでは、プログラム的な面から見ましたので、次に、分析的な視点から継承という概念について説明します。 継承を使用する局面に2種類の場合があります。1つは「クラスの汎化」、もう一つは「クラスの特化」です。
汎化としての継承 ・汎化(Generalization)というのは、複数のクラスの共通概念を抽出して、独立したクラスとして実装することをいう。
汎化としての継承 オブジェクト指向では、「人物」とう抽象的概念を「生徒」と「教師」から別に切り出してクラス化し、独立したクラスとして扱う方法が用意されています。 今、以前学習塾管理システムの分析で抽出した、生徒クラスと教師クラスがあるとします。 並べてみるとわかりますが、「生徒」と「教師」には、多くの同名の属性や操作を持っています。これらは単に名前が同じだけでなく、実際に教師の氏名を取得する場合と生徒の氏名を取得する場合で、その処理内容が違うとは考えられません。 これをそれぞれのクラスに対していちいち別々に実装するのは、無駄と言えます。 C言語的な考えでは、共通の処理はグローバル領域に共通関数として実装するような方法で解決するのが一般的にだと思います。 では、どうやって解決するかを考えてみたいと思います。 ここで「教師」と「生徒」に注目します。また、この複数のクラスの間に類似性から考えて見ます。これらは、ともに社会を構成している「人間」、つまり「人物」です。オブジェクト指向と独特の抽象的な概念です。 そして、(図中の言葉) この「ある概念のうちの抽象的な部分をさらに別の概念として切り出して実装する」という仕組みを「汎化としての継承」といいます。
継承の分析 ①分析的な面から継承とは何かということを定義すると、「あるクラスが、別のクラスの性質をそのまま引き継ぐ」というものです。 (オブジェクト指向独自の、抽象化の方法) ②例を見てみましょう。 先ほどは、メカニズム面から継承を考えましたが、今度は分析的な面から継承とはなにかを考えて見たいと思います。 (図中①)
継承の分析 これは継承の記法です。詳細は、後で説明するとします。 これは、「生徒」というモノの中に存在している抽象的な概念である「人物」を、継承をもちいてクラス化して切り出した例です。 (以下次ページと同じ) この構造は、「生徒」という概念は「人物」という概念を引き継いでいる(継承している)、ということを意味しています。このような関係を一般的に「is-a関係」といいます。 つまり、「生徒is a 人物」というわけです。 継承は、このようなis-a関係を実現する際に使用します。
継承の分析 ・この構造は、「生徒」という概念は「人物」と いう概念を引き継いでいる(継承している)、 ということを意味します。 いう概念を引き継いでいる(継承している)、 ということを意味します。 これを、[is-aの関係」といいます。 ・「生徒 is a 人物」 ・継承は、このようなis-a関係を実現する際に 使用します。 前頁の説明
継承の分析 ・人物クラスのほうを「基底クラス(スーパー クラス)、生徒クラスのほうを「派生クラス (サブクラス)」であると、いいます。 クラス)、生徒クラスのほうを「派生クラス (サブクラス)」であると、いいます。 ・継承には多重度という概念はない。 ・継承は必ず明確な上下関係があります。 図の説明 この場合、人物クラスのほうを「基底クラス(スーパークラス)」といいます。 生徒クラスのほうを「派生クラス(サブクラス)」といいます。 関連とは、クラス間に関連があるといいました。また、インスタンス間にも関連があります。継承の場合は、クラス間に継承関連があっても、そのインスタンス同士はかならずしも関連があるとはかぎりません。(多くの場合は他人)多重度はインスタンス間の対応関係を表す概念ですので、継承とは無関係です。 継承には、どちらが基底クラス、どちらが派生クラスというはっきりとした方向性があります。(とういう点でも関連とは異なる)お互いに継承し合うこともありません。
継承の記述 通常の関連同様、クラスの間を直線で結ぶ ことによって表現します。それだけでなく、そ の途中に三角形を配置して、その向きによっ てその方向性を表現します。 三角形の頂点側が基底クラス、底辺側が派 生クラスです。 継承の記法について説明します。 先ほどの継承の分析で出来てきた図にて説明します。 通常の関連同様、クラスの間を直線で結ぶ ことによって表現します。それだけでなく、そ の途中に三角形を配置して、その向きによっ てその方向性を表現します。 三角形の頂点側が基底クラス、底辺側が派 生クラスです。
継承の記述 基本的には、クラス図には基底クラスの方を 派生クラスより上に書きます。だから、三角 形は常に頂点が上を向いています。 次に、クラスからインスタンスについて見てみましょう。
生徒クラスのインスタンスの生成 クラスからインスタンスがどのように生成されるか見てみます。(右下の図) まず、人物クラスのインスタンスは人物クラスの構成そのままに生成されています。 では、生徒クラスのインスタンスはどうでしょう。それだけでは、属性・操作は少ない ですが、人物クラスを継承することによって、人物クラスの属性/操作を自分自身 と合成し、あたかも元から自分の属性/操作として存在していたかのようにインスタンス を生成します。実際に生徒クラスから生成されるインスタンスは、多くの属性/操作 を持つことになります。 動作としてみてみると 生徒クラスのインスタンスに対して「氏名設定」の操作を行った場合、生徒クラスに その操作が存在していなくても、基底である人物クラスにその操作が存在しているので、 あたかも生徒クラスに自体にその操作が存在しているように動作します。 操作する側から見た場合、「氏名設定」の操作が、生徒クラスに実装されているか、 あるいは基底の別のクラスに実装されているのかはまったく意識する必要はありません。 使う側からは、「生徒クラス」とその基底クラスは、まったく1つのクラスとして意識します。
生徒クラスのインスタンスの生成 ①人物クラスを継承することによって、人物クラスの属性/操作を自分自身と合成し、あたかも元から自分の属性/操作として存在していたかのようにインスタンスを生成する。
生徒クラスのインスタンスの生成 ②操作する側から見た場合、「氏名設定」の操作が、生徒クラスに実装されているか、あるいは基底の別のクラスに実装されているのかはまったく意識する必要はありません。 ③使う側からは、「生徒クラス」とその基底クラスは、まったく1つのクラスとして意識します。
継承のメリット 今までは、メカニズム、或いは分析的な面を見てきました。しかし それらはいったいどのようなメリットがあるのか考えてみたいと思います。 図を見てください。 このように、多くの類似したクラスが共通の基底クラスとして「人物」クラス を持つことができます。 これによって、すでに人物クラスが存在していれば、教師クラスでは、人物クラス に当たる部分の分析、実装、テスト、デバッグ、保守全ての作業が必要なくなります。 つまり、その部分のプログラムを書く必要がないということになります。 また、クラスの共通化は、拡張性、保守性の面でもメリットがあります。
継承のメリット ①多くの類似したクラスが共通の基底クラスとして「人物」クラスをもつことができます。 これによって、人物クラスが存在していれば、教師クラスでは、「人物」に当たる部分の分析、設計、実装、テスト、デバッグ、保守、全ての作業が必要なくなります。つまり、その部分のプログラムを書く必要がなくなるということになります。
継承のメリット ②汎化によるクラスの共通化は、拡張性、保守性の面でメリットがあります。
継承のメリット 年齢 生徒クラスと教師クラスに「年齢」という属性を追加する場合を考えてみましょう。 ・生徒と教師の共通の基底クラスである「人物」クラスに対して「年齢」属性を 追加することによって、生徒クラスと教師クラスの両方で、自動的に年齢という 属性が追加されたのと同じ動作をすることになります。
継承のメリット 生徒クラスと教師クラスに「年齢」という属性 追加する場合を考えてみよう。 ・生徒と教師の共通の基底クラスである「人 物」クラスに対して「年齢」属性を追加する ことによって、生徒クラスと教師クラスの両 方で、自動的に年齢という属性が追加され たのと同じ動作をすることになる。
汎化としてのまとめ 不適切な属性の付加 人物クラスにIDコードをという属性を持たせてしまったら IDカードを持たない保護者まで、その属性がふられてしまう。 もう一つ例を見てみましょう。 もう一度、学習塾管理システムに「IDコード」という概念があったとします。 これは、生徒、教師ともに、塾に入るときにIDカードをカードリーダに通す ことによって、システムに読み込まれるコードで、一人一人にユニークに振られる コードです。 これは、生徒、教師両方のクラスに共通の属性です。これを人物クラスに含めて しまってよいかです。(あまり深く考えないとします) ここで、その学習塾管理システムで新たに「保護者」クラスが必要になったとします。 保護者is-a人物である以上は人物クラスから派生させることができます。 しかし、人物クラスにIDコードを持たせてしまったら、IDカードも持たない保護者 にまでその属性が振られることになってしまいます。 現実の世界で「人物」全てにIDコードがふられていないことからも、適切でないこと がわかると思います。 このような場合、次ページのようにあらたなクラスを派生させて考えて見ます。 現実の世界で考えると「人物」すべてにIDコードが振られることとなり適切でないことなる。
汎化としてのまとめ Is-aの関係 Is-aの関係 Is-aの関係 Is-aの関係 人物食クラスにIDコードを付与するのではなく、 人物クラスから新たに「塾が管理する人物」クラスを派生させます。 さらにそこから「生徒」「教師」を派生させるのが適切です。 そして、「保護者」クラスは、人物クラスから派生させます。 こうすることにより、全てのクラス間には、「is-aの関係」が成り立ちます。 このように、ものごとの抽象的な部分に着目し、まるで世の中の成り立ちを考えるように 概念の階層を構築し、基底クラスを分離、実装してくのが「汎化」という作業です。
特化としての継承 ・特化(Specialization)というのは、あるクラスを基にして、それを特殊化したクラスを派生クラスとして作ること。 たとえば、塾に「奨学生」という制度があったとします。 たとえば、塾に「奨学生」という制度があったとします。 ここで、継承してみます。
特化としての継承 「生徒」を基底クラスにして、「奨学生」クラスを派生させる。つまり「奨学生」is-a「生徒」という継承関連を結ぶ(左図)。 このようにして、継承をしようすれば、生徒クラスを基にしていろいろな特別な生徒クラスを派生させていくことができます。 どれだけクラスを派生させたとしても、元の生徒クラスにはまったく手を加える必要がない。 「生徒」を基底クラスにして、「奨学生」クラスを派生させる。 つまり「奨学生」is-a「生徒」という継承関連を結ぶ(左図)。 このようにして、継承を使用すれば、生徒クラスを基にしていろいろな特別な 生徒クラスを派生させていくことができます。 どれだけクラスを派生させたとしても、元の生徒クラスにはまったく手を加える必要がありません。 基底である生徒クラスは派生クラスの影響を受けることなく、従来通り単独で使用することができます。 基底クラスは派生クラスからは完全に独立した存在です。 継承は、複数のクラスの共通部分を抽出するためでなく、 あるクラスを基にして、そのクラスの特殊な形のクラスを作成することができる。 これを「特化」といいます。
特化としての継承 ①基底クラスは、派生クラスから完全に独立 した存在です。 した存在です。 ②継承は、複数のクラスの共通部分を抽出するためでなく、あるクラスを基にして、そのクラスの特殊な形のクラスを作成することができる。これを「特化」といいます。
汎化と特化 汎化と特化をまとめると。 ①「汎化」は、基底クラスを作ること ②「特化」は、派生クラスを作ること 元のクラスがどちらであるかということが違う だけで、メカニズムとしては同じものです。
プログラムレベルでの継承の例 「文字列の最後にスペースがついていた場合にそれを除去する」という処理の例 基底クラスである「文字列」クラスにはまったく手を加えることなく、機能が追加された文字列クラスを手にいれることができる。 「文字列の最後にスペースがついていた場合にそれを除去する」という処理の例です プリグラムを作成しているとき、文字列クラスのメンバ関数にするほどでもない、必要性も ない。つまり「再利用したいけど、いつでも必要というほどの処理ではない。」、このような中途半端に 重要な処理というのが実はかなりあるということを経験したことはないでしょうか。 そのようなときは継承の出番です。 このような場合、文字列クラスを特化させた「末尾がスペースの場合それを除去することができる文字列クラス」 を作るのが適当です。 これによって、基底クラスである「文字列」クラスにはまったく手を加えることなく、 機能が追加された文字列クラスを手にいれることができます。という例です。 これは、派生クラスの中に1つ、「末尾のスペースを除去」という操作を実装するだけです。 そうすれば、その派生クラスのインスタンスを生成すれば、文字列の設定や長さの取得など、 基底クラスの属性や操作はそのまま引き継がれ、まるで派生クラスにそのまま実装されている かのように使用することができます。 これで継承を使用してできた派生クラス「末尾のスペースを除去できる文字列クラスが完成 しました。
カスタマイズの手段としての継承 クラスの拡張 ①属性や操作が増える。 ②操作それ自体の処理内容の変更。 ③高度なカスタマイズのような拡張。 ①属性や操作が増える。 ②操作それ自体の処理内容の変更。 ③高度なカスタマイズのような拡張。 このような問題を解決する方法が継承には用意されています。 それは、操作のオーバーライドです。 継承を利用すれば、それぞれのクラスの独立性も保ったまま、属性や操作を付加してクラスを拡張し、 新しいクラスを続々と作ることが可能です。これが継承の基本的な使用方法です。 ところで、クラスを拡張するといっても、単に属性や操作が増えるだけではありません。 操作それ自体の処理内容の変更、高度なカスタマイズのような拡張もあると思います。 このような問題を解決する方法が継承には用意されています。 それは、操作のオーバーライドです。
カスタマイズの手段としての継承 操作のオーバーライド ・基底クラスのメンバ関数とまったく同じ名前のメン バ関数を派生クラスに実装することをいいます。 ・派生クラスのインスタンスの中には、基底クラス と派生クラスの2つの同じな名のメンバ関数が衝 突していることをいいます。 つまり、派生クラスの操作が基底クラスの操作を 横取りしてしまう方法です。 操作のオーバーライド ・基底クラスのメンバ関数とまったく同じ名前のメンバ関数を派生クラスに実装することをいいます。 ・派生クラスのインスタンスの中には、基底クラスと派生クラスの2つの同じな名のメンバ関数が 衝突していることをいいます。 つまり、派生クラスの操作が基底クラスの操作を横取りしてしまう方法です。(次ページの図参照)
カスタマイズの手段としての継承 派生クラスのほうにも「文字列設定」という操作を用意して、 基底の文字列クラスにある「文字列設定という操作を乗っ取ってしまう方法です。
カスタマイズの手段としての継承 派生クラスにある操作が優先して実行されます。 「文字列」クラスをカスタマイズしたにもかかわわず、その文字列クラスにはまったく手を触れる必要がない。 実際の動きを見てみましょう。 派生クラスのインスタンスに対して「文字列設定」が発行されます。 そのクラスインスタンスは、まず派生クラスのほうのメンバ関数の中に 「文字列設定」という操作が存在しているか調べます。 用意されていなければ、自動的に基底クラスの「文字列設定」が呼び出されます。 しかし、派生クラスにも同じ名前の操作が重複して存在している場合には、 派生クラスにある操作が優先して実行されます。 よって、派生クラスに、末尾スペースを除去するという機能を含めた「文字列設定」 を重複して実装すれば、処理を横取りでき、カスタマイズできた、ということに なります。 これで、既存の文字列クラスを基にして、操作をたった1つ追加しただけで、「末 尾のスペースが必ず削除される文字列」クラスを新しく作成したことになります。 なぜなら、「文字列」クラスをカスタマイズしたにもかかわらず、その文字列クラスには まったく手を触れる必要がないのです。
継承関係を導入したクラス図 インベーダゲームのクラス図に継承関係を 導入してみよう!
継承関係を導入したクラス図
継承の実装 LIST11:人物クラスから生徒クラスを派生 // person.h // 人物クラス class Person { int age ; // おなじみの属性 char * name ; public : virtual void setname ( char * name ) ; virtual void setage ( int age ) ; : } ;
継承の実装 // student.h // 生徒クラス // クラスを継承する際には,必ず基底クラスの宣言部が必要 #include "person.h" class Student : public Person // ここで継承している!! { int schoolyear ; // 学年(生徒独自の属性) : } ;
継承の実装 LIST12:生徒クラスの使用例 #include "student.h" // クラスが宣言されている // ヘッダファイルをインクルードする // 生徒クラスの使用例 main () { Student suzuki ; // Studentクラスのインスタンス生成 // 基底のPersonクラスのメンバ関数を呼ぶことができる suzuki . setage ( 15 ) ; :
継承の実装 LIST13:派生クラスから基底クラスへのアクセス class Base // 基底クラス { virtual void private_func () { // プライベートメンバ関数 puts ("プライベートメンバ関数") ; } public : virtual void public_func () { // パブリックメンバ関数 puts ("パブリックメンバ関数") ; } ; class Derived : public Base // Baseを継承している // 基底クラスのメンバ関数を呼ぶメンバ関数の例 virtual void foo () public_func () ; // OK private_func () ; // コンパイルエラー! :
多重継承(Multiple Inheritance) 多重継承とは。 「基底クラスを複数持つような継承」
多重継承 ある人事管理システムがあったとします。 その中で、「管理職社員」クラス、そして、「営業職社員」クラスがあるとします。 管理職社員には、その管理職が管理している「部署の名前」が属性として存在し、 また、営業職社員には「営業成績」が属性として存在します。 さて、営業課長はどのように分析されるべきでしょうか。 課長くらいでしたら、外回りはしないでも、実際の成績になるような営業も行う かもしれません。よって、管理職であると同時に営業職でもあるわけです。 つまり「管理職社員」クラスと「営業職社員」クラスの両方のクラスとis-a関連 があります。 したがって、上図は、問題があります。管理職はすべて営業職になってしまっています。 では、どのように実現するかというと、このような時に使用されるのが「多重継承」です。 つぎの例
多重継承 このように、営業課長は「管理職社員」であると同時に、営業成績が存在する「営業職社員」 でもあります。 このように、1つのインスタンスが複数の面を持ち、それをシステムとして実現しなといけない 場面は少なくはないでしょう。このように現実に対応するように考えられたのが多重継承です。 多重継承は、あるクラスの基底クラスとして、1つだけでなく複数のクラスを持つことができる、 ということです。上図がそれを表しています。 このようにすれば、営業課長のインスタンスに対して、営業成績を問い合わせることもできます、 管理している課の名前を聞くこともできます。 このように多重継承すれば、「営業管理職社員」クラスは、すべての属性と操作を「管理職社員」 と「営業職社員」から引き継ぐことができ、特別な実装はなにも必要ありません。
多重継承が表す概念 ・1つのインスタンスが複数の面を持ち、それ をシステムとして実現しないといけないという ことは非常によくあることです。 ・このような現実に対応するように考えられた のが「多重継承」です。 ・あるクラスの基底クラスとして、1つだけで なく複数のクラスを持つことができる。
多重継承の実装 リスト-1 営業社員クラスと管理職社員クラス class TradeStaff // 営業職クラス { リスト-1 営業社員クラスと管理職社員クラス class TradeStaff // 営業職クラス { unsigned long sales ; // 売上 : } ; class ManagementStaff // 管理職クラス String * sectionName ; // 管理している部署へのポインタ
多重継承の実装 リスト-2:「営業管理職社員(TradeManagementStaff)」クラス class TradeManagementStaff : public TradeStaff , public ManagementStaff { // 営業管理職クラス : : } ;
多重継承の実装 リスト-3:「営業管理職社員(TradeManagementStaff)」クラスの利用方法 foo () { // 営業管理職の鈴木さん TradeManagementStaff suzuki ; : // 営業職クラスのメンバ関数を呼べる // (営業成績の取得) sales = suzuki . getSales () ; // 管理職クラスのメンバ関数も呼べる // (管理している部署名の取得) puts ( suzuki . getSectionName () ) ; :