2-2. Gamer 구현
Gamer의 역할은 아래와 같습니다.
- 추가로 카드를 받는다.
- 뽑은 카드를 소유한다.
- 카드를 오픈한다.
receiveCard와 cards를 담을 구현체를 생성자에 추가하였습니다.
public class Gamer {
private List<Card> cards;
public Gamer() {
cards = new ArrayList<>();
}
public void receiveCard(Card card) {
this.cards.add(card);
}
public List<Card> openCards(){
return null;
}
}
Gamer의 경우 사용자가 현재 카드들의 총 Point를 보며 카드를 더 뽑을지 말지를 결정하게 됩니다.
이를 위해서는 Gamer는 현재 카드들을 확인 할 수 있어야 합니다.
그래서 showCards라는 메소드를 통해 이 기능을 구현하겠습니다.
showCards는 Gamer의 역할 입니다. Gamer가 소유한 카드들의 목록을 보여주는 것이기 때문입니다.
public void receiveCard(Card card) {
this.cards.add(card);
this.showCards();
}
public void showCards(){
StringBuilder sb = new StringBuilder();
sb.append("현재 보유 카드 목록 \n");
for(Card card : cards){
sb.append(card.toString());
sb.append("\n");
}
System.out.println(sb.toString());
}
카드를 받을때마다 현재 소유한 카드를 확인해야 하는 것이 필수이니, receiveCard의 마지막 코드로 showCards 메소드가 추가되었습니다.
매번 System.out을 하는 것은 성능상 좋지 않기 때문에 StringBuilder로 출력결과를 모두 작성후 최종 1번만 System.out 할 수 있도록 하였습니다.
(여담으로 알고리즘 문제 사이트에서 결과를 출력하실 경우에도 이렇게 하시는게 결과시간 단축에 도움이 됩니다.)
추가로 openCards 메소드는 현재 갖고 있는 모든 카드들을 전달하는 역할이기 때문에 아주 쉽게 구현이 됩니다.
public List<Card> openCards(){
return this.cards;
}
자 그럼 여기까지 구현된 Gamer를 통해 Game.play 메소드 내용을 작성해보겠습니다.
public void play(){
System.out.println("========= Blackjack =========");
Scanner sc = new Scanner(System.in);
Dealer dealer = new Dealer();
Gamer gamer = new Gamer();
Rule rule = new Rule();
CardDeck cardDeck = new CardDeck();
playingPhase(sc, cardDeck, gamer);
}
private void playingPhase(Scanner sc, CardDeck cardDeck, Gamer gamer) {
String gamerInput;
while(true){
System.out.println("카드를 뽑겠습니까? 종료를 원하시면 0을 입력하세요.");
gamerInput = sc.nextLine();
if("0".equals(gamerInput)) {
break;
}
Card card = cardDeck.draw();
gamer.receiveCard(card);
}
}
playingPhase 메소드를 통해 카드 뽑는 단계를 분리하였습니다.
0은 종료, 그외에는 카드뽑기로 간주하여 진행이 됩니다.
- CardDeck을 통해 카드를 뽑고,
- Gamer가 그 카드를 받고,
- Gamer의 현재 카드를 확인
여기서 중요한 점은 Gamer는 CardDeck이 어떤 과정을 거쳐서 카드를 뽑아주는지 모른다는 것 입니다.
CardDeck 내부에서 (1) 남아 있는 카드 중 하나를 랜덤으로 뽑고, (2) 뽑은 카드는 목록에서 제거 라는 과정을 Gamer가 알 필요는 없다는 것이죠.
Gamer는 단지 CardDeck에게 카드 하나를 뽑아 달라는 요청만 하면 되는 것입니다.
이게 정말 중요한 내용입니다.
객체는 다른 객체에게 요청을 할때, 이렇게 한뒤에 저렇게 하고 마지막으로 어떻게 해달라 라는 식으로 세세하게 요청해서는 안됩니다.
객체는 본인의 역할에 충실하면 됩니다.
CardDeck은 카드를 뽑아 주는 것에,
Gamer는 CardDeck에게 카드를 받는 것에 충실해야 합니다.
만일 각 객체의 책임이 모호하게 구현이 되어 있다면, 차후 변경이 있을 경우 어디까지 수정을 해야하는지 알 수 없는 상황이 올 수도 있습니다.
그러므로 다른 객체에게 요청하는 일은 최대한 해당 객체를 믿고 맡기는 것이 좋습니다.
playingPhase 메소드 작성이 끝났다면, 이제 initPhase 메소드 작성을 진행하겠습니다.
initPhase는 블랙잭 규칙에 따라 처음 시작시 Dealer와 Gamer가 2장씩의 카드를 받는 역할을 담당할 예정입니다.
private static final int INIT_RECEIVE_CARD_COUNT = 2;
private void initPhase(CardDeck cardDeck, Gamer gamer){
System.out.println("처음 2장의 카드를 각자 뽑겠습니다.");
for(int i=0;i<INIT_RECEIVE_CARD_COUNT;i++) {
Card card = cardDeck.draw();
gamer.receiveCard(card);
}
}
구현부는 크게 어려울 것이 없습니다.
단순히 for문을 통해 카드를 뽑아 gamer에게 카드를 전달해준 것이 전부입니다. (Dealer는 차후 챕터에서 코드를 추가할 예정입니다.)
여기서 눈여겨 보셔야할 것은 for문의 반복횟수인 2회를 static 상수로 선언한 것입니다.
i<2
로 작성해도 똑같은 기능이 작동될 것입니다. 그럼에도 이렇게 상수로 선언한 이유는 매직넘버를 피하기 위함입니다.
매직넘버란 정체를 알 수 없지만 특정 기능을 하는 마법의 숫자를 얘기합니다.
여기서는 처음 시작시 카드를 받는 횟수인 2를 변수나 상수에 담지 않고, 코드에서 그대로 사용하게 되면 매직넘버가 됩니다.
매직넘버를 피해야 하는 이유는 다음과 같습니다.
- 의미가 모호합니다.
- 단순히 2라는 숫자만 있으면 어떤 의미인지 알 수가 없습니다. 이로인해 다른 개발자는 전체 맥락과 코드를 읽어야만 하는 상황이 발생합니다.
- 상수 혹은 변수명으로 의도를 명확히 하는 것이 좋습니다.
- 변경범위를 확인하기 어렵습니다.
- 똑같이 2를 사용하는 A라는 메소드가 하나 더있다고 생각해봅시다.
- 초반 카드뽑기 횟수가 2->3으로 늘어날 경우 A메소드의 2도 3으로 변경해야 할까요? 변경하는 것은 확실한가요?
- 특히나 0, 1, 10 등 빈번하게 사용되는 숫자를 전부 매직넘버로 처리할 경우 히스토리를 알지못하면 변경시 치명적인 버그를 발생시킬 수 있습니다.
initPhase 메소드 작성이 끝났으니 play메소드에 initPhase 실행부도 추가하겠습니다.
public void play(){
System.out.println("========= Blackjack =========");
Scanner sc = new Scanner(System.in);
Dealer dealer = new Dealer();
Gamer gamer = new Gamer();
Rule rule = new Rule();
CardDeck cardDeck = new CardDeck();
initPhase(cardDeck, gamer);
playingPhase(sc, cardDeck, gamer);
}
그리고 현재까지의 코드를 실행시키겠습니다.
처음 2장의 카드가 포함되어 총 3장의 카드가 출력되는 것을 확인할 수 있습니다.
Gamer와 관련된 코드는 여기까지입니다.
다음은 Dealer의 구현부를 작성해보겠습니다.