LoginSignup
7
8

More than 3 years have passed since last update.

Golangで、デザインパターン「Strategy」を学ぶ

Posted at

GoFのデザインパターンを学習する素材として、書籍「増補改訂版Java言語で学ぶデザインパターン入門」が参考になるみたいですね。
取り上げられている実例は、JAVAベースのため、Pythonで同等のプラクティスに挑んだことがありました。
Qiita記事: "Pythonで、デザインパターン「Strategy」を学ぶ"

今回は、Pythonで実装した”Strategy”のサンプルアプリをGolangで実装し直してみました。

■ Strategyパターン(ストラテジー・パターン)

Strategyパターンは、コンピュータープログラミングの領域において、アルゴリズムを実行時に選択することができるデザインパターンである。
Strategyパターンはアルゴリズムを記述するサブルーチンへの参照をデータ構造の内部に保持する。このパターンの実現には、関数ポインタや関数オブジェクト、デリゲートのほか、オーソドックスなオブジェクト指向言語におけるポリモーフィズムと委譲、あるいはリフレクションによる動的ダック・タイピングなどが利用される。

UML class and sequence diagram

W3sDesign_Strategy_Design_Pattern_UML.jpg

UML class diagram

strategy.png
(以上、ウィキペディア(Wikipedia)より引用)

□ 備忘録

Strategyパターンでは、アルゴリズムの部分を他の部分と意識的に分離して、そのアルゴリズムとのインタフェース部分だけを規定します。そして、プログラムから委譲によってアルゴリズムを利用する形態になります。
プログラムを改良する場合、Strategyパターンを使っていれば、Strategy役のインタフェースを変更しないように注意して、ConcreteStrategy役だけを修正すればよく、さらに、委譲という緩やかな結びつきを使っているから、アルゴリズムを容易に切り替えることも可能になります

■ "Strategy"のサンプルプログラム

実際に、Strategyパターンを活用したサンプルプログラムを動かしてみて、次のような動作の様子を確認したいと思います。ここでは、TaroHanakoが、ジャンケンを繰り返して勝敗を競うものになります。

<ジャンケンの戦略>

  • Taroは、ジャンケンに勝ったら、次のジャンケンでも同じ手で挑む
  • Hanakoは、"グー", "チョキ", "パー"の順番にジャンケンに挑む
$ go run Main.go 
Winner:[Hana: 0games,  0win,  0lose]
Winner:[Taro: 1games,  0win,  1lose]
Winner:[Hana: 2games,  1win,  1lose]
Even...
Even...
Winner:[Hana: 5games,  2win,  1lose]
Winner:[Taro: 6games,  1win,  3lose]
Winner:[Hana: 7games,  3win,  2lose]
Winner:[Hana: 8games,  4win,  2lose]
Winner:[Hana: 9games,  5win,  2lose]
Winner:[Taro: 10games,  2win,  6lose]
Winner:[Hana: 11games,  6win,  3lose]
Even...

...(snip)

Winner:[Taro: 9978games,  2446win,  4964lose]
Winner:[Hana: 9979games,  4964win,  2447lose]
Winner:[Taro: 9980games,  2447win,  4965lose]
Winner:[Hana: 9981games,  4965win,  2448lose]
Winner:[Hana: 9982games,  4966win,  2448lose]
Even...
Winner:[Taro: 9984games,  2448win,  4967lose]
Winner:[Hana: 9985games,  4967win,  2449lose]
Winner:[Taro: 9986games,  2449win,  4968lose]
Winner:[Hana: 9987games,  4968win,  2450lose]
Winner:[Taro: 9988games,  2450win,  4969lose]
Winner:[Hana: 9989games,  4969win,  2451lose]
Even...
Winner:[Hana: 9991games,  4970win,  2451lose]
Winner:[Taro: 9992games,  2451win,  4971lose]
Winner:[Hana: 9993games,  4971win,  2452lose]
Winner:[Taro: 9994games,  2452win,  4972lose]
Winner:[Hana: 9995games,  4972win,  2453lose]
Winner:[Taro: 9996games,  2453win,  4973lose]
Winner:[Hana: 9997games,  4973win,  2454lose]
Even...
Winner:[Taro: 9999games,  2454win,  4974lose]
Total Result:
[Taro: 10000games,  2455win,  4974lose]
[Hana: 10000games,  4974win,  2455lose]

結果は、hanakoのジャンケン戦略("グー", "チョキ", "パー"の順番にジャンケンに挑む)の方が、Taroのジャンケン戦略よりも優れている結果になりました。これは、Taroのジャンケン戦略(ジャンケンに勝ったら、次のジャンケンでも同じ手で挑む)では、絶対に連勝できないためです。

■ サンプルプログラムの詳細

Gitリポジトリにも、同様のコードをアップしています。
https://github.com/ttsubo/study_of_design_pattern_with_golang/tree/master/Strategy

  • ディレクトリ構成
.
├── Main.go
└── strategy
    ├── hand.go
    ├── player.go
    └── strategy.go

(1) Strategy(戦略)の役

戦略を利用するためのインタフェースを定める役です。
サンプルプログラムでは、Strategyインタフェースが、この役を努めます。

strategy/strategy.go
package strategy

import (
    "math/rand"
    "time"
)

// Strategy is interface
type Strategy interface {
    NextHand() *Hand
    study(win bool)
}

(2) ConcreteStrategy(具体的戦略)の役

Strategy役のインタフェースを実際に実装する役です。ここで具体的な戦略(作業・方策・方法・アルゴリズム)を実際にプログラミングします。
サンプルプログラムでは、WinningStrategy構造体と、CircularStrategy構造体が、この役を努めます。

strategy/strategy.go
// WinningStrategy is struct
type WinningStrategy struct {
    won      bool
    prevHand *Hand
}

// NewWinningStrategy func for initializing WinningStrategy
func NewWinningStrategy() *WinningStrategy {
    return &WinningStrategy{
        won:      false,
        prevHand: nil,
    }
}

// NextHand func can handle result of preHand
func (ws *WinningStrategy) NextHand() *Hand {
    if !ws.won {
        rand.Seed(time.Now().UnixNano())
        ws.prevHand = getHand(rand.Intn(3))
    }
    return ws.prevHand
}

func (ws *WinningStrategy) study(win bool) {
    ws.won = win
}
strategy/strategy.go
// CircularStrategy is struct
type CircularStrategy struct {
    hand int
}

// NewCircularStrategy func for initializing CircularStrategy
func NewCircularStrategy() *CircularStrategy {
    return &CircularStrategy{
        hand: 0,
    }
}

// NextHand func can handle result of each Hand
func (cs *CircularStrategy) NextHand() *Hand {
    return getHand(cs.hand)
}

func (cs *CircularStrategy) study(win bool) {
    cs.hand = (cs.hand + 1) % 3
}

(3) Context(文脈)の役

Strategy役を利用する役です。ConcreteStrategy役のインスタンスを持っていて、必要に応じてそれを利用します。
サンプルプログラムでは、Player構造体が、この役を努めます。

strategy/player.go
package strategy

import "fmt"

// Player is struct
type Player struct {
    Name                           string
    strategy                       Strategy
    wincount, losecount, gamecount int
}

// NewPlayer func for initializing Player
func NewPlayer(name string, strategy Strategy) *Player {
    return &Player{
        Name:     name,
        strategy: strategy,
    }
}

// NextHand func can handle result of preHand
func (p *Player) NextHand() *Hand {
    return p.strategy.NextHand()
}

// Win func can judge result as game win
func (p *Player) Win() {
    p.strategy.study(true)
    p.wincount++
    p.gamecount++
}

// Lose func can judge result as game lose
func (p *Player) Lose() {
    p.strategy.study(false)
    p.losecount++
    p.gamecount++
}

// Even func can judge result as game even
func (p *Player) Even() {
    p.gamecount++
}

// ToString func can display results of all games
func (p *Player) ToString() string {
    str := fmt.Sprintf("[%s: %dgames,  %dwin,  %dlose]", p.Name,
        p.gamecount,
        p.wincount,
        p.losecount)
    return str
}

(4) Client(依頼人)の役

サンプルプログラムでは、startMain関数が、この役を努めます。

Main.go
package main

import (
    "fmt"

    "./strategy"
)

func startMain() {
    player1 := strategy.NewPlayer("Taro", strategy.NewWinningStrategy())
    player2 := strategy.NewPlayer("Hana", strategy.NewCircularStrategy())

    for i := 0; i < 10000; i++ {
        hand1 := player1.NextHand()
        hand2 := player2.NextHand()
        if hand1.IsStrongerThan(hand2) {
            fmt.Printf("Winner:%s\n", player1.ToString())
            player1.Win()
            player2.Lose()
        } else if hand1.IsWeakerThan(hand2) {
            fmt.Printf("Winner:%s\n", player2.ToString())
            player1.Lose()
            player2.Win()
        } else {
            fmt.Println("Even...")
            player1.Even()
            player2.Even()
        }
    }

    fmt.Println("Total Result:")
    fmt.Println(player1.ToString())
    fmt.Println(player2.ToString())
}

func main() {
    startMain()
}

(5) その他

ジャンケンの勝敗を管理します。

strategy/hand.go
package strategy

// Const Value for HandGame
const (
    handValueGUU = iota
    handValueCHO
    handValuePAA
)

//Hand is struct
type Hand struct {
    handValue int
}

var hands []*Hand

func init() {
    hands = []*Hand{
        &Hand{handValueGUU},
        &Hand{handValueCHO},
        &Hand{handValuePAA},
    }
}

func getHand(handValue int) *Hand {
    return hands[handValue]
}

// IsStrongerThan func can judge result of handGame as Winner
func (myHand *Hand) IsStrongerThan(opponentHand *Hand) bool {
    return myHand.fight(opponentHand) == 1
}

// IsWeakerThan func can judge result of handGame as Looser
func (myHand *Hand) IsWeakerThan(opponentHand *Hand) bool {
    return myHand.fight(opponentHand) == -1
}

func (myHand *Hand) fight(opponentHand *Hand) int {
    if myHand == opponentHand {
        return 0
    } else if (myHand.handValue+1)%3 == opponentHand.handValue {
        return 1
    } else {
        return -1
    }
}

■ 参考URL

7
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
8