連載

改訂版
プロフェッショナルVB.NETプログラミング

Chapter 10 継承とポリモーフィズム

株式会社ピーデー 川俣 晶
2004/06/24


 本記事は、(株)技術評論社が発行する書籍『VB6プログラマーのための入門 Visual Basic .NET 独習講座』の一部分を許可を得て転載したものです。同書籍に関する詳しい情報については、本記事の最後に掲載しています。

ソース・コードの弱点を分析

 さて、前ページのソースを見てどう思われるだろうか? もちろん、このソースはよい例ではない。今回のテーマであるポリモーフィズムは、このソースをより分かりやすく、より短くするために利用できる。だが、これを利用する前に、少しこのソースの弱点を分析してみよう。

 パッと見てすぐに気付くのは、出力する方法を数値で区別する方法をとっていることだろう。変数の値が1のときはイベント・ログへの出力としているが、そのようなルールは書き間違いひとつで破れてしまう。うっかり、その値が2のときにイベント・ログに出力するようなコードがソース・コード上に紛れ込んでも、それがバグであると気付くのは容易ではない。常識的に考えれば、数値に名前を当てるべきだろう。Enumステートメントで数値に名前を与えてみた例が、リスト10-37である。

 1: Imports System.Web.Mail
 2:
 3: Enum ReportType
 4:   ToMessageBox = 0
 5:   ToEventLog = 1
 6:   ToEMail = 2
 7: End Enum
 8:
 9: Public Class Form1
10:   Inherits System.Windows.Forms.Form
      ……
35:   Private reportMode As ReportType
36:
37:   Private Sub Form1_Load(……
38:     reportMode = ReportType.ToMessageBox】
39:   End Sub
40:
41:   Private Sub Button1_Click(……
42:     Dim msg As String
43:     msg = "エラーが発生しました"
44:     Select Case reportMode
45:       Case ReportType.ToMessageBox
46:         ReportToMessageBox(msg)
47:       Case ReportType.ToEventLog
48:         ReportToEventLog(msg)
49:       Case ReportType.ToEMail
50:         ReportToEMail(msg)
51:     End Select
52:   End Sub
53:
54:   Private Sub RadioButton1_CheckedChanged(……
55:     reportMode = ReportType.ToMessageBox
56:   End Sub
57:
58:   Private Sub RadioButton2_CheckedChanged(……
59:     reportMode = ReportType.ToEventLog
60:   End Sub
61:
62:   Private Sub RadioButton3_CheckedChanged(……
63:     reportMode = ReportType.ToEMail
64:   End Sub
65: End Class
リスト10-37 出力手段を数値からEnumステートメントによる列挙値に変更したプログラム(太字部分が変更箇所、“……”部分は一部省略)

 この変更により、“Case 1”のような記述が“Case ReportType.ToEventLog”に変わり、より意味が分かりやすくなった。Case ReportType.ToEventLogに対応する処理として、イベント・ログ以外に出力するコードが書かれていたら、すぐにおかしいと気付くことができるだろう。しかし、毎回毎回、エラーが起こるたびにSelect文を書いていたのではソースが長くなるし、出力先が増えるごとに正しくすべてのSelect文を書き直す手間は、プログラムが大きくなればなるほど大変なものである。やはり、Select文は1カ所にまとめるのが正解だろう。そのために、新しくReportメソッドを作成して、エラーの報告はすべてこのメソッドを呼び出すように変更すれば、エラー送信先が増えても減ってもソース修正の手間が小さいものとなる。リスト10-38は、実際にReportメソッドを作成してみた例である。

 1: Imports System.Web.Mail
 2:
 3: Enum ReportType
 4:   ToMessageBox = 0
 5:   ToEventLog = 1
 6:   ToEMail = 2
 7: End Enum
 8:
 9: Public Class Form1
10:   Inherits System.Windows.Forms.Form
      ……
35:   Private reportMode As ReportType
36:
37:   Private Sub Report(ByVal msg As String)
38:     Select Case reportMode
39:       Case ReportType.ToMessageBox
40:         ReportToMessageBox(msg)
41:       Case ReportType.ToEventLog
42:         ReportToEventLog(msg)
43:       Case ReportType.ToEMail
44:         ReportToEMail(msg)
45:     End Select
46:   End Sub
47:
48:   Private Sub Form1_Load(……
49:     reportMode = ReportType.ToMessageBox
50:   End Sub
51:
52:   Private Sub Button1_Click(……
53:     Report("エラーが発生しました")
54:   End Sub
      ……
67: End Class
リスト10-38 Reportメソッドを作成して、その中にSelect文を記述したプログラム(太字部分が変更箇所、“……”部分は一部省略)

 ここまで直してもまだ不満が残る。というのも、エラー報告でReportメソッドを呼び出すというのは、あくまでプログラマーにとっての約束事であって、このメソッドを呼ばないで、いきなりReportToEventLogメソッドを呼び出してイベント・ログに書き込ませることもできるからだ。これは、クラスとPrivateキーワードを活用することで解消できる。

 エラー報告関連の機能を別のクラスとしてまとめ、Reportメソッド以外にはPrivateキーワードを付ける。このような構造にしておけば、フォーム側からReportメソッド以外を呼び出すコードを記述するとエラーになるため、多人数のプロジェクトチームでプログラムを開発する場合でも、勘違いして誤用するプログラマーが出てくるのを防止できる。

 実際には、エラー報告クラスには、報告方法を指定するためのメソッドもPublic付きで用意する必要があるので、ReportメソッドだけがPublic指定というわけにはいかない。実際に、このような構造に書き直してみたのが以下のリスト10-39である。

 1: Imports System.Web.Mail
 2:
 3: Public Enum ReportType
 4:   ToMessageBox = 0
 5:   ToEventLog = 1
 6:   ToEMail = 2
 7: End Enum
 8:
 9: Public Class Reporter
10:   Private Shared Sub ReportToMessageBox(ByVal msg As String)
11:     MessageBox.Show(msg)
12:   End Sub
13:
14:   Private Shared Sub ReportToEventLog(ByVal msg As String)
15:     If Not EventLog.SourceExists("SampleSource") Then
16:       EventLog.CreateEventSource("SampleSource", "SampleNewLog")
17:     End If
18:     Dim myLog As New EventLog()
19:     myLog.Source = "SampleSource"
20:     myLog.WriteEntry(msg)
21:   End Sub
22:
23:   Private Shared Sub ReportToEMail(ByVal msg As String)
24:     Dim from As String = "autumn@piedey.co.jp"
25:     Dim mailto As String = "autumn@piedey.co.jp"
26:     Dim subject As String = "Sample Error Report"
27:     Dim body As String = msg
28:     SmtpMail.Send(from, mailto, subject, body)
29:   End Sub
30:
31:   Private Shared reportMode As ReportType
32:
33:   Public Shared Sub SetReportMode(ByVal mode As ReportType)
34:     reportMode = mode
35:   End Sub
36:
37:   Public Shared Sub Report(ByVal msg As String)
38:     Select Case reportMode
39:       Case ReportType.ToMessageBox
40:         ReportToMessageBox(msg)
41:       Case ReportType.ToEventLog
42:         ReportToEventLog(msg)
43:       Case ReportType.ToEMail
44:         ReportToEMail(msg)
45:     End Select
46:   End Sub
47:
48: End Class
49:
50: Public Class Form1
51:   Inherits System.Windows.Forms.Form
52:
53: …Windows フォーム デザイナで生成されたコード…
54:
55:   Private Sub Form1_Load(……
56:     Reporter.SetReportMode(ReportType.ToMessageBox)
57:   End Sub
58:
59:   Private Sub Button1_Click(……
60:     Reporter.Report("エラーが発生しました")
61:   End Sub
62:
63:   Private Sub RadioButton1_CheckedChanged(……
64:     Reporter.SetReportMode(ReportType.ToMessageBox)
65:   End Sub
66:
67:   Private Sub RadioButton2_CheckedChanged(……
68:     Reporter.SetReportMode(ReportType.ToEventLog)
69:   End Sub
70:
71:   Private Sub RadioButton3_CheckedChanged(……
72:     Reporter.SetReportMode(ReportType.ToEMail)
73:   End Sub
74: End Class
リスト10-39 エラー報告関連の機能を別のクラスReporterにしたプログラム(“……”部分は一部省略)

 このソースならクラスも利用しているし、Privateキーワードによるアクセス制御も有意義に活用されており、オブジェクト指向の世界に足を踏み入れたかのように思えるかもしれない。だが、オブジェクト指向の世界では、こういう問題を扱うときに、もっとよい方法が使われる。ここまで長々とソース・コードの初歩的な改良方法を解説してきたのは、ポリモーフィズムの利用がこのような改良とはまったく別次元の改善であることを、深く印象づけていただきたいからだ

ポリモーフィズムを用いた書き換え

 オブジェクト指向の世界では、このような改善方法とは異なる「ポリモーフィズム」という解決方法が存在する。実際に、まったく同じ機能を、ポリモーフィズムを用いて実現した例をリスト10-40に示す。

 1: Imports System.Web.Mail
 2:
 3: Public MustInherit Class Reporter
 4:   Public MustOverride Sub ReportMessage(ByVal msg As String)
 5: End Class
 6:
 7: Public Class ToMessageBox
 8:   Inherits Reporter
 9:   Public Overrides Sub ReportMessage(ByVal msg As String)
10:     MessageBox.Show(msg)
11:   End Sub
12: End Class
13:
14: Public Class ToEventLog
15:   Inherits Reporter
16:   Public Overrides Sub ReportMessage(ByVal msg As String)
17:     If Not EventLog.SourceExists("SampleSource") Then
18:       EventLog.CreateEventSource("SampleSource", "SampleNewLog")
19:     End If
20:     Dim myLog As New EventLog()
21:     myLog.Source = "SampleSource"
22:     myLog.WriteEntry(msg)
23:   End Sub
24: End Class
25:
26: Public Class ToEMail
27:   Inherits Reporter
28:   Public Overrides Sub ReportMessage(ByVal msg As String)
29:     Dim from As String = "autumn@piedey.co.jp"
30:     Dim mailto As String = "autumn@piedey.co.jp"
31:     Dim subject As String = "Sample Error Report"
32:     Dim body As String = msg
33:     SmtpMail.Send(from, mailto, subject, body)
34:   End Sub
35: End Class
36:
37: Public Class Form1
38:   Inherits System.Windows.Forms.Form
39:
40: …Windows フォーム デザイナで生成されたコード…
41:
42:   Private myReporter As Reporter
43:
44:   Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
45:     myReporter = New ToMessageBox()
46:   End Sub
47:
48:   Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
49:     myReporter.ReportMessage("エラーが発生しました")
50:   End Sub
51:
52:   Private Sub RadioButton1_CheckedChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles RadioButton1.CheckedChanged
53:     myReporter = New ToMessageBox()
54:   End Sub
55:
56:   Private Sub RadioButton2_CheckedChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles RadioButton2.CheckedChanged
57:     myReporter = New ToEventLog()
58:   End Sub
59:
60:   Private Sub RadioButton3_CheckedChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles RadioButton3.CheckedChanged
61:     myReporter = New ToEMail()
62:   End Sub
63: End Class
リスト10-40 ポリモーフィズムを用いて記述したプログラム

 ポリモーフィズムに慣れていない方は、とっさに何が書かれているのかわからないかもしれない。だが、一度理解してしまえば、難しいものではない。

 ポリモーフィズムを理解するポイントは、実行時に呼び出すべきメソッドが決定されるという点である。このサンプル・プログラムの場合、49行目でReportMessageメソッドを呼び出しているが、このメソッド呼び出しが具体的にどのメソッドを呼び出すかは、実行時に決定される。つまり、9行目のReportMessageメソッドを呼ぶのか、16行目のReportMessageメソッドを呼ぶのか、28行目のReportMessageメソッドを呼ぶのか、それは実行中に決定されるわけである。もちろん、実行時に決定できなければ、ラジオボタンによって切り替えができないわけだから、ある意味で当然である。

 次に注目していただきたいのは、49行目のReportMessageメソッドの呼び出しが、Reporter型の変数myReporterに対して行われていることである。ここでの呼び出しは、あくまでReporter型、つまりReporterクラスのReportMessageメソッドを対象としたものであって、決して、ToMessageBoxクラスやToEventLogクラスを対象とはしていないのである。しかし、直接的に対象とされていないからといって、呼び出せないわけではない。実際にこの変数に代入されている値は、ToMessageBoxクラスやToEventLogクラスのインスタンスであることは、ソース・コードを見れば一目瞭然だろう。

 ここで、ポイントが2点ある。1点は、ToMessageBoxクラスやToEventLogクラスのインスタンスへの参照を、どうしてReporter型の変数に代入できるのか。もう1点は、Reporterクラスのメソッドを呼び出しているのに、どうしてほかのクラスのメソッドが呼び出されるのか、である。

 前者は、インスタンスへの参照は常に自分のスーパー・クラスへの参照に変換可能であるという機能から実現可能なのは明らかだ。つまり、Aというクラスを継承したB、C、Dというクラスのインスタンスへの参照は、常にA型の変数に格納できる。そのため、異なる機能を持つクラスへの参照を保持したければ、それらのすべてのクラスに対するスーパー・クラスとなるクラスを作成しておけば簡単に実現できるわけである。

 後者は、中身のないメソッドに対し、継承により中身を入れるにて説明した内容を理解していれば、実現できることは容易に分かるだろう。この場合、ReporterクラスのReportMessageメソッドは、MustOverrideキーワードが付加されており、中身が存在しない仮想的なメソッドになっている。そのため、Reporterクラスのインスタンスは作成できない。作成しても、中身のないReportMessageメソッドが呼び出せないので、インスタンスとして不完全なのである。しかし、このクラスは継承には使用できる。例えば、8行目でToMessageBoxクラスは、Reporterクラスを継承していることが示されている。そして、9〜11行目のReportMessageメソッドの定義は、ただ単にメソッドを定義しているわけではなく、Overridesキーワードにより、スーパー・クラス(ここではReporterクラス)の同名のメソッドの中身を入れ替えることを示している。つまり、ToMessageBoxクラスのインスタンスへの参照は、Reporterクラスへの参照に変換できるが、変換された場合でも、ReportMessageメソッドの呼び出しは、Reporterクラスではなく、ToMessageBoxクラスのメソッドを呼び出すのである。このように、参照される型が変換されても、呼び出すメソッドが不変であることが、ポリモーフィズムにおいて重要な役割を果たしているのである。

 その結果、49行目のように、常にReporterクラスのReportMessageメソッドを呼び出しているにもかかわらず、実際にはほかのクラスのReportMessageメソッドを呼び出している、という機能が実現できる。

 ポリモーフィズムを利用すると、実際の処理は違うが表面的には同じような呼び出し方法で利用できる機能を、分かりやすく整理することが容易にできる。このサンプル・プログラムの場合、コンパクトさ、安全性、分かりやすさ、拡張性などが実現されていることが分かると思う。以下に、そのポイントを列挙しておく。

・ コンパクトさ ・・・・・ レポートの出力を管理する専用クラスや出力の種類を示すための列挙型の定義などが不要になっている
・ 安全性 ・・・・・・・・・ 出力に使用できるクラスはすべてReporterクラスを継承したクラスでなければならず、そうではないクラスをReporter型の変数に入れようとしても、コンパイラがエラーを報告してくれる
・ 分かりやすさ ・・・・ 継承関係を見るだけで、出力に使用できるクラスとそうではないクラスを区別することができ、プログラマー間の約束事などをいちいち確認する必要がない
・ 拡張性 ・・・・・・・・・ (このソースの場合)出力先の種類を増やすとき、ただ単にReporterクラスを継承したクラスを新しく作るだけでよい

 列挙型で区別する場合は、列挙型の項目を増やし、それと同時に、列挙型の値を見て処理を区別するメソッドにも判定項目を増やす必要がある。これが正しく同期して記述されなければ正常には動作しないが、正しく同期させるのはプログラマーの仕事である。これに対して、ポリモーフィズムを用いた方法の場合、Reporterクラスを継承していないクラスのインスタンスを代入しようとしたり、継承していてもReportMessageメソッドを実装していないクラスを利用しようとしたりすれば、コンパイル時にエラーになり、間違いを早期に察知することができる。


 INDEX
  [連載] 改訂版 プロフェッショナルVB.NETプログラミング
  Chapter 10 継承とポリモーフィズム
    1.Inheritsステートメントによる継承機能/クラス内で継承元クラスのメソッドを呼ぶ
    2.Windows FormにおけるInheritsステートメントの効果/継承時にメソッドの動作を入れ替える/Overridesキーワードを使って継承時にメソッドの動作を入れ替える
    3.中身のないメソッドに対し、継承により中身を入れる/Protectedアクセス/継承を禁止するNotInheritableキーワード
    4.ポリモーフィズム(異なる機能と共通の呼び出し方法)
  5.ポリモーフィズム(ソース・コードの弱点を分析/ポリモーフィズムを用いた書き換え)
    6.クラス・ライブラリに見るポリモーフィズムの実例 - その1/その2
 
「改訂版 プロフェッショナルVB.NETプログラミング 」


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