안녕하세요 오늘은 Strategy 패턴에 대해 알아보고자 합니다.
Strategy라는 의미는 전략이라는 의미를 갖고 있습니다. 보통 전략은 적군을 어떻게 상대하는 등의 어떤 승부를 위한 계획으로 주로 사용하는데요. 프로그래밍으로 치면 알고리즘으로 생각해도 됩니다.
모든 프로그램은 어떤 문제를 해결하기 위해 만들어졌고, 작성되는데 이 문제를 해결하기 위해서 그것에 맞는 알고리즘으로 구현되고 있죠.
Strategy 패턴은 알고리즘을 빈틈없이 교체해서 같은 문제를 다른 방법으로도 쉽게 해결 할 수 있도록 도와주는 패턴이라 생각하면 이해하기 쉬우실 것 같네요.
Strategy패턴으로 만들어 볼 예제는 컴퓨터에서 가위바위보를 실행해주는 코드입니다.
가위바위보의 전략을 두 가지 방법이 있는데 이기면 다음에도 같은 손을 내민다는 것과 직전에 냈던 손에서 다음 낼 손을 확률적으로 계산한다라는 방법입니다.
package bami;
public class Hand {
public static final int HANDVALUE_ROCK = 0;
public static final int HANDVALUE_SCISSORS = 1;
public static final int HANDVALUE_PAPER = 2;
public static final Hand[] hand = {
new Hand(HANDVALUE_ROCK),
new Hand(HANDVALUE_SISSORS),
new Hand(HANDVALUE_PAPER),
};
private static final String[] name = {
"주먹", "가위", "보",
};
private int handvalue;
private Hand(int handvalue) {
this.handvalue = handvalue;
}
public static Hand getHand(int handvalue) {
return hand[handvalue];
}
public boolean isStrongThan(Hand h) {
return fight(h) == 1;
}
public boolean isWeakerThan(Hand h) {
return fight(h) == -1;
}
private int fight(Hand h) {
if (this == h) {
return 0;
} else if ((this.handvalue + 1) % 3 == h.handvalue) {
return 1;
} else {
return -1;
}
}
public String toString() {
return name[handvalue];
}
}
먼저 손을 나타내는 Hand 클래스를 만들어보겠습니다.
클래스 내우베서 주먹은 0, 가위는 1, 보는 2라는 int로 표현하였습니다. 이것을 손의 값을 표시하는 필드에 저장하고, Hand 클래스의 인스턴스는 세 개만 작성되고, 처음에 세개의 인스턴스가 만들어져 배열 hand에 저장됩니다.
클래스 메소드 getHand를 사용하여 인스턴스를 얻을 수 있는데 손의 값을 인수로 할당하면 인스턴스가 반환값이 됩니다.
이것이 싱글톤 패턴의 일종입니다.
isStrongerThan과 isWeakerThan은 승패를 비교하는데 두개의 hand가 있을 경우
hand1.isStrongerThan(hand2)
또는
hand1.isWeakerThan(hand2)
와 같이 사용합니다.
그리고
(this.handvalue + 1) % 3 == h.handvalue
라는 식은 this의 handvalue에 값에 1을 더한 것이 h의 handvalue의 값(this가 주먹이라면 h는 가위, this가 가위라는 h는 보, this가 보라면 h는 주먹)이 되므로 this는 h를 이긴다는 것을 의미합니다.
연산자 %를 사용하여 3의 나머지를 취하고 있는 것은 보인 2값에 1을 더했을 때 주먹인 0값이 나오게 하기 위해서입니다.
package bami;
public interface Strategy {
public abstract Hand nextHand();
public abstract void study(boolean win);
}
그 다음에 만들어 볼 Strategy 인터페이스는 전략을 위한 추상 메소드의 집합입니다.
nextHand는 다음에 내는 손을 얻기 위한 메소드 입니다. 이 메소드가 호출되면 Strategy 인터페이스를 구현하는 클래스는 전략적으로 다음에 낼 손을 결정합니다.
Study는 직전에 낸 손으로 이겼는지, 졌는지를 학습하기 위한 메소드 입니다.
직전의 nextHand 메소드를 호출해서 이길 경우 study(true)로 호출하고, 진 경우 study(false)로 호출합니다.
package bami;
import java.util.Random;
public class WinningStrategy implements Strategy {
private Random random;
private boolean win = false;
private Hand prevHand;
public WinningStrategy(int seed) {
random = new Random(seed);
}
public Hand nextHand() {
if (!win) {
prevHand = Hand.getHand(random.nextInt(3));
}
return prevHand;
}
public void study(boolean iswin) {
win = iswin;
}
}
WinningStrategy 클래스는 위 인터페이스를 구현하는 클래스 중에 하나인데, Strategy 인터페이스를 구현한다는 것은 nextHand와 study라는 두 개의 메소드를 구현하는 것이 됩니다.
이 클래스는 직전 승부에서 이겼으면 다음에도 같은 손을 낸다는 어리석은 전략을 취하죠. 만약 직전 승부에서 졌다면 다음 손은 난수를 사용하여 결정합니다.
random 필드는 이 클래스가 난수를 필요로 할 때 사용할 java.util.Random의 인스턴스를 저장합니다. win 필드는 이전 승부의 결과를 저장하여 이기면 true, 지면 false가 됩니다.
prevHand 필드는 이전 승부에서 내민 것을 저장합니다.
package bami;
import java.util.Random;
public class ProbStrategy implements Strategy {
private Random random;
private int prevHandValue = 0;
private int currenthandVaue = 0;
private int [][] history = {
{1, 1, 1, },
{1, 1, 1, },
{1, 1, 1, },
};
public ProbStrategy(int seed) {
random = new Random(seed);
}
public Hand nextHand() {
int bet = random.nextInt(getSum(currenthandVaue));
int handvalue = 0;
if (bet < history[currenthandVaue][0]) {
handvalue = 0;
} else if (bet < history[currenthandVaue][0] + history[currenthandVaue][1]) {
handvalue = 1;
} else {
handvalue = 2;
}
prevHandValue = currenthandVaue;
currenthandVaue = handvalue;
return Hand.getHand(handvalue);
}
private int getSum(int handvalue) {
int sum = 0;
for (int i = 0; i < 3; i++) {
sum += history[handvalue][i];
}
return sum;
}
public void study(boolean win) {
if (win) {
history[prevHandValue][currenthandVaue]++;
} else {
history[prevHandValue][(currenthandVaue + 1) % 3]++;
history[prevHandValue][(currenthandVaue + 2) % 3]++;
}
}
}
ProbStrategy 클래스는 또 하나의 구체적인 전략을 의미합니다. 다음 손은 난수로 결정하지만 과거 승패의 이력을 사용해 각각 손을 낼 확률을 바꿔주고 있습니다.
histroy필드는 과거의 승패를 반영한 확률 계산을 위한 표를 만듭니다. histroy는 int의 2차원 배열로 각 차원의 첨자는 다음과 같은 의미를 가집니다.
histroy[이전에 낸 것][이번에 낼 것]
이 식의 값이 클 수록 과거의 확률이 높다는 것을 의미하게 되는데
- history[0][1] : 주먹, 주먹을 자신이 냈을 때 과거의 승 수
- history[0][2] : 주먹, 가위을 자신이 냈을 때 과거의 승 수
- history[0][1] : 주먹, 보를 자신이 냈을 때 과거의 승 수
이전에 자신이 주먹을 냈다고 가정했을 때 다음 자신이 무엇을 낼 것인지 위에 적은 값에서 확률을 계산하는 것이죠.
요약해보자면 이 세가지 식의 값을 더해서 0부터 그 수까지 난수를 계산하고 그것을 기초로 다음 손을 결정합니다.
package bami;
public class Player {
private String name;
private Strategy strategy;
private int winCount;
private int loseCount;
private int gameCount;
public Player(String name, Strategy strategy) {
this.name = name;
this.strategy = strategy;
}
public Hand nextHand() {
return strategy.nextHand();
}
public void Win() {
strategy.study(true);
winCount++;
gameCount++;
}
public void Lose() {
strategy.study(false);
loseCount++;
gameCount++;
}
public void even() {
gameCount++;
}
public String toString() {
return "[" + name + ":" + gameCount + " games," + winCount + "win, " + loseCount + " lose" + "]";
}
}
Player 클래스는 가위바위보를 하는 사람을 표현한 클래스입니다.
Player클래스는 이름과 전략이 할당되어 인스턴스를 만들어 줍니다. nextHand 메소드는 다음의 손을 얻기 위한 것이지만, 실제로 다음의 손을 결정하는 것은 자신의 전략입니다.
전략의 nextHand 메소드의 반환값이 그대로 Player의 nextHand 메소드의 반환값이 되고, nextHand 메소드는 자신이 해야 할 처리를 Strategy에게 위임을 하고 있습니다.
이기거나 지거나 비기거나 한 승부의 결과를 다음 승부에 활용하기 위해 Player 클래스는 strategy 필드를 통해서 study 메소드를 호출합니다.
study 메소드를 사용해 전략의 내부 상태를 변화시키고, winCount, loseCount, gameCount는 각 플레이어의 승수를 기록합니다.
package bami;
public class Main {
public static void main(String[] args) {
if (args.length != 2) {
System.out.println("Usage : java Main Radomseed1 Randomseed2");
System.out.println("Example : java main 314 15");
System.exit(0);
}
final int gameCount = 1000;
int seed1 = Integer.parseInt(args[0]);
int seed2 = Integer.parseInt(args[1]);
Player player1 = new Player("바미", new WinningStrategy(seed1));
Player player2 = new Player("철수", new ProbStrategy(seed2));
for (int i = 0; i < gameCount; i++) {
Hand nextHand1 = player1.nextHand();
Hand nextHand2 = player2.nextHand();
if (nextHand1.isStrongThan(nextHand2)) {
System.out.println("Winner : " + player1.toString());
player1.Win();
player2.Lose();
} else if (nextHand2.isStrongThan(nextHand1)) {
System.out.println("Winner :" + player2);
player1.Lose();
player2.Win();
} else {
System.out.println("Even!!");
player1.even();
player2.even();
}
}
System.out.println("Toltal result : ");
System.out.println(player1.toString());
System.out.println(player2.toString());
}
}
드디어 Main 클래스입니다. 앞에 만들었던 클래스를 이용해 실제로 컴퓨터에서 가위바위보를 실행하기 위한 클래스이고, 두 명의 플레이어를 만들어 WinningStrategy 전략과 ProbStrategy 전략을 사용하여 1000번을 대전시켜 결과를 살펴조죠.
'프로그래밍(Basic) > 디자인 패턴(Java)' 카테고리의 다른 글
[바미] Composite 패턴에 대해 알아봅시다. (0) | 2022.01.31 |
---|---|
[바미] Bridge 패턴에 대해 알아봅시다. (0) | 2022.01.24 |
[바미] Builder 패턴에 대해 알아봅시다. (0) | 2021.10.25 |
[바미] Prototype 패턴에 대해 알아봅시다. (0) | 2021.10.04 |
[바미] Singleton 패턴에 대해 알아봅시다. (0) | 2021.09.23 |