とはいえ、今回のお話はどちらかというと Java や C++ の学習によるシナジー効果で身に付けた知識だったりします。『VB しか必要ないから VB だけがんばる』という硬直した方針では、なかなか前に進めないものだなぁと思ったり思わなかったり。
そんなわけで本日は、GoF のデザインパターンの中から個人的に気に入っているものを独断と偏見でチョイスして、実際のソースコードとともにご紹介したいと思います。
【Singleton パターン】
まずは、理解しやすく実装も容易な Singleton パターンからご紹介しましょう。
これは、プログラム内に存在するオブジェクトが 1 つだけである事を保証するための仕組みです。一体それが何の役に立つのでしょうか。その答えの一つが『ゲームプログラマになる前に覚えておきたい技術』の p.183 で述べられていますので、当該箇所を引用します。
シングルトンクラスの目的は以下のようにまとめられる。……だそうです。
要するに、安全性を高めたグローバル変数のことである。
- 1. グローバル変数の危険さを軽減する
- 2. グローバル変数と同じように使えるようにする
実際には、オブジェクトを生成する際に New が実行されるたび、(新たなオブジェクトを格納するための)メモリ領域が逐一確保されます。そのため、このままでは『オブジェクトが 1 つである事』を保証できません。
Singleton クラスでは、コンストラクタを Private にする事によって、外側から New の実行を禁止します。Singleton クラスは、内部的に静的な唯一の自己参照 _instance を持ち、外部から Singleton オブジェクトを要求された場合は、その _instance を返却します。
以下に実装例を示します(展開してご覧ください)。
' ======================================== ' Singleton ' ======================================== ' ----------------------------------------- ' Singletonクラス Public Class Singleton Private Shared _instance As Singleton = New Singleton() ' インスタンス取得 Public Shared ReadOnly Property Instance() As Singleton Get Return _instance End Get End Property Private Sub New() Console.WriteLine("オブジェクトを生成しました") End Sub End Class ' ----------------------------------------- ' テスト用モジュール Module Module1 Sub main() ' Singleton であるオブジェクトの取得 Dim singleton1 = Singleton.Instance Dim singleton2 = Singleton.Instance If singleton1 Is singleton2 Then Console.WriteLine("同じオブジェクトです") Else Console.WriteLine("異なるオブジェクトです") End If End Sub End Module
実行すると、コンストラクタが 1 回しか実行されていない事、および main 内の singleton1 と singleton2 が同じオブジェクトを共有している事が分かります。
Singleton クラスがデータを適切にカプセル化していれば、前述の通り安全なグローバル変数として使用する事ができますし、また、New が重たいオブジェクトを使い回す際にも効果的でしょう。
【Iterator パターン】
多くのオブジェクトを一元的に集約したオブジェクトを作ったとしましょう。その際、その集約オブジェクトが持つ要素すべてにアクセスする統一的な手段を用意するのが Iterator パターンです。
『統一的な手段』とは、「集約オブジェクト内部の実装がどのように変更されようとも、要素のアクセス方法は変える必要がない」というほどの意味です。
以下に、Iterator パターンの VB サンプルコードを掲載します(展開してご覧ください)。
これは、複数の名前を集約管理する名簿(ListOfNames)クラスを作り、それに対してイテレータを追加したものです。
' ======================================== ' Iterator ' ======================================== ' ----------------------------------------- ' オブジェクトの集合体 Public Interface MyAggregate Function iterator() As MyIterator End Interface ' イテレータ Public Interface MyIterator Function hasNext() As Boolean Function getNext() As Object End Interface ' ----------------------------------------- ' ----------------------------------------- ' 名簿クラス Public Class ListOfNames Implements MyAggregate Private names() As String Private last As Integer 'コンストラクタ Public Sub New(ByVal maxSize As Integer) ReDim names(maxSize) End Sub Public Function getNameAt(ByVal index As Integer) Return names(index) End Function Public Sub appendName(ByRef name As String) names(last) = name last += 1 End Sub Public Function getLength() As Integer Return last End Function Public Function iterator() As MyIterator Implements MyAggregate.iterator Return New ListOfNamesIterator(Me) End Function End Class ' 名簿イテレータクラス Public Class ListOfNamesIterator Implements MyIterator Private listOfNames As ListOfNames Private index As Integer Public Sub New(ByRef arg As ListOfNames) listOfNames = arg index = 0 End Sub Public Function getNext() As Object Implements MyIterator.getNext Dim name As String = listOfNames.getNameAt(index) index += 1 Return name End Function Public Function hasNext() As Boolean Implements MyIterator.hasNext If index < listOfNames.getLength Then Return True Else Return False End If End Function End Class ' ----------------------------------------- ' テスト用モジュール Module Module1 Sub main() Dim listOfNames As New ListOfNames(10) listOfNames.appendName("Tercel") listOfNames.appendName("Syusyu_Syarin") listOfNames.appendName("pi_cro_s") listOfNames.appendName("tsumach") Dim it As MyIterator = listOfNames.iterator() While it.hasNext Dim name As String = CStr(it.getNext()) Console.WriteLine(name) End While End Sub End Module
このプログラムは単独で動作しますので、理解したい方はぜひコピペして動かしてみて下さい。
Main の中の While ループで、実際にイテレータを使用して名簿オブジェクトの要素にアクセスしています。
While it.hasNext Dim name As String = CStr(it.getNext()) Console.WriteLine(name) End While
よく見ると、呼び出されているのは MyIterator のメソッドだけ ― つまり、ListOfNames の実装が、要素へのアクセス方法に影響を及ぼしていない事が判ります。
Iterator パターンを適用しないと、データ構造が変わるたびに要素へのアクセス方法を切り替える必要があります。
集約オブジェクトの正体がただの配列であれば、For ループで添字アクセスが可能ですが、もし木構造に格納されている場合は、より複雑なトラバースアルゴリズムをコーディングしなければなりません。
しかし、Iterator パターンを用いると、外部に対してデータ構造を隠蔽する事ができます。しつこいようですが、内部の実装がどうなっていようとも、同じ方法で要素を走査する事が可能になるという事が、Iterator パターンの大きな利点です。
【State パターン】
State パターンは、状態に応じて振る舞いを切り替えたいときに真価を発揮するパターンです。
『状態に応じた切り替え』と聞くと、わざわざオブジェクト指向などを使わずとも If 文で単純に処理を分岐させればよいように思えますし、実際それは可能です。
ですが、状態数が増加すると、制御構造が極めて複雑化してしまいます(3月27日の日記あたりを適当に参照)。余談ですが、『ゲームプログラマになる前に覚えておきたい技術』でもこの問題点の解決のために 5 章を丸ごと割いています。
今回は、3月27日の日記をより簡単にした状態遷移プログラムを紹介します(展開してご覧ください)。
' ======================================== ' State ' ======================================== ' ----------------------------------------- ' 状態インタフェース Public Interface State Function update() As State End Interface ' ----------------------------------------- ' ----------------------------------------- ' 状態1 Public Class State1 Implements State Public Sub New() Console.WriteLine("状態1に移りました") End Sub ' 状態の更新 ' 戻り値は次状態を返すupdateメソッド Public Function update() As State Implements State.update Console.WriteLine("※ ここは、状態1です") Return New State2() ' 次状態は State2 End Function End Class ' ----------------------------------------- ' 状態2 Public Class State2 Implements State Public Sub New() Console.WriteLine("状態2に移りました") End Sub ' 状態の更新 Public Function update() As State Implements State.update Console.WriteLine("※ ここは、状態2です") Return New State3() ' 次状態は State3 End Function End Class ' ----------------------------------------- ' 状態3 Public Class State3 Implements State Public Sub New() Console.WriteLine("状態3に移りました") End Sub ' 状態の更新 Public Function update() As State Implements State.update Console.WriteLine("※ ここは、状態3です") Return New State4 ' 次状態は State4 End Function End Class ' ----------------------------------------- ' 最終状態(番兵) Public Class State4 Implements State Public Sub New() Console.WriteLine("最終状態に移りました") End Sub Public Function update() As State Implements State.update Return Me End Function End Class ' ----------------------------------------- ' テスト用モジュール Module Module1 Sub main() Dim state As State ' State型の宣言 state = New State1() ' 初期状態の代入 Do Until (TypeOf state Is State4) state = state.update() Loop End Sub End Moduleこれは、State インタフェースを実装した各状態クラス(State1 ~ State4)の間を遷移するプログラムです。
これによって、『状態の切り替え』と、『状態に応じた振る舞い』を巧妙に実装する事ができます。
状態クラス State1 を見てみましょう。ここには、『その状態における振る舞い』と、『次状態の情報』のみが含まれています。
Public Class State1 Implements State Public Sub New() Console.WriteLine("状態1に移りました") End Sub ' 状態の更新 ' 戻り値は次状態を返すupdateメソッド Public Function update() As State Implements State.update Console.WriteLine("※ ここは、状態1です") Return New State2() ' 次状態は State2 End Function End Class
次に、状態遷移を実際に行う main の中身を見てみましょう。
Dim state As State ' State型の宣言 state = New State1() ' 初期状態の代入 Do Until (TypeOf state Is State4) state = state.update() Loop
条件分岐のための制御構造がソースコードから排除されている事が判ります。つまり、オブジェクトがどの状態にあるのかを意識する必要がなくなるという事です。
また、遷移先を変更したい場合も、修正箇所は当該状態クラスに限定されるため、ソースコードの更新が容易になります。
【Template Method パターン】
Template Method は、複数の処理を決まった順序で実行させたいときに、その『順序』を前もって定型化しておくパターンです。
あくまで処理順序の『雛型』を作り、個々の具体的な処理は切り離して考える事がポイントです。これによって、柔軟で取り回しの利きやすいプログラムになります。
以下に、Template Method パターンのサンプルを示します(展開してご覧ください)。
これは、プログラムのコメントを自動生成するというものです。コメント生成機能は、VB 用と C 言語用を用意しました。どちらも同じテンプレートを継承している事にご注目ください。
' ======================================== ' Template Method ' ======================================== ' ----------------------------------------- ' テンプレート(抽象クラス) Public MustInherit Class AbstractTemplate Public MustOverride Sub printHeader() Public MustOverride Sub printMain() Public MustOverride Sub printFooter() Public Sub print() ' 決まった順序で処理を行う printHeader() printMain() printFooter() End Sub End Class ' ----------------------------------------- ' ----------------------------------------- ' VBコメント用テンプレート Public Class VBCommentPrinter Inherits AbstractTemplate Private comment As String Private width As Integer Public Sub New(ByRef comment As String) Me.comment = comment Me.width = System.Text.Encoding.Default.GetByteCount(comment) End Sub Public Overrides Sub printHeader() printLine("=") End Sub Public Overrides Sub printMain() Console.WriteLine("' " & comment) End Sub Public Overrides Sub printFooter() printLine("-") End Sub Private Sub printLine(ByVal c As Char) Console.Write("' ") For i As Integer = 1 To width Console.Write(c) Next Console.WriteLine() End Sub End Class ' ----------------------------------------- ' C言語コメント用テンプレート Public Class CCommentPrinter Inherits AbstractTemplate Private comment As String Private width As Integer Public Sub New(ByRef comment As String) Me.comment = comment Me.width = System.Text.Encoding.Default.GetByteCount(comment) End Sub Public Overrides Sub printHeader() printLine("+") End Sub Public Overrides Sub printMain() Console.WriteLine("/* " & comment & " */") End Sub Public Overrides Sub printFooter() printLine("+") End Sub Private Sub printLine(ByVal c As Char) Console.Write("/* ") For i As Integer = 1 To width Console.Write(c) Next Console.WriteLine(" */") End Sub End Class ' ----------------------------------------- ' テスト用モジュール Module Module1 Sub main() Dim vbComment As New VBCommentPrinter("VB用コメント") vbComment.print() Console.WriteLine() Dim cComment As New CCommentPrinter("C言語用コメント") cComment.print() Console.WriteLine() End Sub End Module
上記のプログラムを実行すると、以下のように表示されます。
VBCommentPrinter と CCommentPrinter オブジェクトともに、いずれも printHeader() 、printMain() 、printFooter() 関数が AbstractTemplate クラスで定めた順序通りに実行されている事がわかります。
繰り返しますが、基底クラスはあくまで雛型の役割しかしておらず、具体的な処理を一切制限しないという点が重要です。
そのため、よく使う処理の流れだけを前もってテンプレートにしておき、後から具体的に処理を書いていくという使い方ができます。便利ですよ。
【Facade パターン】
そろそろ力尽きそうなので、今日はこれで最後。ラストは個人的にかなり気に入っている Facade パターンのご紹介です。
オブジェクト指向に少し慣れてくると、部品のような細々としたクラスを個別に扱うのが面倒になってきます。なぜかというと、プログラマはそれらの雑多なクラスに関する知識が必要になってくるからです。
そこで、それらをまとめて扱うための『窓口(Facade)』を作り、以後は個々の部品を直接いじるのではなく、その窓口を通して処理を行うようにしよう、というのが Facade パターンです。
サンプルは、掃除、洗濯、調理といったあらゆる家事を行う個々のクラスと、それらのまとめ役であるチーフ(これが Facade になります)を作ります(展開してご覧ください)。
' ======================================== ' Facade ' ======================================== ' ----------------------------------------- ' 掃除屋さん Public Class Cleaner Public Sub clean() Console.WriteLine("お掃除します∩( ・ω・)∩") End Sub End Class ' ----------------------------------------- ' 洗濯屋さん Public Class Washer Public Sub wash() Console.WriteLine("お洗濯します(>Д<)ゝ""") End Sub End Class ' ----------------------------------------- ' 料理人さん Public Class Kitchener Public Sub cook() Console.WriteLine("お料理しますω(゚ω゚)ω ") End Sub End Class ' ----------------------------------------- ' 3人をまとめるチーフ Public Class Chief Dim cleaner As Cleaner Dim washer As Washer Dim kitchener As Kitchener Public Sub New() cleaner = New Cleaner() washer = New Washer() kitchener = New Kitchener() End Sub ' めんどうな家事をまとめて行う Public Sub doHousework() cleaner.clean() washer.wash() kitchener.cook() End Sub End Class ' ----------------------------------------- ' テスト用モジュール Module Module1 Sub main() Dim chief As New Chief() chief.doHousework() End Sub End Moduleこのプログラムでは、チーフオブジェクトが掃除屋、洗濯屋、料理人といったすべてのオブジェクトを一元的に管理してくれるため、(main の中身を書く)プログラマは、ごちゃごちゃしたオブジェクトを意識する必要がなくなります。これが一番の利点です。
そして、チーフにアクセスしている(外部の)オブジェクトは、(チーフが管理している)個々のオブジェクトに対して直接的な依存関係がありませんから、それら雑多なオブジェクトのいずれか一つが変更されても、影響を受ける範囲を制限する事ができます。
このパターンは比較的簡単ですから、パターンを学ぶ前から当たり前のように行っていた方も多いと思います。
【てきとうあとがき】
なんかちょっと消化不良ではありますが、VB のための GoF のパターンをいくつかご紹介してきました。
個人的には、Adapter パターンや Strategy パターン、Composite パターンや Abstract Factory パターンなどについても、実コードを交えつつ紹介したかったのですが、使用頻度的にそれほどでもなかった事と(※当社比)、これ以上は普通に面倒だったので挫折しました。
ただ、概念レベルで見れば、この日記で紹介したパターンに非常に近いパターンがいくつもありますので、とりあえずよく使いそうなものから身に付ければどんどん習得が容易になっていくのではないかなと思います。
それでは、今日はこのへんで…。
【本日の参考文献】
- Erich Gamma, Ralph Johnson, Richard Helm, John Vlissides, 『オブジェクト指向における再利用のためのデザインパターン』,ソフトバンクパブリッシング,1994.
- 結城浩,『Java言語で学ぶ デザインパターン入門』 ,ソフトバンクパブリッシング,2001.
- 平山尚,『ゲームプログラマになる前に覚えておきたい技術』,秀和システム,2008.
0 件のコメント:
コメントを投稿
ひとことどうぞφ(・ω・,,)