LoginSignup
2
1

More than 1 year has passed since last update.

Strategyパターンから理解するGo言語のinterface

Last updated at Posted at 2022-12-22

この記事は Go Advent Calendar 2022 23日目の記事です。

はじめに

Go言語はJavaなどのオブジェクト指向のように明示的な継承関係がない分、interfaceをうまく使いこなせるかがカギになってくるように個人的に感じます。
そのinterfaceの使い方は、オブジェクト指向におけるStrategyパターンが参考になるよ、という話をしようと思います。
読者としてはGoに入門したばかりの初学者を想定しています。

Strategyパターン

Strategyパターンは「GoFのデザインパターン」のなかで紹介されている、クラスやインタフェースの使い方に関する設計パターンです。

図式

Strategyパターンの説明については、結城浩『Java言語で学ぶデザインパターン入門 第3版』に詳しいのでそのまま引用します。

strategyというのは、「戦略」という意味です。敵をやっつけるときの作戦、軍隊を動かすときの方策、それに問題を解いていくときの方法、そういった意味合いを持っています。プログラミングの場合には「アルゴリズム」と考えてもいいでしょう。
どんなプログラムも何らかの問題を解くために書かれており、その問題を解くために特定のアルゴリズムが実装されています。Strategyパターンでは、実装したアルゴリズムをごっそりと交換できるようになっています。アルゴリズム(戦略・作戦・方策)をカチッと切り替え、同じ問題を別の方法で解くのを容易にするパターン、それがStrategyパターンなのです。

strategy-uml

Strategy(戦略)

戦略を利用するためのインタフェースを定める役です。

ConcreteStrategy(具体的戦略)

Strategy役のインタフェースを実際に実装する役です。ここで具体的な戦略(作戦・方策・方法・アルゴリズム)を実際にプログラミングします。

Context(文脈)

Strategy役を利用する役です。ConcreteStrategy役のインスタンスを持っていて、必要に応じてそれを利用します(あくまでも呼び出すのはStrategy役のインタフェース)。

じゃんけんサンプルプログラム:JavaとGoの比較

Strategyパターンについての詳しい説明は、書籍やネット記事に溢れているのでそちらに譲ることにして、実際のJavaとGoのコードを見比べながら、GoにおけるStrategyパターンの実現の仕方を見ていこうと思います。
Javaサンプルコードについては上述『Java言語で学ぶデザインパターン入門 第3版』からお借りしています。

サンプルプログラムの内容

2人のプレイヤーがじゃんけんを行う。じゃんけんの手を出す戦略として、WinningStrategyProbStrategyという方法を考える。
一方のプレイヤーはWinningStrategyに従って手を出し、他方はProbStrategyに従って手を出すとしたら、どちらの勝率が高いかということをシミュレーションするプログラム。

  • Hand:じゃんけんの「手」を表すクラス
  • Strategy:じゃんけんの「戦略」を表すインタフェース
  • WinningStrategy:勝ったら次も同じ手を出す戦略を表すクラス
  • ProbStrategy:1回前の手から次の手を確率的に計算する戦略を表すクラス
  • Player:じゃんけんを行うプレイヤーを表すクラス
  • Main:動作テスト用のクラス

janken-uml

以下のソースコードは、説明のために一部を抜粋したものです。
全量についてはGitHubで公開しているので、そちらもご参照ください。

Hand型

Java

public enum Hand {
    // じゃんけんの手を表す3つのenum定数
    ROCK("グー", 0),
    SCISSORS("チョキ", 1),
    PAPER("パー", 2);
}

Go

  • Goにはenum型がないので、intを拡張した型(Hand型)を定義し、const識別子iotaでインクリメントする
type Hand int
// じゃんけんの手を表す3つの定数
const (
	Rock     Hand = iota // 0
	Scissors             // 1
	Paper                // 2
)

Strategy(インタフェース)

  • nextHand:「次に出す手を取得する」ためのメソッド
  • study:「さっき出した手によって勝ったどうか」を学習するためのメソッド

Java

public interface Strategy {
    public abstract Hand nextHand();
    public abstract void study(boolean win);
}

Go

type Strategy interface {
	NextHand() Hand
	Study(win bool)
}

WinningStrategy(実装)

  • 前回の勝負に勝ったら次も同じ手を出す
  • 前回の勝負に負けたら次の手は乱数を使って決定する

Java

  • Strategy(インタフェース)で定義した2つのメソッド(nextHand(), study())をオーバーライドする
public class WinningStrategy implements Strategy {
    private Random random;
    private boolean won = false;
    private Hand prevHand;

    public WinningStrategy(int seed) {
        random = new Random(seed);
    }

    @Override
    public Hand nextHand() {
        if (!won) {
            prevHand = Hand.getHand(random.nextInt(3));
        }
        return prevHand;
    }

    @Override
    public void study(boolean win) {
        won = win;
    }
}

Go

  • Javaのように明示的なimplements@Overrideのようなキーワードはない
  • そのかわりに構造体の型(WinningStrategy)に対して、Strategy(インタフェース)で定義した2つのメソッド(NextHand(), Study())を追加してあげることで、WinningStrategyStrategy(インタフェース)である条件を満足する(WinningStrategy型はStrategyインタフェースを実装していることになる)
type WinningStrategy struct {
	random   *rand.Rand
	won      bool
	prevHand Hand
}

func NewWinningStrategy(seed int64) *WinningStrategy {
	return &WinningStrategy{
		random: rand.New(rand.NewSource(seed)),
	}
}

func (s *WinningStrategy) NextHand() Hand {
	if !s.won {
		s.prevHand = Hand(s.random.Intn(3))
	}
	return s.prevHand
}

func (s *WinningStrategy) Study(win bool) {
	s.won = win
}

ProbStrategy(実装)

  • 過去の勝ち負けの履歴を使って、それぞれの手を出す確率を変える

細かい実装は重要でなくて、ProbStrategyWinningStrategyと同様にStrategy(インタフェース)で定義した2つのメソッドを実装していることが重要。

Java

public class ProbStrategy implements Strategy {
    private Random random;
    private int prevHandValue = 0;
    private int currentHandValue = 0;
    private int[][] history = {
        { 1, 1, 1, },
        { 1, 1, 1, },
        { 1, 1, 1, },
    };

    public ProbStrategy(int seed) {
        random = new Random(seed);
    }

    @Override
    public Hand nextHand() {
        int bet = random.nextInt(getSum(currentHandValue));
        int handvalue = 0;
        if (bet < history[currentHandValue][0]) {
            handvalue = 0;
        } else if (bet < history[currentHandValue][0] + history[currentHandValue][1]) {
            handvalue = 1;
        } else {
            handvalue = 2;
        }
        prevHandValue = currentHandValue;
        currentHandValue = handvalue;
        return Hand.getHand(handvalue);
    }

    @Override
    public void study(boolean win) {
        if (win) {
            history[prevHandValue][currentHandValue]++;
        } else {
            history[prevHandValue][(currentHandValue + 1) % 3]++;
            history[prevHandValue][(currentHandValue + 2) % 3]++;
        }
    }

    private int getSum(int handvalue) {
        // 省略
    }
}

Go

type ProbStrategy struct {
	random      *rand.Rand
	prevHand    Hand
	currentHand Hand
	history     [][]int
}

func NewProbStrategy(seed int64) *ProbStrategy {
	return &ProbStrategy{
		random:      rand.New(rand.NewSource(seed)),
		prevHand:    0,
		currentHand: 0,
		history: [][]int{
			{1, 1, 1},
			{1, 1, 1},
			{1, 1, 1},
		},
	}
}

func (s *ProbStrategy) NextHand() Hand {
	bet := s.random.Intn(s.getSum(s.currentHand))
	var hand Hand
	if bet < s.history[s.currentHand][0] {
		hand = Rock
	} else if bet < s.history[s.currentHand][0]+s.history[s.currentHand][1] {
		hand = Scissors
	} else {
		hand = Paper
	}
	s.prevHand = s.currentHand
	s.currentHand = hand
	return hand
}

func (s *ProbStrategy) Study(win bool) {
	if win {
		s.history[s.prevHand][s.currentHand]++
	} else {
		s.history[s.prevHand][(s.currentHand+1)%3]++
		s.history[s.prevHand][(s.currentHand+2)%3]++
	}
}

func (s *ProbStrategy) getSum(hand Hand) int {
	// 省略
}

Player

Java

public class Player {
    private String name;
    private Strategy strategy;

    // 名前と戦略を授けてプレイヤーを作る
    public Player(String name, Strategy strategy) {
        this.name = name;
        this.strategy = strategy;
    }

    // 戦略におうかがいを立てて次の手を決める
    public Hand nextHand() {
        return strategy.nextHand();
    }
}

Go

  • 構造体のフィールドにStrategy(インタフェース)を持っている
  • 次の手を決めるときにはStrategy(インタフェース)NextHand()メソッドを呼び出す
type Player struct {
	name      string
	strategy  Strategy
}

// 名前と戦略を授けてプレイヤーを作る
func NewPlayer(name string, strategy Strategy) *Player {
	return &Player{
		name:     name,
		strategy: strategy,
	}
}

// 戦略におうかがいを立てて次の手を決める
func (p *Player) NextHand() Hand {
	return p.strategy.NextHand()
}

Main

Java

public class Main {
    public static void main(String[] args) {
        // Taroは、WinningStrategyを利用
        Player player1 = new Player("Taro", new WinningStrategy(314));
        // Hanaは、ProbStrategyを利用
        Player player2 = new Player("Hana", new ProbStrategy(15));
        // 10000回勝負
        for (int i = 0; i < 10000; i++) {
            Hand nextHand1 = player1.nextHand();
            Hand nextHand2 = player2.nextHand();
            if (nextHand1.isStrongerThan(nextHand2)) {
                System.out.println("Winner:" + player1);
            } else if (nextHand2.isStrongerThan(nextHand1)) {
                System.out.println("Winner:" + player2);
            } else {
                System.out.println("Even...");
            }
        }
    }
}

Go

  • PlayerをNewするこのタイミングで、どちらのStrategyを利用するかを設定している(それまではPlayer自身がどちらのStrategyを利用するか一切知る必要がない、というのがポイント)
  • すなわち依存オブジェクトの注入をしてあげている(この場合はConstructor Injectionに該当し、PlayerのコンストラクタでStrategyインタフェースの実装を注入している)
func main() {
	// Taroは、WinningStrategyを利用
	player1 := NewPlayer("Taro", NewWinningStrategy(314))
	// Hanaは、ProbStrategyを利用
	player2 := NewPlayer("Hana", NewProbStrategy(15))
	// 10000回勝負
	for i := 0; i < 10000; i++ {
		nextHand1 := player1.NextHand()
		nextHand2 := player2.NextHand()
		if nextHand1.IsStrongerThan(nextHand2) {
			fmt.Printf("Winner:%v\n", player1)
			player1.Win()
			player2.Lose()
		} else if nextHand2.IsStrongerThan(nextHand1) {
			fmt.Printf("Winner:%v\n", player2)
			player1.Lose()
			player2.Win()
		} else {
			fmt.Println("Even...")
			player1.Even()
			player2.Even()
		}
	}
}

おわりに

DIP(依存関係逆転原則)とのつながり

Strategyパターンは、SOLID原則の"D"である依存関係逆転の原則(Dependency Inversion Principle)と深い関連があります。
Robert C. Martin『Clean Architecture』を引用します。

オブジェクト指向とは「ポリモーフィズムを使用することで、システムにあるすべてのソースコードの依存関係を絶対的に制御する能力」である。
これにより、アーキテクトは「プラグインアーキテクチャ」を作成できる。これは、上位レベルの方針を含んだモジュールを下位レベルの詳細を含んだモジュールから独立させることである。下位レベルの詳細はプラグインモジュールとなり、上位レベルの方針を含んだモジュールとは独立して、デプロイおよび開発することが可能となる。

第5章 図5-2 依存関係逆転
cleanarchitecture-fig5_2

この図ではHL1クラスがインタフェースを経由してML1クラスのF()関数を呼び出しています。まさにStrategyパターンの図式そのままです。

ContextがHL1クラスに、StrategyがI(インタフェース)に、ConcreteStrategyがML1クラスに対応する

HL1が直接ML1を呼び出すのではなく、インタフェースを経由して間接的に呼び出すかたちにすることで、依存関係が制御の流れと逆になっています。これが依存関係逆転です。
すなわち、制御の流れとしては変わらずHL1からML1の向き(緑の点線矢印)なのですが、依存関係の向きはML1からインタフェースIの方向(赤の上矢印)になります。

これの何が嬉しいかと言うと、インタフェースを間に挟むことで、上位レベルのクラス(HL1)が下位レベルのクラス(ML1)の詳細に引っ張られなくなります。上位レベルのクラスはインタフェースに対してプログラミングすればよくなり(実際に呼び出す下位クラスの具体的な実装については知らなくよくなり)、下位クラスはインタフェースの振る舞いを全うするようにコーディングしてあげればよくなります。
それによってお互いの責務が明確になり、さらにはお互いの変更が影響し合わないため改修がしやすい構造になります。

2
1
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
2
1