タイトル(OO-Style)
タイトル(オブジェクト指向りゅう)

クラスの継承 prev<<>>next メソッドテーブル

多態性(ポリモーフィズム)

多態性(ポリモーフィズム)はオブジェクト指向の基本となる(であり自然な)動作です。
「多態性」と立派(?)に名前がついていますが、動作自体は既にこのサイトでも説明済みで、クラスの継承 で説明した動作そのものです。 少々極端に言うと オブジェクトの上位クラスへの代入互換メソッドのオーバーライド により 起こる動作に名前がついただけなのです。
継承もわかった、オーバーライドもわかった、よし今度は多態性だ。。などと気構える必要は全然ありません。 継承、オーバーライドまでわかっていれば、それをどう使う?こう考えたらすごく便利だ。。というのが多態性です。
ここが理解できればシステムの設計や作りがガラリと変わるくらいのすごみ(おもしろさ)があります。 軽い気持ちで行きましょう。

▲このページのTOPへ

多態性の動作

まず1つ大きく概要として理解しておくことは、多態性を利用することで、呼び出される側のクラスが変わっても、呼び出し側・メインとなる処理は変わらない構造を作れるということです。
さて、多態性について実際どのような動作をするのかについて最も簡単な例で説明します。
このようなクラスを例にあげます。

多態性サンプルのクラス図

多態性サンプルのクラス図

SomeProcessクラスはメインとなる処理を行うクラスで、Figureクラスはなんだか知らないけども図形クラスです。 図形クラスには描画処理であるDrawメソッドが定義されています(virtualです)。 そして、Figureクラスを継承した3つのクラス、Circle, Triangle, Squareがあり、それぞれFigureクラスのDrawメソッドをoverrideしています。 Circle, Triangle, SquareクラスのDrawメソッドは、それぞれ "○","△","□" を描画する処理を実装しています。
サンプルソースを示します。

多態性サンプルソース1

 1|public class Figure
 2|{
 3|  public virtual void Draw()
 4|  {
 5|    Console.WriteLine("Figure->Draw");
 6|  }
 7|}
 8|
 9|
10|public class Circle : Figure
11|{
12|  public override void Draw()
13|  {
14|    Console.WriteLine("○");
15|  }
16|}
17|
18|
19|public class Triangle : Figure
20|{
21|  public override void Draw()
22|  {
23|    Console.WriteLine("△");
24|  }
25|}
26|
27|
28|public class Square : Figure
29|{
30|  public override void Draw()
31|  {
32|    Console.WriteLine("□");
33|  }
34|}
35|
36|
37|class SomeProcess
38|{
39|  [STAThread]
40|  static void Main(string[] args)
41|  {
42|    ArrayList list = new ArrayList();
43|    list.Add(new Circle());
44|    list.Add(new Triangle());
45|    list.Add(new Square());
46|
47|    foreach(Figure fig in list)
48|    {
49|      fig.Draw();    // ←描画の指示
50|    }
51|  }
52|}

Circle, Triangle, SquareのクラスのDrawメソッドは、自分の描画すべき図をコンソールに出力する処理を実装しています。 SomeProcessクラスのMain処理の中は2つに分けられます。
前半の43〜45行目はオブジェクトの設定部で、Circle, Triangle, Squareのクラスのインスタンスを生成し、リストに追加しています。
後半の47〜50行目は描画処理部で、リスト内のオブジェクトに対して描画を指示する。という内容です。

ちなみにAllayListクラスは、オブジェクトをリスト形式で格納するクラスです。格納する際の型は最上位クラスであるSystem.Object(以下、Object)クラス型で格納しますので、 全ての型のオブジェクトを格納することができます。(オブジェクトの上位クラスへの代入互換) ArrayListは、追加されたオブジェクトが何のクラスのインスタンスであるかは知り得ませんし、知る必要もありません。あくまでもObjectクラス型としてインスタンスを格納しているのみです。

実行結果は次のようになります。

サンプルソースの実行結果

サンプルソースの実行結果

SomeProcessクラスでの描画処理の詳細を見てみます。
47行目のfor文で、AllayListに格納されたオブジェクト(の参照)をFigureクラス型の変数figに代入します。 49行目で、figオブジェクトにDrawを指示(コール)します。 このときにfigが格納しているインスタンス(Circle, Triangle, Squareオブジェクトのいずれか)のDrawメソッドがコールされ、その図形がコンソール出力されます。 動作については正に 仮想メソッドとメソッドのオーバーライド で説明したものです。

オブジェクトを上位のクラス型変数に代入しても、インスタンス自体も変数に代入される参照値もなんら変わりはありません。 ただただオブジェクトを使用する側(ここではfigを使う側)がそのオブジェクトをどのように扱えるかが変わるのみです。 そして、継承をうまく使うことにより、上位のオブジェクト(だと思って)に対してメソッドをコールしても、 実際はそこに格納されている参照先のオブジェクトのメソッドが呼び出される。という動作をさせることができます。
多態性と言われる所以はここにあります。SomeProcessは、「FigureオブジェクトのDrawを呼び出す」という1つのことを しているだけなのに、実際の動作はというと、Drawメソッドをコールしたときにfigに代入されている参照先の インスタンスのDrawメソッドが動作します。 見方を変えると、SomeProcess(処理を呼び出す側)にしてみると、サンプルソース49行目のfigオブジェクトは 同じ呼び出しをしているのに、その時々によって異なる動作をする(多態)と見えます。
ですので、このような動作を多態性(ポリモーフィズム)と呼んでいます。

また、サンプルソースではFigureクラスのDrawメソッドにも"Figure->Draw"が出力されるようにコーディングされていますが、 出力結果には出力されていません。後述の 上位クラスのメソッドの存在の意味は? でお話しますが、なぜここで出力されないのかを しっかりと理解してください。FigureクラスのDrawメソッドは実際どこからも呼び出されていません。 (Figureクラスを継承したくラスを作成し、Drawメソッドをoverrideしなかった場合は、"Figure->Draw"が出力されますが)

最後に、ここではSomeProcessクラスは上位クラスであるFigureクラスのDrawメソッドのコールで「描画をする」ことさえ指示すれば、あとは自然に適切な「描画」が行われる。 という説明を主としています。 実際の開発で43〜45行目のように、ベタでCircleなどのクラスをNewしていては意味がありません。 実際の開発では、Circleな, TriangleなどのインスタンスはSomeProcessクラスに引数で渡されてきたり、クラス定義をXMLなどに行っておき、 何らかの条件から機械的にクラス名を取得しインスタンスを生成(クラス名からインスタンスを生成)するなどし、 SomeProcessクラス内にCircle, Triangleなどのクラスは存在しないような設計をします。
今回の例でいうと、SomeProcessクラスはFigureクラスしか知らない。という形でクラスの設計を行います。

 メイン処理フロー側のSomeProcessクラスは、Figureオブジェクト(実際のインスタンスはFigureクラスを継承したクラスのインスタンスであり何のクラスのインスタンスかはわからない) に対してしか意識していないことがポイントです。実際に何の図形が出力されるかは別として、「図形を描画する」ということを行い 次の処理に進むという、共通的なメイン処理を作ることができます。そして、Figureクラスを継承したクラスは、出力する図形に関してしか 意識する必要はありません。
 SomeProcessとFigureクラスにしてみれば、今後別の図形を出力するクラスがいくつ増えるか減るかはわかりませんが、増えても減っても この2クラス、全体としての処理はなんら影響を受けないところが多態性を利用した作りのすごさであり、おもしろさでもあります。
Figureクラスを継承したクラスのインスタンス生成方法はもちろん自動的に行うようにしておく必要はあります

 

▲このページのTOPへ

上位クラスのメソッドの存在の意味は?

ところで、多態性の動作サンプルソース1 内にある、FigureクラスのDrawメソッドは、 結局どこからも呼ばれていません(Figureクラス型変数の参照先のインスタンスがFigureクラスのインスタンスではなかった為)。 ○,△,□は出力されましたが、"Figure->Draw" は出力されませんでした。 今回サンプルとしてあげた3つのクラス Circle, Triangle, Squareクラス以外に、Figureクラスを継承したクラスが存在し、 そのクラスがDrawメソッドをオーバーライドしなかった場合や、Figureクラス自体をインスタンス化した場合などは、 "Figure->Draw" は出力されます。
しかし今回のような使用方法の場合、Figureクラスはそれぞれの図形のクラスを「図形」として抽象化して 一意に扱っているだけの存在であるため、 FigureクラスのFrawメソッド(仮想メソッド)の存在自体は意味があり、メソッドの中身は意味がない。 といえます。
このあたりの話は仕様が絡んでくるので「こうです。」と一言で言えなくなってきますが、 今回のような、SomeProcessクラスが、いろんな形を出力する処理を一括して扱う(「図形」を出力するという抽象的な扱い)様な場合は、 次のことがいえます。

  • Figureクラス自体をインスタンス化して使用することはない
  • FigureクラスのDrawメソッドの中身は無用

つまり、今回Figureクラスは次のソースで十分といえます。

多態性サンプルソース2

 1|public class Figure
 2|{
 3|  public virtual void Draw()
 4|  {
 6|  }
 7|}

Drawメソッドは、中にソースが1行も存在しません。しかし「FigureクラスのDrawメソッド」が無かったら サンプルソース1の49行目のように、それぞれの図形描画を1文で表すことはできません。 空の(仮想)メソッドに非常に重要な役割があることがわかると思います。
先にあげた2つのポイントをもっと安全に実装する手法があります。抽象メソッドがそれにあたります

仮想メソッドの場合、メソッドの中が空であってもメソッドそのものの存在自体に意味があります。
JAVAは、virtualなどと宣言しなくても全てのメソッドが仮想メソッドとして扱われます。

 

ちなみに先にサンプルであげた サンプルソース1 のvirtualなFigure.Drawの処理は確かに 動作させる必要はありませんでした(というか動作しない方がよい)。 しかし、図形出力の際に前準備が必ず必要で、その処理を上位クラスのDrawメソッドが実装してたとしたらどうでしょう。 (共通な処理を上位のメソッドが実装し、virtual定義ということはよくあるパターンです)
このような場合、ソースは次のようになります。(FigureとCircleクラスのみ記載)

多態性サンプルソース3

 1|public class Figure
 2|{
 3|  public virtual void Draw()
 4|  {
 5|    XXX.DrawJunbi();   ← 描画の前準備を行う処理
 6|  }
 7|}
 8|
 9|public class Circle : Figure
10|{
11|  public override void Draw()
12|  {
13|    base.Draw();      ← FigureクラスのDrawメソッドをコール
14|
15|    Console.WriteLine("○");
16|  }
17|}
18|
19|  public override void Draw()
20|  {
21|    base.Draw();      ← FigureクラスのDrawメソッドをコール
22|
23|public class ETC
24|{
25|  [STAThread]
26|  static void Main(string[] args)
27|  {
28|    Circle circle = new Circle();  ← Circleクラス型変数に代入
29|    Figure figure = new Circle();  ← Figureクラス型変数に代入
30|
31|    circle.Draw();    ← 描画の指示
32|    figure.Draw();    ← 描画の指示
33|  }
34|}

CircleクラスのインスタンスのDrawメソッドをコール(31,32行目)した際に動作するメソッドは、 インスタンスへの参照を格納している変数の型が、CircleであってもFigureであっても Circle.Draw です。 Firgure.Drawは31,32行目の呼び出しから直接呼び出されることはありません(仮想メソッドとメソッドのオーバーライド を参照)。 したがって、今描画の前処理として必須としている 5行目の処理 を行うためには 13行目の base.Draw のコールは必須となります。つまり、
「上位のクラス(ここではFigure)で基本的な機能はすでに持っている。 このクラスを継承してメソッドをオーバーライドする場合は、上位で実装されている機能を動作させるために、 オーバーライドした際はまず上位クラスの機能を呼び、その後自分のクラスのやるべきことをやりなさい。」
というような意味になります。
実際、このような形で上位クラスが提供されることは非常に多く、base.オーバーライド元メソッド(); を まず呼び出す。というのが基本です。
ただ上位クラスのメソッドの呼び出しについては仕様に大きくかかわりますので一概には言えません。 呼び出す必要があるのかないのか、オーバーライドしたメソッド内のどこで呼び出すのかは慎重に検討する必要があります。 (上位クラスのメソッドの呼び出し有無についてのミスは、コンパイルエラーなどのような形でわかるものではない) メソッドをオーバーライドする際に、上位クラスのメソッドをコールする必要があるのかないのかは、コーディングレベルでは判断できない。ということです。
実際MSDN内でも、継承時の注意 として上位クラスのメソッドの呼び出しについて書かれています。(例:Control.OnEnter メソッド)

このように、オーバーライドしたメソッドから上位のメソッドをコールするということは、処理の追加になります。 上位のメソッドをコールしないということは完全にメソッドの上書き(書き換え)になります。
サンプルソース3 のように上位クラスのvirtualなメソッド内に有効な処理がある場合、 上位のコール(13行目)がないと動作が保障できません。とても重要な1行です。

 

▲このページのTOPへ

開発の二分化

先に(多態性の動作)に記したような発想でクラスを組み立てることで、 メイン処理側と、個別処理を完全に分離させることができるようになります。
もう1度クラス図を見てみます。

開発の二分化クラス図

多態性サンプルのクラス図

メイン処理となるSomeProcessクラスと、「描画をする」という抽象的で中身のない(定義だけで実際には処理をしない)Figureクラスを作成するA部。 そして、A部から提供されるFigureクラスを元に、それらを派生して「実際の描画を行う処理」を作成するB部。 A部では「描画をする」という行為がある。という前提において処理を完結させることができます(サンプルソース49行目のように、fig.Draw(); でおしまい)。 しかし、A部だけの完結では処理がほとんど出来上がったようなものですが、実際の描画処理がない未完成品です。 Figureクラスを継承してどのような図形を描画するクラスが作成させれていくかをA部はわかりませんが、A部としてはこれで完成です。 B部開発者はFigureクラスを継承し、実際に必要な図形クラスを作成します。ここでは逆にA部が何をしているのかを意識する必要はありません。 非常に簡単な例ではありますが、各種フレームワークはこのような形を基本に形成されています。
そいういった意味でも、多態性は非常に重要な内容です。

おおまかにいってしまうと、A部側はフレームワークなどの特定の業務に特化しない開発者に利用されるクラスの開発で、 B部側は、フレームワークを利用した業務アプリ開発。という区分けになります。

A部,B部と簡単にいっていますが、この位置関係のレイヤーは捕らえ方によって変わりますし、 自分たちでの定義(意識)も必要となってきます。 例えば、JavaAPI,.NET Framework はもちろんA部に該当しますが、 サードパーティが提供するフレームワークもA部であり、開発プロジェクト内で専用のアプリケーションフレームワークを作成すれば それもまたA部となります。
どのレベルで区分けを行うかは開発者が線引きをする必要があります。システムが大きくなるほどこの線引きが開発方法・効率などに 大きく影響してきます。

この多態性の利用で、最終実装であるB部がなくとも全体の処理を組み上げることができる(半完成品)ことにより、 メインフローの共通化・標準化を推し進めることが用意になります。また、B部の開発は、A部が提供する機能を 学習する必要はありますが、システムの全体的な動作などは気にかけず、自分の開発すべき箇所にのみ注力して 開発を進めることができます。

 

クラスの継承 prev<<>>next メソッドテーブル