블랙잭 게임 구현(java)(1)에 이어서 이제 코드를 리팩터링 해보자
• 리팩터링 01 : 실제게임에서는 A, J, Q, K을 1, 10, 10, 10으로 표현하지 않는다.
현실에 맞게 변경해 보자
• 리팩터링 02 : Card 객체의 책임을 중점으로 getCardValue() 메서드 리팩터링
• 리팩터링 03 : CardDeck 객채 성능을 개선해 보자
🥔 리팩터링 01
🍟 문제점
전 게시물 Application.java 실행화면
* 블랙잭 게임 구현(JAVA)(1) 참고
실제 블랙잭 게임에서 A, J, Q, K을 숫자로 표현하지 않는다.
(단지, 점수로 나타낼 때 각각 1, 10, 10, 10점으로 계산할 뿐이다.)
위 콘솔처럼 10으로만 출력되면 이것이 J, Q, K 중 어떤 건지 알 수 없다.
콘솔에 A, J, Q, K으로 출력되게 리팩터링 해보자.
카드의 값은 CardValues.java의 책임이기 때문에 이 클래스를 리팩터링 해보자.
🍟 CardValues 리팩터링
CardValues.java(기존)
public enum CardValues {
ACE("1"),
TWO("2"), THREE("3"), FOUR("4"), FIVE("5"), SIX("6"), SEVEN("7"), EIGHT("8"), NINE("9"), TEN("10"),
JACK("10"), QUEEN("10"), KING("10");
private String value;
CardValues(String value) {
this.value = value;
}
public int getValue() {
return Integer.parseInt(value);
}
}
기존 코드에서 "1", "10", "10", "10"을 "A", "J", "Q", "K"으로 변경하자.
이렇게 변경함에 따라 getValue() 메서드도 변경해야 하는데,
객체지향적인 관점으로 생각해 보니
getValue() 메서드의 returnr값을 굳이 int형으로 변경해서 외부로 줄 필요가 없다.
왜냐하면 CardValues.java는 카드 그대로의 정보를 외부로 알려주는 것이지
"A"가 숫자 1인지, "Q"가 숫자 10인지, "7"이 숫자 7인지는
게임의 룰(Rule)에 따라 언제든 변경될 수 있기 때문이다.
결국 각 카드의 문자가 어떤 값인지 해석하는 것은 Rule의 책무이다.
그래서 getValue() 메서드의 반환타입이 String이 되도록 리팩터링 했다.
CardValues.java(리팩터링 후)
public enum CardValues {
ACE("A"),
TWO("2"), THREE("3"), FOUR("4"), FIVE("5"), SIX("6"), SEVEN("7"), EIGHT("8"), NINE("9"), TEN("10"),
JACK("J"), QUEEN("Q"), KING("K");
private String value;
CardValues(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
CardValues.java 리팩터링 후 getValue()의 return 타입이 int에서 String으로 변경되었기 때문에
getValue()를 호출하는 쪽도 살펴봐야 한다.
CardValues 객체는 Card 객체랑만 소통하기 때문에
getValue() 메서드를 호출하는 부분은 Card 객체뿐이다.
🍟 Card 리팩터링
Card.java(기존)
public class Card {
private CardPatterns pattern;
private CardValues value
Card(CardValues value, CardPatterns pattern) {
this.value = value;
this.pattern = pattern;
}
void printCardInfo() {
System.out.print(pattern.name());
System.out.print(" ");
System.out.println(value.getValue());
}
int getCardValue() {
return value.getValue();
}
}
Card 클래스를 살펴보면 getCardValue() 메서드에서
CardVlues 객체의 getValue() 메서드를 호출하는 것을 알 수 있다.
getValue() 메서드의 return 타입이 String으로 리팩터링 되었기 때문에
이 부분도 아래와 같이 변경해야 함을 알 수 있다.
Card.java(리팩터링 후)
public class Card {
private CardPatterns pattern;
private CardValues value;
Card(CardValues value, CardPatterns pattern) {
this.value = value;
this.pattern = pattern;
}
void printCardInfo() {
System.out.print(pattern.name());
System.out.print(" ");
System.out.println(value.getValue());
}
int getCardValue() {
if (value.getValue().equals("A")) {
return 1;
}
if (value.getValue().equals("J") || value.getValue().equals("Q") || value.getValue().equals("K")) {
return 10;
}
return Integer.parseInt(value.getValue());
}
}
getValue()의 return값에 따라 getCardValue() 메서드가 카드의 값을 해석할 수 있도록 하였다.
🍟 리팩터링 01 이후 Application.java 실행
리팩터링 이후 의도한 데로 잘 동작함을 확인할 수 있다.
🥔 리팩터링 02
🍟 문제점
리팩터링 01을 진행하면서 Card 객체의 getCardValue() 메서드 내부 로직이 눈에 계속 거슬렸다.
이유가 뭘까?? 고민해 보니...
리팩터링 01에서 아래와 같이 언급했는데
"A"가 숫자 1인지, "Q"가 숫자 10인지, "7"이 숫자 7인지는
게임의 룰(Rule)에 따라 언제든 변경될 수 있기 때문이다.
결국 각 카드의 문자가 어떤 값인지 해석하는 것은 Rule의 책무이다.
이것을 제대로 반영하지 못한 것을 느꼈기 때문이다.
getCardValue() 메서드는 카드의 값을 블랙잭 규칙에 맞는 점수로 환산하여 return 하는 로직인데,
이것은 Card 객체의 책임이 아니다.
각 Card 객체의 값에 따라 점수가 어떻게 환산되는지는 Card 객체가 알 필요가 없다.!!
점수를 환산하는 것은 Rule 객체의 책무이다.!!!
이제 문제점을 알았으니 리팩토링을 해보자
🍟 객체 간 역할 추상화
먼저 기존 '객체 간 역할 추상화'를 살펴보자.
객체 간 역할 추상화(기존)
Player, Dealer객체의 totalValue() 메서드가 요청을 하면
Card 객체가 getCardValue() 메서드를 통해 스스로의 점수를 환산해서
점수값을 각각 Palyer, Dealer 객체에게 전달하고 있다.
점수 환산 기능을 Rule 객체가 수행할 수 있도록
객체 간 역할 추상화를 재설계 해보자.
객체 간 역할 추상화(리팩터링 후)
Rule 객체의 convertScore() 메서드를 통해서 점수 환산 기능을 할 수 있도록 했다.
(점수 환산 책임 이동 : Card 객체 → Rule 객체)
책임을 이동하면서 추가로 리팩터링 한것(빨간색 부분)이 많은데
코드를 보면서 하나하나 분석해 보자
🍟 Card 리팩터링
Card.java(기존)
public class Card {
private CardPatterns pattern;
private CardValues value;
Card(CardValues value, CardPatterns pattern) {
this.value = value;
this.pattern = pattern;
}
void printCardInfo() {
System.out.print(pattern.name());
System.out.print(" ");
System.out.println(value.getValue());
}
int getCardValue() {
if (value.getValue().equals("A")) {
return 1;
}
if (value.getValue().equals("J") || value.getValue().equals("Q") || value.getValue().equals("K")) {
return 10;
}
return Integer.parseInt(value.getValue());
}
}
Card.java(리팩터링 후)
public class Card {
private String pattern;
private String value;
Card(CardValues value, CardPatterns pattern) {
this.value = value.getValue();
this.pattern = pattern.name();
}
void printCardInfo() {
System.out.print(pattern);
System.out.print(" ");
System.out.println(value);
}
String getCardValue() {
return value;
}
String getCardPattern() {
return pattern;
}
}
Card 클래스의 메서드 리팩터링
1. Card의 맴버변수를 CardPattrens, CardValues 타입객체가 아닌
String pattern, String value로 변경하여 카드의 값, 패턴을 Card객체가 직접 소유할 수 있도록 하였다.
2. getCardValue메서드 내부 로직을 Rule객체로 넘겼고,
getCardValue(), getCardPattern()으로 분리하여 각각 값과 패턴을 return할 수 있도록 하였다.
🍟 Player 리팩터링
Player.java(기존)
public class Player {
private Card firstCard;
private Card secondCard;
Player(Card firstCard, Card secondCard) {
this.firstCard = firstCard;
this.secondCard = secondCard;
}
void printCard() {
System.out.print("Player 1st Card : ");
firstCard.printCardInfo();
System.out.print("Player 2nd Card : ");
secondCard.printCardInfo();
System.out.println();
}
int totalValue() {
return firstCard.getCardValue() + secondCard.getCardValue();
}
}
Player.java(리팩터링 후)
public class Player {
private Card firstCard;
private Card secondCard;
Player(Card firstCard, Card secondCard) {
this.firstCard = firstCard;
this.secondCard = secondCard;
}
void printCard() {
System.out.print("Player 1st Card : ");
firstCard.printCardInfo();
System.out.print("Player 2nd Card : ");
secondCard.printCardInfo();
System.out.println();
}
String informFirstCard() {
return firstCard.getCardValue();
}
String informSecondCard() {
return secondCard.getCardValue();
}
}
Player 클래스의 메서드 리팩터링
1. 점수의 계산은 오롯이 Rule 객체가 할 수 있도록 totalValue() 메서드를 삭제하였다.
2. Rule 객체가 플레이어 객체의 카드 정보를 알 수 있도록
informFirstCard(), informSecondCard() 메서드 추가하였다.
🍟 Dealer 리팩터링
Dealer.java(기존)
public class Dealer {
private Card firstCard;
private Card secondCard;
Dealer(Card firstCard, Card secondCard) {
this.firstCard = firstCard;
this.secondCard = secondCard;
}
void printCard() {
System.out.print("Dealer 1st Card : ");
firstCard.printCardInfo();
System.out.print("Dealer 2nd Card : ");
secondCard.printCardInfo();
System.out.println();
}
int totalValue() {
return firstCard.getCardValue() + secondCard.getCardValue();
}
}
Dealer.java(리팩터링 후)
public class Dealer {
private Card firstCard;
private Card secondCard;
Dealer(Card firstCard, Card secondCard) {
this.firstCard = firstCard;
this.secondCard = secondCard;
}
void printCard() {
System.out.print("Dealer 1st Card : ");
firstCard.printCardInfo();
System.out.print("Dealer 2nd Card : ");
secondCard.printCardInfo();
System.out.println();
}
String informFirstCard() {
return firstCard.getCardValue();
}
String informSecondCard() {
return secondCard.getCardValue();
}
}
Dealer 클래스의 메서드 리팩터링
1. 점수의 계산은 Rule 객체가 책임질 수 있도록 totalValue() 메서드를 삭제하였다.
2. Rule 객체가 딜러 객체의 카드 정보를 알 수 있도록
informFirstCard(), informSecondCard() 메서드를 추가하였다.
🍟 Rule 리팩터링
Rule.java(기존)
public class Rule {
private Player player;
private Dealer dealer;
Rule(Player player, Dealer dealer) {
this.player = player;
this.dealer = dealer;
}
void printResult() {
System.out.println("Player 총 점수 : " + player.totalValue());
System.out.println("Dealer 총 점수 : " + dealer.totalValue());
if ((player.totalValue() > 21 && dealer.totalValue() > 21) || (player.totalValue() == dealer.totalValue())) {
System.out.println("무승부!!!");
return;
}
if (player.totalValue() > 21) {
System.out.println("플레이어 패배!!");
return;
}
if (dealer.totalValue() > 21){
System.out.println("딜러 패배!! 야호!!");
return;
}
if (player.totalValue() > dealer.totalValue()) {
System.out.println("플레이어 승리");
} else {
System.out.println("딜러 승리");
}
}
void openPlayerCard() {
player.printCard();
}
void openDealerCard() {
dealer.printCard();
}
}
Rule.java(리팩터링 후)
public class Rule {
private Player player;
private Dealer dealer;
Rule(Player player, Dealer dealer) {
this.player = player;
this.dealer = dealer;
}
void printResult() {
int playerScore = showScore(player);
int dealerScore = showScore(dealer);
System.out.println("Player 총 점수 : " + playerScore);
System.out.println("Dealer 총 점수 : " + dealerScore);
if ((playerScore > 21 && dealerScore > 21) || (playerScore == dealerScore)) {
System.out.println("무승부!!!");
return;
}
if (playerScore > 21) {
System.out.println("플레이어 패배!!");
return;
}
if (dealerScore > 21){
System.out.println("딜러 패배!! 야호!!");
return;
}
if (playerScore > dealerScore) {
System.out.println("플레이어 승리");
} else {
System.out.println("딜러 승리");
}
}
private int showScore(Player player) {
int firstCardScore = convertScore(player.informFirstCard());
int secondCardScore = convertScore(player.informSecondCard());
return firstCardScore + secondCardScore;
}
private int showScore(Dealer dealer) {
int firstCardScore = convertScore(dealer.informFirstCard());
int secondCardScore = convertScore(dealer.informSecondCard());
return firstCardScore + secondCardScore;
}
private int convertScore(String value) {
if (value.equals("A")) {
return 1;
}
if (value.equals("J") || value.equals("Q") || value.equals("K")) {
return 10;
}
return Integer.parseInt(value);
}
void openPlayerCard() {
player.printCard();
}
void openDealerCard() {
dealer.printCard();
}
}
Rule 클래스의 메서드 리팩터링
1. convertScore() 메서드를 추가하여 Rule 객체에서 점수를 환산하도록 했다.
2. showScore() 메서드를 추가하여 플레이어와 딜러의 점수를 계산하도록 했다.
3. 1,2번에 따른 변경점이 있음으로 printResult() 메서드를 리팩터링 했다.
🍟 리팩터링 02 이후 Application.java 실행
리팩터링 02 이후 문제없이 잘 작동한다.!!
🥔 리팩터링 03
🍟 문제점
deck이 참조하는 리스트 객체가 ArrayList인데, 이 리스트의 성능이 블랙잭 게임과 잘 어울리나?
의문이 든다.!!!
🍟 CardDeck 리팩터링
CardDeck.java(기존)
import java.util.ArrayList;
public class CardDeck {
private final ArrayList<Card> deck = new ArrayList<>();
private int drawCount = 0;
Card draw() {
int i = (int)(Math.random() * (52 - drawCount));
drawCount++;
Card card = deck.get(i);
deck.remove(i);
return card;
}
void gameStartDeckSetting() {
CardPatterns[] patterns = CardPatterns.values();
CardValues[] Values = CardValues.values();
drawCount = 0;
for (CardPatterns pattern : Pattens) {
for (CardValues value : Values) {
deck.add(new Card(value, pattren));
}
}
}
}
CardDeck 클래스 성능을 개선해 보자.
deck 멤버변수를 보면 Card 타입 객체를 ArrayList 자료구조로 저장하는 것을 알 수 있다.
그런데, draw() 메서드를 살펴보면 draw() 메서드가 호출될 때마다 deck의 Card 객체가 삭제된다.
과연 ArrayList의 삭제 성능이 좋은가? 아니다.
ArrayList는 삭제 시 리스트의 요소들을 하나하나 이동해줘야 하는 단점을 가지고 있다.
즉, n의 시간복잡도를 가지고 있다.
그러면 삭제 시 성능이 좋은 자료구조는 어떤 게 있을까?
LInkedList는 삭제 시 1의 시간복잡도를 가지고 있기 때문에 deck의 자료구조로 알맞다고 판단할 수 있다.
LInkedLIst로 deck의 성능을 개선해 보자
CardDeck.java(리팩터링 후)
import java.util.LinkedList;
import java.util.List;
public class CardDeck {
private final List<Card> deck = new LinkedList<>(); // 성능개선 부분
private int drawCount = 0;
Card draw() {
int i = (int)(Math.random() * (52 - drawCount));
drawCount++;
Card card = deck.get(i);
deck.remove(i);
return card;
}
void gameStartDeckSetting() {
CardPatterns[] patterns = CardPatterns.values();
CardValues[] values = CardValues.values();
drawCount = 0;
for (CardPatterns pattern : pattens {
for (CardValues value : values) {
deck.add(new Card(value, pattren));
}
}
}
}
ArrayList에서 LinkedList로 변경함으로
draw() 메서드 호출 시 Card객체를 deck에서 삭제하는 성능을 개선하였다.(시간복잡도 n → 1)
추가로 deck의 타입을 ArrayList에서 List로 변경하여
List 인터페이스의 구현체로 언제든지 변경할 수 있도록 리팩터링 하였다.
🍟 리팩터링 03 이후 Application.java 실행
리팩터링 03 이후 문제없이 잘 작동한다.!!
'Project' 카테고리의 다른 글
[토이] 블랙잭 게임 구현(java)(6) (0) | 2024.03.05 |
---|---|
[토이] 블랙잭 게임 구현(java)(5) (0) | 2024.03.03 |
[토이] 블랙잭 게임 구현(java)(4) (0) | 2024.03.03 |
[토이] 블랙잭 게임 구현(java)(3) (0) | 2024.03.03 |
[토이] 블랙잭 게임 구현(Java)(1) (0) | 2024.02.27 |