VisualBasic.Net　TemplateMethodでリファクタリング

VB.netでのTemplateMethodのサンプルです。
仕事でたまたまできたので備忘録的に残します。
リファクタリング対象ではないソースコードやエラー制御等は省いて記載しています。

## 修正前のコード

メインの処理です。
複数のシステムから顧客情報を取得してCSVに出力する流れです。
データベースはSQL Serverで、各システム毎にDB接続文字列や情報取得のSQLは変わりますが、システムの種類（ここではSunとMoon）が同じであれば、システム構成は同じという前提です。（顧客1・顧客2はSunシステム、顧客3・顧客4はMoonシステムを使用というイメージ。）
SunDataCollector、MoonDataCollectorをテンプレートメソッド化します。
テンプレートメソッドに関係のない処理については記載を省略しています。

```vb:Main.cls
Public Class Main
    Public Sub CreateCustomerCSV()

        'データ抽出対象のDB接続情報取得
        Dim DbSettings As New List(Of DbSetting)
        DbSettings = GetDbSetting

        'CSV作成オブジェクト
        Dim CsvCreater As New CsvCreater

        '取得した接続先毎に処理
        For Each DbSetting In DbSettings

            Dim Customers As New List(Of CustomerData)

            Select Case DbSetting.SystemType
                Case "Sun"
                    'Sunシステム用のデータ取得
                    SunDataCollector.GetCustomerData(DbSetting, Customers)  '←リファクタリング対象
                Case "Moon"
                    'Moonシステム用のデータ取得
                    MoonDataCollector.GetCustomerData(DbSetting, Customers)  '←リファクタリング対象
            End Select

            '取得したデータでCSVファイル出力
            CsvCreater.CreatePatientCsv(DbSetting, Customers)
        Next
    End Sub
End Class
```

Sunシステム用のデータ取得クラスです。

```vb:SunDataCollector.vb
Public Class SunDataCollector

    '顧客情報取得
    Friend Shared Sub GetCustomerData(ByVal DbSettingAs DbSetting, ByVal CustomersAs List(Of CustomerData)) As　Result
        'DB接続
        Dim ConnectionString As String = CreateConnectionString(DbSetting)
        Using connection As New SqlClient.SqlConnection(ConnectionString)

            'データ取得
            Dim sql As String = GetInPatientSQL()
            Dim command As New SqlClient.SqlCommand(sql, connection)
            connection.Open()
            Dim dr As SqlClient.SqlDataReader = command.ExecuteReader()
            SetPatientData(FacilitySetting, dr, Patients) 
        End Using
    End Sub

    'DB接続オブジェクトからB接続文字列生成（オブジェクトの詳細は省略）
    Private Shared Function CreateConnectionString(DbSetting As DbSetting) As String
        'DB接続文字列（SQL Server認証）
        Return "Data Source=" & DbSetting.DbInstance &
                           ";Initial Catalog=" & DbSetting.DbName &
                           ";User Id=" & DbSetting.DbUser &
                           ";Password=" & DbSetting.DbPw
    End Function

   '顧客情報取得SQL生成（SQLの詳細は省略）
   Private Shared Function GetInPatientSQL() As String
        'Sunシステム用の顧客情報取得SQL
        Return "SELECT CustomerID,CusutomerName・・・"
    End Function


    '取得データを顧客オブジェクトに設定
    Private Shared Sub SetCustomerData(ByVal DbSetting As DbSetting , ByVal dr As SqlClient.SqlDataReader, ByVal Customers As List(Of CustomerData)) As Result

        Do While dr.Read
            Dim Customer As New CustomerData
            With Customer
                .CustomerId = dr("CustomerID").ToString
                .Name = dr("CustomerName").ToString
                ・・・・（省略）
            End With
            Customers.Add(Customer)
        Loop
    End Sub
End Class

```

Moonシステム用のデータ取得クラスです。
Sunシステムとは接続文字列、SQLが異なります。

```vb:MoonDataCollector.vb
Public Class MoonDataCollector

    '顧客情報取得
    Friend Shared Sub GetCustomerData(ByVal DbSettingAs DbSetting, ByVal CustomersAs List(Of CustomerData)) As　Result
        'DB接続
        Dim ConnectionString As String = CreateConnectionString(DbSetting)
        Using connection As New SqlClient.SqlConnection(ConnectionString)

            'データ取得
            Dim sql As String = GetInPatientSQL()
            Dim command As New SqlClient.SqlCommand(sql, connection)
            connection.Open()
            Dim dr As SqlClient.SqlDataReader = command.ExecuteReader()
            SetPatientData(FacilitySetting, dr, Patients) 
        End Using
    End Sub

    'DB接続オブジェクトからB接続文字列生成（オブジェクトの詳細は省略）
    Private Shared Function CreateConnectionString(DbSetting As DbSetting) As String
        'DB接続文字列（Windows認証）
        Return "Data Source=" & DbSetting.DbInstance &
                           ";Initial Catalog=" & DbSetting.DbName &
                           ";Integrated Security = SSPI"
    End Function

   '顧客情報取得SQL生成（SQLの詳細は省略）
   Private Shared Function GetInPatientSQL() As String
        'Moonシステム用の顧客情報取得SQL
        Return "SELECT KokyakuID,KokyakuName・・・"
    End Function


    '取得データを顧客オブジェクトに設定
    Private Shared Sub SetCustomerData(ByVal DbSetting As DbSetting , ByVal dr As SqlClient.SqlDataReader, ByVal Customers As List(Of CustomerData))

        Do While dr.Read
            Dim Customer As New CustomerData
            With Customer
                .CustomerId = dr("KokyakuID").ToString
                .Name = dr("KokyakuName").ToString
                ・・・・（省略）
            End With
            Customers.Add(Customer)
        Loop

    End Sub

End Class

```

SunシステムとMoonシステムでは接続文字列、SQLが異なりますが、DB接続、データ取得という流れは変わりません
GetCustomerDataメソッドが全く同じことに気づきました。
そこで、テンプレートメソッドを適用します。


## テンプレートメソッド適用のコード

DataCollector という抽象クラスを作り、Sun、Moonそれぞれのインスタンスをセットして、各データを取得するように変更しました。

```vb:Main.cls
Public Class Main
    Public Sub CreateCustomerCSV()

        'データ抽出対象のDB接続情報取得
        Dim DbSettings As New List(Of DbSetting)
        DbSettings = GetDbSetting

        'CSV作成オブジェクト
        Dim CsvCreater As New CsvCreater

        '取得した接続先毎に処理
        For Each DbSetting In DbSettings

            Dim Customers As New List(Of CustomerData)
            Dim DataCollector As New DataCollector  '←データ取得抽象クラス

            Select Case DbSetting.DbType
                Case "Sun"
                    'Sunシステム用のデータ取得
                    DataCollector = New SunDataCollector '←Sun用のインスタンスセット
                Case "Moon"
                    'Moonシステム用のデータ取得
                    DataCollector = New MoonDataCollector '←Moon用のインスタンスセット
            End Select

            'DB接続情報を使ってデータ取得
            DataCollector.GetCustomerData(DbSetting, Customers)

            '取得したデータでCSVファイル出力
            CsvCreater.CreatePatientCsv(Patients)
        Next
    End Sub
End Class
```

テンプレートとなるデータ取得の抽象クラスです。
データを取得するGetCustomerDataメソッドは実装していますが、その中で使用する他のメソッドはシステム毎に処理内容が異なるのでメソッドの定義のみです。実装は各システム用のクラスで行います。

```vb:DataCollector.vb
Public MustInherit Class DataCollector
 
    '顧客情報取得
    Friend Sub GetCustomerData(ByVal DbSetting As DbSetting, ByVal Customers As List(Of CustomerData))

        'DB接続
        Dim ConnectionString As String = CreateConnectionString(DbSetting)
        Using connection As New SqlClient.SqlConnection(ConnectionString)

        'データ取得
        Dim sql As String = GetInCustomerSQL()
        Dim command As New SqlClient.SqlCommand(sql, connection)
        connection.Open()
        Dim dr As SqlClient.SqlDataReader = command.ExecuteReader()
        SetCustomerData(DbSetting, dr, Customers)
        End Using

    End Function

    'DB接続文字列生成
    Protected MustOverride Function CreateConnectionString(DbSetting As DbSetting) As String

    'SQL生成
    Protected MustOverride Function GetInCustomerSQL() As String

    '取得データを顧客オブジェクトに設定
    Protected MustOverride Function SetCustomerData(ByVal DbSetting As DbSetting, ByVal dr As SqlClient.SqlDataReader, ByVal Customers As List(Of CustomerData)) As Result

End Class
```

変更後のSunDataCollectorです。
DataCollectorクラスを継承し、DataCollectorで定義だけした3つの抽象メソッドをここで実装します。
GetCustomerDataメソッドは継承元のDataCollectorで実装しているので、このクラスでは何もしません。

```vb:SunDataCollector.vb
Friend Class SunDataCollector
    Inherits DataCollector

    ''' DB接続文字列生成
    Protected Overrides Function CreateConnectionString(DbySetting As DbySetting) As String
        'DB接続文字列（SQL Server認証）
        Return "Data Source=" & DbySetting.DbInstance &
                           ";Initial Catalog=" & DbySetting.DbName &
                           ";User Id=" & DbySetting.DbUser &
                           ";Password=" & DbySetting.DbPw
    End Function

   '顧客情報取得SQL生成（SQLの詳細は省略）
    Protected Overrides Function GetInPatientSQL() As String
        'Sunシステム用の顧客情報取得SQL
        Return "SELECT CustomerID,CusutomerName・・・"
    End Function

    '取得データを顧客オブジェクトに設定
    Protected Overrides Sub SetPatientData(ByVal DbySetting As DbySetting, ByVal dr As SqlClient.SqlDataReader, ByVal Patients As List(Of PatientData)) As Result
        Do While dr.Read
            Dim Customer As New CustomerData
            With Customer
                .CustomerId = dr("CustomerID").ToString
                .Name = dr("CustomerName").ToString
                ・・・・（省略）
            End With
            Customers.Add(Customer)
        Loop
    End Sub
End Class
```


変更後のMoonDataCollectorです。
MoonDataCollectortorと同様の変更となります。

```vb:MoonDataCollector.vb
Friend Class MoonDataCollector
    Inherits DataCollector

    'DB接続文字列（Windows認証）
    Protected Overrides Function CreateConnectionString(DbySetting As DbySetting) As String
        'DB接続文字列（Windows認証）
        Return "Data Source=" & DbSetting.DbInstance &
                           ";Initial Catalog=" & DbSetting.DbName &
                           ";Integrated Security = SSPI"
     End Function

   '顧客情報取得SQL生成（SQLの詳細は省略）
    Protected Overrides Function GetInPatientSQL() As String
        'Moonシステム用の顧客情報取得SQL
        Return "SELECT KokyakuID,KokyakuName・・・"
    End Function

    '取得データを顧客オブジェクトに設定
    Protected Overrides Sub SetPatientData(ByVal DbySetting As DbySetting, ByVal dr As SqlClient.SqlDataReader, ByVal Patients As List(Of PatientData)) As Result
         Do While dr.Read
            Dim Customer As New CustomerData
            With Customer
                .CustomerId = dr("KokyakuID").ToString
                .Name = dr("KokyakuName").ToString
                ・・・・（省略）
            End With
            Customers.Add(Customer)
        Loop
    End Sub
End Class
```

システムの種類が増えれば、SunDataCollector、MoonDataCollectorに続いて、～DataCollectorクラスが増えていく形になります。

テンプレートメソッドの話としてはこれで終わりなのですが、もう少し手を加えたいと思います。
MainクラスのCreateCustomerCSVメソッドの中で、「Select Case DbSetting.DbType」によって、セットするインスタンスを判断していますが、DbTypeの種類が増える毎にCaseが増えていくことになります。種類が多くなると見た目があまりきれいではありません。
そこでもう一工夫加えてみます。

## データ取得オブジェクト生成クラス追加後

データ取得オブジェクト生成クラスを追加しました。
DataCollectorクラス型を値に持つCollectorsというディクショナリを持っておきます。GetInstanceメソッドでは、引数で指定されたキー（システム名）に対応したDataCollectorクラスを返します。

```vb:CollectorFactory.vb
Public Class CollectorFactory

    Private Shared Collectors As New Dictionary(Of String, DataCollector) From {
            {"Sun", New SunDataCollector},
            {"Moon", New MoonDataCollector}
        }

    Friend Shared Function GetInstance(ByVal DbType As String) As DataCollector
        Return Collectors(DbType)
    End Function

End Class
```

上記クラスを利用したMainクラスです。

```vb:Main.cls
Public Class Main
    Public Sub CreateCustomerCSV()

        'データ抽出対象のDB接続情報取得
        Dim DbSettings As New List(Of DbSetting)
        DbSettings = GetDbSetting

        'CSV作成オブジェクト
        Dim CsvCreater As New CsvCreater

        '取得した接続先毎に処理
        For Each DbSetting In DbSettings

            Dim Customers As New List(Of CustomerData)
            Dim DataCollector As New DataCollector  '←データ取得抽象クラス

            'Select Case DbSetting.DbType
            '    Case "Sun"
            '        'Sunシステム用のデータ取得
            '        DataCollector = New SQLServerDataCollector '←Sun用のインスタンスセット
            '    Case "Moon"
            '        'Moonシステム用のデータ取得
            '        DataCollector = New MySQLDataCollector '←Moon用のインスタンスセット
            'End Select

            '↓上記のSelect Caseブロックは以下の1行ですむ

            'データ取得クラス
            DataCollector = CollectorFactory.GetInstance(DbSetting.DbType)

            'DB接続情報を使ってデータ取得
            DataCollector.GetCustomerData(DbSetting, Customers)

            '取得したデータでCSVファイル出力
            CsvCreater.CreatePatientCsv(Patients)
        Next
    End Sub
End Class
```

変更点がわかりやすいようにSelect Caseのブロックをコメントで残していますが、必要なのは
「DataCollector = CollectorFactory.GetInstance(DbSetting.DbType)」
の1行だけです。
見た目がスッキリしました。
システムの種類が増えたときは、CollectorFactoryクラスのCollectorsディクショナリの要素を増やす形になります。

## まとめ

DataCollectorクラスのGetCustomerDataメソッドはSQL Serverに接続することが前提となっていますが、ここも工夫すればデータベースの種類（Oracle、MySQLなど）が変わっても使えるようになると思います。

普段の仕事の中でデザインパターンを意識して利用するのは自分にとって正直難しいです。デザインパターンが頭に入ってないといけないし、最初からデザインパターンを適用したコードを書けることはほとんどなく、後から気がついて直すことになります。しかし、時間的な問題や、複数人で作業しているときの影響などで、手がつけられないことがほとんどです。

今回の内容は以下の書籍が役に立ちました。
C#の本ですが、.Netの実践的な使い方が解説されており、非常に参考になりました。
https://gihyo.jp/book/2017/978-4-7741-8758-7