デザインパターン勉強会⑩Strategy

<h1>はじめに</h1>
ZOOM勉強会の議事録です。
<a href="https://www.amazon.co.jp/%E5%A2%97%E8%A3%9C%E6%94%B9%E8%A8%82%E7%89%88Java%E8%A8%80%E8%AA%9E%E3%81%A7%E5%AD%A6%E3%81%B6%E3%83%87%E3%82%B6%E3%82%A4%E3%83%B3%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3%E5%85%A5%E9%96%80-%E7%B5%90%E5%9F%8E-%E6%B5%A9/dp/4797327030">「増補改訂版Java言語で学ぶデザインパターン入門」</a>を読んで、プログラム作成・パターンの理解を行います。
第10回はStrategyパターンです。

<h1>Strategyパターン</h1>
プログラムを切り替え、ポリモーフィズム・多態性を実現する方法です。
役割は以下です。

<ul><li><b>Strategy</b></li>ロジックを切り替えるためのインタフェース<li><b>ConcreteStrategy</b></li>Strategyの具体的な実装。切り替えられるクラス<li><b>Context</b></li>Starategyを利用する役</ul>

本ではじゃんけんプログラムを取り上げていましたが、今回は自作プログラムで説明します。

<h2>リファクタリング前</h2>
入力された値に応じて返答を返すだけのプログラムです。
また、コマンドライン引数の値に応じて言語が変わります。

```vb:Main
    Sub Main(ByVal args() As String)
        If args.Length = 0 Then
            Return
        End If

        While True
            Dim line = Console.ReadLine()

            Select Case line
                '会ったときの挨拶
                Case 1
                    'コマンドライン引数の値に応じて言語が代わる
                    If args(0) = 1 Then
                        Console.WriteLine("こんにちは")
                    ElseIf args(0) = 2 Then
                        Console.WriteLine("Hello")
                    ElseIf args(0) = 3 Then
                        Console.WriteLine("안녕하세요")
                    Else
                        Return
                    End If
                'お礼
                Case 2
                    If args(0) = 1 Then
                        Console.WriteLine("ありがとう")
                    ElseIf args(0) = 2 Then
                        Console.WriteLine("Thank you")
                    ElseIf args(0) = 3 Then
                        Console.WriteLine("감사")
                    Else
                        Return
                    End If
                '別れの挨拶
                Case 3
                    If args(0) = 1 Then
                        Console.WriteLine("バイバイ")
                    ElseIf args(0) = 2 Then
                        Console.WriteLine("Goodbye")
                    ElseIf args(0) = 3 Then
                        Console.WriteLine("안녕")
                    Else
                        Return
                    End If
                Case Else
                    Exit While
            End Select

        End While

    End Sub
```

<h2>Strategy</h2>
ロジックを切り替えるためのインタフェースです。
今回は言語のインタフェースを用意しました。

```vb:Strategy
Public Interface ILanguage

    Sub Hello()

    Sub Thanks()

    Sub GoodBye()

End Interface
```

<h2>ConcreteStrategy</h2>

クラスごとに実装内容を変えています。
例として日本語と英語のみ記載しています。

```vb:Japanese
Public Class Japanese : Implements ILanguage
    Public Sub Hello() Implements ILanguage.Hello
        Console.WriteLine("こんにちは")
    End Sub

    Public Sub Thanks() Implements ILanguage.Thanks
        Console.WriteLine("ありがとう")
    End Sub

    Public Sub GoodBye() Implements ILanguage.GoodBye
        Console.WriteLine("バイバイ")
    End Sub
End Class
```

```vb:English
Public Class English : Implements ILanguage
    Public Sub Hello() Implements ILanguage.Hello
        Console.WriteLine("Hello")
    End Sub

    Public Sub Thanks() Implements ILanguage.Thanks
        Console.WriteLine("Thank you")
    End Sub

    Public Sub GoodBye() Implements ILanguage.GoodBye
        Console.WriteLine("Goodbye")
    End Sub
End Class
```

<h2>Context</h2>
リファクタリング後のMainです。
最初にコマンドライン引数でConcreteStrategy役（言語）が決定されます。
次に入力された値に応じて、`ILanguage`インタフェースのメソッドを介してConcreteStrategy役のメソッドが呼ばれるようになります。
Strategyパターンの導入により、`While`内の分岐がシンプルになったことがわかります。
これは、Strategyパターンによって言語選択の分岐を最初の1つにまとめれるようになったためです。
新しい言語を追加するときも`ILanguage`インタフェースを実装した新しい言語クラスを作り最初の`Select Case`に追加するだけで良くなります。
また、`ILanguage`インタフェースのメソッドがすべて実装されている限り、`While`文は変更する必要もなくなりメソッドの呼び出し側（`While`以降の処理）を使い回すことができます。

```vb:Main
    Sub Main(ByVal args() As String)

        If args.Length = 0 Then
            Return
        End If

        Dim language As ILanguage

        'コマンドライン引数に応じて言語が決定
        Select Case args(0)
            Case 1
                language = New Japanese()
            Case 2
                language = New English()
            Case 3
                language = New Korean()
            Case Else
                Return
        End Select

        While True
            Dim line = Console.ReadLine()

            '入力された値に応じて表示する言葉を変える
            Select Case line
                Case 1
                    language.Hello()
                Case 2
                    language.Thanks()
                Case 3
                    language.GoodBye()
                Case Else
                    Exit While
            End Select

        End While

    End Sub
```
Strategyパターンの説明は以上です。


<h1>余談</h1>
メソッド1つのインタフェースとデリゲートの挙動が一緒だと思い、どう使い分けるのがいいのかという話がでました。
例として、二項演算を行うプログラムを作成してみました。

<h2>Strategy</h2>
二項演算を定義しているインタフェースです。

```vb:IBinaryOperator
Public Interface IBinaryOperator
    Function Calculate(ByVal leftOpearnd As Double, ByVal rightOperand As Double) As Nullable(Of Double)
End Interface
```

<h2>ConcreteStrategy</h2>

`Plus`クラスと`Minus`クラスは`IBinaryOperator`インタフェースを実装しており、それぞれの`Calculate()`は、引数を足し算した結果、引き算した結果を返します。省略しますが、掛け算・割り算なども実装します。

```vb:
Public Class Plus : Implements IBinaryOperator
    Public Function Calculate(leftOpearnd As Double, rightOperand As Double) As Nullable(Of Double) Implements IBinaryOperator.Calculate
        Return leftOpearnd + rightOperand
    End Function
End Class
```

```vb:
Public Class Minus : Implements IBinaryOperator
    Public Function Calculate(leftOpearnd As Double, rightOperand As Double) As Nullable(Of Double) Implements IBinaryOperator.Calculate
        Return leftOpearnd - rightOperand
    End Function
End Class
```
<h2>Factory</h2>
以下がInterfaceを使った場合のFactoryです。
演算子を表す文字列を受け取るとそれに応じて`IBinaryOperator`インタフェースを実装した具象クラスを返します。

```vb:OperatorFactory
Public Class OperatorFactory
    Public Shared Function Create(ByVal operatorStr As String) As IBinaryOperator
        Select Case operatorStr
            Case "+"
                Return New Plus()
            Case "-"
                Return New Minus()
            Case "*"
                Return New Multiply()
            Case "/"
                Return New Divide()
            Case Else
                Return New NullOperator()
        End Select
    End Function
End Class
```

以下はデリゲートの定義とデリゲートを返すメソッドを定義したModuleです。
今回はVB.NETで定義済みの`Func`を使用します。
こちらも`Calculate()`と同様に`Double`型2つを受け取って`Nullable(Of Double)`型を返すようにしています。

```vb:DelegateFactory
Module DelegateFactory

    Public Function Create(ByVal operatorStr As String) As Func(Of Double, Double, Nullable(Of Double))
        Select Case operatorStr
            Case "+"
                Return AddressOf Plus
            Case "-"
                Return AddressOf Minus
            Case "*"
                Return AddressOf Multiply
            Case "/"
                Return AddressOf Divide
            Case Else
                Return AddressOf NullOperator
        End Select
    End Function

    Private Function Plus(ByVal leftOperator As Double, ByVal rightOperator As Double) As Nullable(Of Double)
        Return leftOperator + rightOperator
    End Function

    Private Function Minus(ByVal leftOperator As Double, ByVal rightOperator As Double) As Nullable(Of Double)
        Return leftOperator - rightOperator
    End Function

    Private Function Multiply(ByVal leftOperator As Double, ByVal rightOperator As Double) As Nullable(Of Double)
        Return leftOperator * rightOperator
    End Function

    Private Function Divide(ByVal leftOperator As Double, ByVal rightOperator As Double) As Nullable(Of Double)
        Return leftOperator / rightOperator
    End Function

    Private Function NullOperator(ByVal leftOperator As Double, ByVal rightOperator As Double) As Nullable(Of Double)
        Return Nothing
    End Function

End Module
```


<h2>Context</h2>
入力チェック等の部分も作成していましたが省略しています。
インタフェースとデリゲートを使うもので同じ結果が出力されます。

```vb:Context
Module Module1
    Sub Main()
        Console.Write("受付中 > ")
        Dim expressions = Console.ReadLine()

        Dim expressionArray = expressions.Split(" ")

        Dim leftOperand = expressionArray(0)
        Dim operatorStr = expressionArray(1)
        Dim rightOperand = expressionArray(2)

        Dim result As Nullable(Of Double) = UseInterface(leftOperand, operatorStr, rightOperand)
        Dim result2 As Nullable(Of Double) = UseDelegate(leftOperand, operatorStr, rightOperand)

        Console.WriteLine("計算結果:" & result)
        Console.WriteLine("計算結果:" & result2)
        Console.ReadLine()
    End Sub

    Public Function UseInterface(ByVal leftOperand As Double, ByVal operatorStr As String, ByVal rightOperand As Double) As Nullable(Of Double)
        Dim operator1 = OperatorFactory.Create(operatorStr)
        Return operator1.Calculate(leftOperand, rightOperand)
    End Function

    Public Function UseDelegate(ByVal leftOperand As Double, ByVal operatorStr As String, ByVal rightOperand As Double) As Nullable(Of Double)
        Dim operatorFunction = DelegateFactory.Create(operatorStr)
        Return operatorFunction(leftOperand, rightOperand)
    End Function
End Module
```

メソッド1つのインタフェースとデリゲートを使用する2つの方法を紹介しました。
どちらを使うべきかはプロジェクトに合わせて考える必要があるという結論になりました。


今回は簡単な数学の二項演算をする関数を定義したかったものになります。そのような意図を持った抽象化を行うときはインタフェースが良いと考えました。
理由として、インタフェースはより明確に何を抽象化しているかを表現できると考えたからです。
今回の`DelegateFactory`の中には、`Func(Of Double, Double, Nullable(Of Double))`な形の関数を返すことのみが定義されており、それが簡単な二項演算を返すことを表現できていません。
一方、インタフェースは`IBinaryOperator`という名前によって、二項演算子による計算を行っていることを表現できます。

<h1>まとめ</h1>
今回はStrategyパターンについて学びました。
同じ条件で分岐するSelect文やIfElse文が複数の場所に現れた場合には、この方法でリファクタリングするのが良いと思います。
今回は紹介できませんでしたが、NullObjectパターンというStrategyパターンと関係のあるパターンがあることも議論にあがりました。
デザインパターンの中でも使用頻度が高そうなので、しっかり勉強した上で使っていきたいです。