連載:熱血VBプログラマ応援団


第11回 継承とは隠された条件判断である

―― VB.NETの難解な新機能「継承」に取り組む1つの方法 ――

株式会社ピーデー 川俣 晶
2004/08/18
 

− 今回のご相談 −

 最近、Visual Basic .NETの勉強を始めました。いろいろな新しい機能があってドキドキする半面、全部使いこなすことができるか不安も感じます。

 特に不安があるのが「継承」です。といっても、継承が使えないという意味ではありません。一応、継承について勉強したので、継承を使ったプログラムを書くことはできます。問題はどこで継承を使えばよいのか、ピンとこないことです。これまで継承という機能のないVisual Basic 6.0でプログラムを作成してきましたが、特に継承が必要とされる状況はありませんでした。ですから、これからも継承を使わないで開発していけると思うのです。

 しかし、オブジェクト指向プログラミングでは、継承は絶対に必要だといわれているそうです。特に使う必要のない継承を使うと、かえって分かりにくいプログラムになってしまいそうな気がします。どうしても継承は使わなければならないのでしょうか。

赤き血のV11 より

なぜ継承は分かりにくいのか

 継承は、従来型プログラミングを行ってきた立場から見ると、オブジェクト指向プログラミングを理解するうえでの1つのハードルだと思います。特に問題になるのは、継承のやり方は分かっても、それが何の役に立つのかが分からないケースでしょう。それは、私自身にも覚えがあります。最初にオブジェクト指向プログラミングに触れたとき、継承という機能を何に使えばよいかを把握するのは、かなりの難物でした。

 では、どうして継承は難しいのでしょうか。本連載は技術解説ではないので、私なりに非常に単純化した解釈で説明していきたいと思います。厳密に正しい解釈を知りたい方は、専門書をひもといてください。

 継承によって実現される機能は、対象をより抽象的に扱うことだと思います。という文章を読んで、すでに「分かりにくい」「難しい」と思った人がいると思います。恐らく、そう思った理由は「対象をより抽象的に扱う」という部分にあると思います。「対象」「抽象的」「扱う」といった言葉は、どれも具体的ではありません。もし、「変数に1を代入する」といえば、「変数」「1」「代入」のいずれも具体的なので、何をいいたいのかがすぐにイメージできます。しかし、「対象」「抽象的」「扱う」といった具体的ではない言葉で表現されると急に分かりにくくなるのではないでしょうか。

 これは、学校の算数の時間に、「1+1=」という問題には即答できる子供が、「x+1=2」という問題になった瞬間に分からなくなってしまうのと同じようなことだといえます。「1」という数字は具体的で容易にイメージできます。リンゴが1個、と置き換えてイメージするともっと具体的です。しかし、「x」は具体的ではなく、容易にイメージできません。いくつか分からないが何かの数字がある、と説明されても、数が決まっていないリンゴを想像するのは難しいといえます。

 この文章に出てくる「具体的」という言葉の反対の意味を持つのは「抽象的」という言葉です。そして、「抽象的」であることは、「具体的」であることよりも分かりにくいといえます。継承は対象をより抽象的に扱うこと、と考えるなら、継承が難しい理由は明らかです。継承は抽象的だから難しいのです。

「動物」と「犬」「猫」「ホトトギス」の関係

 ピンとこない読者も多いと思うので、継承が抽象的というのはどういうことか、簡単な図で説明してみましょう。

継承の概念図

 この図では、「犬」「猫」「ホトトギス」という3種類の生き物の名前と、「動物」という言葉が書かれています。「犬」「猫」「ホトトギス」という名前は具体的です。例えば、「犬」といえば、具体的にそれが何かイメージすることは容易でしょう(とても多くの犬を知っていて、そのうちのどれをイメージしてよいか迷うのでなければ)。しかし、「動物」といったらどうでしょうか。動物園にいる多くの動物を思い浮かべるかもしれませんが、「犬」ほど明瞭なイメージは思い浮かばないと思います。

 では、「動物」という言葉が無意味かというと、そんなことはありません。「犬」や「猫」や、そのほかの動く生き物をまとめた総称として「動物」という言葉は必要とされています。その言葉抜きでは、「動物園」という言葉すら使えません。

 オブジェクト指向プログラミングでは、具体的な生き物と、「動物」の関係を、継承を使って表現することができます。つまり、「動物」クラスがあり、そのクラスを「継承」して、「犬」クラスや、「猫」クラスを作成するわけです。

 継承を行うことで、「犬」クラスや「猫」クラスは、「動物」クラスの機能を含むことになります。例えば「動物」クラスに「歩け」というメソッドがあれば、「犬」クラスにも「猫」クラスにも「歩け」というメソッドがあるかのように見えることになります。「歩け」というメソッドを「動物」クラスより継承しているわけです。

 逆に「毛玉を吐け」というメソッドは、「動物」クラスではなく「猫」クラスに用意します(毛玉を吐くのは猫だけですから)。これにより、「毛玉を吐け」というメソッドは、「猫」クラスを使う場合にのみ呼び出し可能になり、「犬」クラスからは呼び出すことができません。

 さて、こういった解説はオブジェクト指向プログラミングの入門書にはよく出てくると思います。問題は、このような説明を読んでも、どうして継承が必要なのかピンとこない人たちも多いということでしょう。「動物」などといわず、条件判断で「犬」の場合、「猫」の場合と分けて処理させて、共通部分を1つのメソッドにまとめておけば、機能的には問題ないプログラムを作成できます。それにもかかわらず、継承を使わなければならない理由は何でしょうか。特に、継承は抽象的であるために難しいとなれば、どうして使う必要があるのか考え込んでしまうのは当然の成り行きだと思います。

条件判断を置き換える継承

 抽象的であることが難しいのなら、できるだけ具体的なソース・コードを見ながら考えてみましょう。具体的な事例を積み重ねることも、抽象的なものを理解するための1つの手段です。多くの事例を積み重ねると、ある日、パッと分かることもあります。

 では、具体例として以下のようなソース・コードについて考えてみましょう。これは、特に継承は使用していないシンプルなVisual Basic.NET 2003によるソース・コードです。できるだけシンプルにするために、コンソール・アプリケーションとして作成しています。

Enum 動物種類
  犬
  猫
  ホトトギス
End Enum

Module Sample001
  Sub 犬鳴かせる()
    Console.WriteLine("ワン")
  End Sub

  Sub 猫鳴かせる()
    Console.WriteLine("ニャア")
  End Sub

  Sub ホトトギス鳴かせる()
    Console.WriteLine("テッペンカケタカ")
  End Sub

  Sub 鳴かぬなら鳴かせてみせよう(ByVal 対象種類 As 動物種類)
    Select Case 対象種類
      Case 動物種類.犬
        犬鳴かせる()
      Case 動物種類.猫
        猫鳴かせる()
      Case 動物種類.ホトトギス
        ホトトギス鳴かせる()
    End Select
  End Sub

  Sub Main()
    鳴かぬなら鳴かせてみせよう(動物種類.犬)
    鳴かぬなら鳴かせてみせよう(動物種類.猫)
    鳴かぬなら鳴かせてみせよう(動物種類.ホトトギス)
  End Sub

End Module

 これを実行すると以下のような結果になります。

ワン
ニャア
テッペンカケタカ

 このプログラムは「犬」「猫」「ホトトギス」の3種類の鳴き声を出力します。実用性はまったくありませんが、実際に使われるプログラムでは「犬」が「出納伝票」に変わったりするわけです。ここでは、親しみやすく取り組むために、動物のままでいきましょう。

 このプログラムのポイントは、「鳴かぬなら鳴かせてみせよう」メソッドです。このメソッドでは、Select Caseステートメントによって動物の種類を判定し、適切な鳴き声を出力させています。

 さて、このメソッドからSelect Caseステートメントを消滅させることができるでしょうか。より具体的にいえば、「鳴かぬなら鳴かせてみせよう」メソッドを以下のように条件判断ステートメント抜きで記述できるでしょうか、ということです。

Sub 鳴かぬなら鳴かせてみせよう(ByVal 対象 As 動物)
  対象.鳴かせる()
End Sub

 これが今回の最大の目玉です。継承には条件判断を行うという機能性があって、Select Caseステートメントを継承で置き換えることができるのです。

 条件判断を継承で置き換えることは、最近流行しているリファクタリングと呼ばれるソース・コードの書き換え手法の中で、「ポリモーフィズムによる条件記述の置き換え」に該当します(マーチン・ファウラー著『リファクタリング − プログラミングの体質改善テクニック』の255ページ参照)。

 条件判断を継承によって置き換えることは、比較的小さなソース・コードの書き換えなので実践しやすく、さらに継承を使う前と使った後のソース・コードを比較しやすいので、継承を体験する第一歩としては悪くない糸口だと思います。

繰り返される条件分岐を継承でスマートに!

 継承を使って「鳴かぬなら鳴かせてみせよう」メソッドからSelect Caseステートメントを消滅させたソース・コードの全文を以下に示します。

MustInherit Class 動物
  Public MustOverride Sub 鳴かせる()
End Class

Class 犬
  Inherits 動物
  Public Overrides Sub 鳴かせる()
    Console.WriteLine("ワン")
  End Sub
End Class

Class 猫
  Inherits 動物
  Public Overrides Sub 鳴かせる()
    Console.WriteLine("ニャア")
  End Sub
End Class

Class ホトトギス
  Inherits 動物
  Public Overrides Sub 鳴かせる()
    Console.WriteLine("テッペンカケタカ")
  End Sub
End Class

Module Sample002

  Sub 鳴かぬなら鳴かせてみせよう(ByVal 対象 As 動物)
    対象.鳴かせる()
  End Sub

  Sub Main()
    鳴かぬなら鳴かせてみせよう(New 犬)
    鳴かぬなら鳴かせてみせよう(New 猫)
    鳴かぬなら鳴かせてみせよう(New ホトトギス)
  End Sub

End Module

 これを実行すると以下のような結果になります。

ワン
ニャア
テッペンカケタカ

 この2つのソース・コードを比較すると、いろいろな違いがあることが分かると思います。もちろん、「鳴かぬなら鳴かせてみせよう」メソッドからSelect Caseステートメントが消滅していることはいうまでもありません。そのほかに、列挙型の「Enum 動物種類」が消滅して、その代わりに、「動物」クラス、「犬」クラス、「猫」クラス、「ホトトギス」クラスが生まれています。そして、具体的に「鳴く」メソッドは、「鳴かせる」メソッドとして、それぞれのクラスに移動しています。

 さて、どうして「鳴かぬなら鳴かせてみせよう」メソッドからSelect Caseステートメントが消滅したのでしょうか。どこにも、「犬」ならばこのメソッドを呼び出せ、という条件判断は存在していないのに、どうして犬の場合には「ワン」という結果が出力され、「ニャア」とは出力されないのでしょうか。継承を知らないプログラマなら、インチキかトリックだと思うかもしれません。

 この結果を理解するには、継承は条件判断を記述しなくても条件判断することができる、つまり、継承は条件判断を抽象化してしまう、という事実を把握しなければなりません。すなわち、継承によって条件判断を行うということは、IfやSelect Caseと書く代わりにほかの何かのキーワードを書くのではない、ということです。IfやSelect Caseと書かないということは、それに付随する条件判断式も書く必要がない、ということを意味します。条件判断式も書かないで条件判断ができるのか、と思う人もいるかと思いますが、もちろん可能です。

 では、条件はいつどこで判断されているのでしょうか。もともと、「鳴かぬなら鳴かせてみせよう」メソッドから呼び出している「鳴かせる」メソッドは、具体的にどのメソッドを呼び出すかが確定していない抽象的なメソッド呼び出しです。

Sub 鳴かぬなら鳴かせてみせよう(ByVal 対象 As 動物)
  対象.鳴かせる()
End Sub

 どのメソッドが呼び出されるかは、実行中にのみ確定されます。そして、実行されるメソッドが確定するのは、クラスのインスタンスがNewキーワードによって作成された瞬間に当たります(「鳴かぬなら鳴かせてみせよう(New 犬)」の「New 犬」の部分に当たる)。この瞬間に、「鳴かせる」メソッドが呼び出すメソッドが具体的になります。「New 犬」であれば、「犬」クラスの「鳴かせる」メソッドが呼び出されます。「New 猫」であれば、「猫」クラスの「鳴かせる」メソッドが呼び出されます。「New ホトトギス」であれば、「ホトトギス」クラスの「鳴かせる」メソッドが呼び出されます。このように、どこにも条件判断式は含まれていませんが、間違いなく「犬」であれば「ワン」と出力するメソッドを呼び出すことができ、一種の条件判断が実現されています。

継承といかにして付き合うか

 さて、ここで「なるほど、継承で条件判断ができるのか。今度から条件判断はすべて継承で行うようにしよう」と思ってはいけません。継承には使うべきところと、使うべきではないところがあって、それを間違えるとソース・コードが分かりにくくなるワナが待ち構えているからです。

 もう一度、上の2つのソース・コードを見比べてみてください。果たして、どちらが分かりやすいソース・コードといえるでしょうか。行数はほとんど変わりませんが、継承を使わない方がプログラムが実行される順番が明確で、より分かりやすいと考えることが可能だと思います。人によって判断が分かれるかもしれませんが、私はこのケースであれば、継承を使うべきではないと思います。

 一方で、継承を使った方がよいケースもあります。例えば以下のようなメソッドがあったとします。このメソッドには、長いSelect Caseステートメントが3回も繰り返されています。

Sub 出力(ByVal 対象種類 As 出力種類)
  Console.WriteLine("出力準備を行います。")

  Select Case 対象種類
    Case 出力種類.ハードディスク
      ハードディスク出力準備()
    Case 出力種類.フロッピーディスク
      フロッピーディスク出力準備()
    Case 出力種類.イーサネット
      イーサネット出力準備()
    Case 出力種類.通信ポート
      通信ポート出力準備()
    Case 出力種類.ディスプレイ
      ディスプレイ出力準備()
    Case 出力種類.音声合成装置
      音声合成装置出力準備()
    Case 出力種類.無線LAN
      無線LAN出力準備()
  End Select

  Console.WriteLine("出力を行います。")

  Dim 出力データ As 出力データ型
  出力データ = 出力データ取得()

  Select Case 対象種類
    Case 出力種類.ハードディスク
      ハードディスク出力(出力データ)
    Case 出力種類.フロッピーディスク
      フロッピーディスク出力(出力データ)
    Case 出力種類.イーサネット
      イーサネット出力(出力データ)
    Case 出力種類.通信ポート
      通信ポート出力(出力データ)
    Case 出力種類.ディスプレイ
      ディスプレイ出力(出力データ)
    Case 出力種類.音声合成装置
      音声合成装置出力(出力データ)
    Case 出力種類.無線LAN
      無線LAN出力(出力データ)
  End Select

  Console.WriteLine("出力を終了します。")

  Select Case 対象種類
    Case 出力種類.ハードディスク
      ハードディスク終了処理()
    Case 出力種類.フロッピーディスク
      フロッピーディスク終了処理()
    Case 出力種類.イーサネット
      イーサネット終了処理()
    Case 出力種類.通信ポート
      通信ポート終了処理()
    Case 出力種類.ディスプレイ
      ディスプレイ終了処理()
    Case 出力種類.音声合成装置
      音声合成装置終了処理()
    Case 出力種類.無線LAN
      無線LAN終了処理()
  End Select

End Sub

 このようなメソッドは、いくつかの点で問題があります。

  • Select Caseステートメントの条件を変更するときに部分的に変更を忘れやすい。デバイスの種類を追加したとき、終了処理だけ条件を追加し忘れた、などのバグが入り込みやすい

  • 本来は、出力準備、出力、終了処理という流れを実現するものなのに、大量の条件表記に埋もれて読み取りにくい

 これらの問題は、条件判断を抽象化することで解決できます。分かり切った単純な条件判断が何回も繰り返されて邪魔に感じられる場合は、それを抽象化し、隠してしまうのは有効な対処です。

 このメソッドのSelect Caseステートメントを、継承を使って取り除いたとすると以下のような内容になります。

Sub 出力(ByVal 出力装置 As 出力デバイス)
  Console.WriteLine("出力準備を行います。")
  出力装置.出力準備()

  Console.WriteLine("出力を行います。")
  Dim 出力データ As 出力データ型
  出力データ = 出力データ取得()
  出力装置.出力(出力データ)

  Console.WriteLine("出力を終了します。")
  出力装置.終了処理()
End Sub

 このコードであれば、出力デバイスの種類を追加したとしても、条件の追加し忘れなどは起こりません。最初から条件を記述していないから忘れようがありません。また、このソース・コードであれば、出力準備、出力、終了処理という流れで処理されることも、一目りょう然です。

終わりに

 今回取り上げた機能は、継承の持つ機能のうちの1つにすぎませんが、少しずつ継承に親しんでいくための第一歩としては十分でしょう。少しずつ理解を深めていければ、いつかは克服できます。

 ここまで見てきたように、継承といっても、それがさほど役に立たない事例もあれば、とてもソース・コードを分かりやすくする事例もあります。

 オブジェクト指向プログラミングを行っているプログラマの中には、継承を悪いものだという人もいますが、私はその意見には賛成しません。正しい使い所さえ間違えなければ、継承はソース・コードを分かりやすくしてくれる強力な武器になると思います。

 頑張れVBプログラマ、君たちが使うVisual Basic .NETは取り組む価値のある可能性に満ちたプログラム言語だ!End of Article

インデックス・ページヘ  「熱血VBプログラマ応援団」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)
- PR -

注目のテーマ

業務アプリInsider 記事ランキング

本日 月間
ソリューションFLASH