2011/10/24

いまさらVisual Basic(オブジェクト指向篇)

本日は Visual Basic のオブジェクト指向篇です。

とはいえ、今回のお話はどちらかというと 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 インタフェースを実装した各状態クラス(State1State4)の間を遷移するプログラムです。

これによって、『状態の切り替え』と、『状態に応じた振る舞い』を巧妙に実装する事ができます。

状態クラス 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

上記のプログラムを実行すると、以下のように表示されます。

VBCommentPrinterCCommentPrinter オブジェクトともに、いずれも 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 件のコメント:

コメントを投稿

ひとことどうぞφ(・ω・,,)