강의) 네오 3단계 블랙잭 피드백(1/4)
블랙잭 시작과 상태 패턴에 강의
📜 제목으로 보기
- 블랙잭
- 도메인 공부후 시작점 찾기
- 캐싱
- TDD 시작점 어디서부터
- 핵심 로직은?
- 시작점은 어디서?
- 응답값이 상수/enum이 행위에 의해 다른 것으로 바뀔 수 있는 상태라면 -> class의 객체로 미리 작성한다.
- 03 테스트는 내부로직 완성전에 응답값 -> then assert문을 미리 작성 -> 클래스와 메서드는 껍데기만 일단 작성해놓기
- 참고) 앞으로 클래스가 상속예정없으면 다 final로 선언하는 버릇 들이기
- 04 Object 응답객체로 받아서 null이 넘어와 실패할 assert문 작성과 동시에 expected에 기입할 응답객체.class로 응답클래스만들어주기
- 05 첫번째 응답객체로 메서드 응답(응답Type+return) 바꿔주기
- 06 2번째 예상응답인 blackjack case로 만들어 주고 로직 추가해주기 -> given예상인자값도 바꿔주자!
- 07 복잡한 case통과를 위한 로직 추가해주기
- 08 최대한 빨리 테스트 통과시키기
- 참고) 도메인vs같/다른도메인의 비교 는 서비스(메인로직 구동) 메서드 내부에서 일단 getter이용해서 하고 -> 도메인로직으로서 클래스를 추출하거나 해서 옮겨야한다.
- 현재 getter를 쓰는 것에 대한 정당성 확보 -> 같은 도메인끼리라도 내부 비교로직이라면 현재 서비스 로직에서 일단 작성해주고 추출해줘야한다.
- but 같은 도메인끼리 or 도메인1개에 대해 단순 상수비교/ 메서드없이 단순집계 등는 개별 일괄처리 or 집계라면 묶어서 -> 메세지 보내서 물어본다` -> 현재의 상황
- 참고) [객체] 내부의 [포장변수 속 변수]를 가져올 때도 getter대신 .최종 나올 변수명()으로 메서드 지어 들어가기
- if분기에 따라 서로 다른형의 객체를 응답해야할 떄 -> 다시 응답을 Object형으로 바꿔주고 -> 다형성을 생각한다 -> 일단 테스트통과부터 확인한다.
- ACE에 대한 처리
- 참고) 2개이상의 같은 형이 개별 일괄처리가 필요할 땐 list에 넣고 -> stream으로 일괄처리 -> 도메인끼리가 아니라 stream 도메인 개별 처리 -> 꺼내지말고 메세지보내서 처리한다
- 다양한class의 객체들 응답시 Object로 응답? 메서드가 많은 일을한다?
- 서비스로직내 도메인로직의 class로 추출
- 10 서비스 로직 내 같은형 객체 2개이상의 일괄처리 로직 -> 일급컬렉션으로 추출
- 객체일괄처리를 위해 뽑아낸 List.of() -> 객체List -> 일괄처리가 메서드로 가는 일급컬렉션 추출과정이 된다.
- 서비스 메서드 내에서 발견된 역할과 객체List -> 메서드 로직 내에서 일급컬렉션 추출해보기
- 포장전 내부까는 코드였다면, 포장전 복붙 or 복사해놓은 상태로 최종메서드만 남기고 -> 내부처리코드를 잘라내 들고들어가 -> list자리에 -> 내부포장되어있던 value만 바꿔넣어준다.
- 1번만 쓰인다면 지역변수 제거하고 inline화
- 참고) 너무빠른 리팩토링이지만, if문이 하나의 도메인에 대한 여러 boolean문장이면, 메세지를 넘길 수 있다. (메서드추출이랑 다름)
- 참고-연결) 메세지를 던지려면, 내부처리로직 코드복사후 -> 최종메서드로 던지기 -> 밖에서 list가 아니라 일급그대로를 썼던 코드는? 내부에서 바로 메서드()형태로 내부메서드를 이용하면 된다.
- 10 서비스 로직 내 같은형 객체 2개이상의 일괄처리 로직 -> 일급컬렉션으로 추출
- 메서드의 응답을 Object -> 응답되는 객체들을 뽑아내서 추상화or상위Type 생성으로 카테고라이징
- 다음 상태로 이어가기 -> 기본 2장받고 가능한 상태:Hit or Blackjack -> 다음엔?? -> 둘 중에 더 만만한 것에서부터상태 변화를 이어나간다.
- 특정 구현체[끝 상태]에서 [상태변화의 전략메서드] 호출인지 / 아니면 단독메서드 호출인지 고민해보기
- 13 게임끝의 상태에서 더 진행하는 상태변화 메서드 호출시 에러발생인데, blackjack상태에서도 재료(인자, card1장)를 받는 공통 전략메서드를 부를 수 있나?
- 중요3) 상카(추상체)가 잡힌이후 메서드의 추가는 (모든 카테고리 구현체 다 가능할 것 같은데, 혹시 모르니) 현재 특정상태(구현체) 1개만 호출 가능한 메서드라고 가정하고 해당 구현체에만 메서드가 생기도록 구현체변수 -> 다운캐스팅 -> 메서드호출() 빨간줄생성하여 코드를 짜보자.
- 14 draw(Card card)가 hit와 blackjack 모든 카테고리가 호출가능할 것 같지만, 보수적으로 blackjack에서만 호출가능한 단독메서드라고 가정해놓고 짜보자.
- 고민 - state가 card를 .draw()로 받아도 되느냐?
- my) 상태값(인변)을 가진다-> 재료로 받아서 시작되거나 재료없이 빈재료로 내부초기화되어서 시작 하며 -> 추가정보 등 내부 상태값이 업데이트 되고, 그 변화된 상태값을 재료로해서 업데이트된 새 객체를 반환해주려면 상태값으로 가져야한다.
- my) 요약: 클래스의 상태값은 재료로 들어와 포장되며 (가공된 뒤 적은 수로)내부에서 가지고 있어야, 변화를 반영한 새객체도 생성시에도 재료로서 생성시 쓰인다라고 할 수 있다.
- 중요4 my) 추상체로서 여러 구현체 state를 가지는 state를 사용하는 순간부터 state 변화를 판단하는 정보들은 state가 내부에서 실시간 반영되는 상태값(인변)가지며 + 전략메서드를 통해 바로 바뀌거나, 필요한 추가정보를 현 정보들과 같이 판단하여 다른 구현체로 바뀐다
- 15 blackjack상태는 끝났다면, hit상태에서 진행해보자.
- 현재 hit상태에서의 목적: 카드1장받는 상태변화 트리거 메서드를 호출하더라도 에러안나고 호출가능 = 응답값 제대로 반환받음(isInstanceOf)증명해야한다.
- 16 hit상태에서 또 draw로 카드받을 수 있나? 받을 수 있다면 단독메서드로서 다운캐스팅후 구현체 변수로 받아놓고 단독메서드로서 가정하고 개발을 시작한다.
- 다운캐스팅 하더라도, 우항에 체이닝으로 개발하면, 다운캐스팅 안먹고 응답되는 추상체로 메서드 개발됨. -> 반드시 좌변 구현체 변수로 받아야 구현체-단독메서드 개발이 가능하다.
- 참고) 어차피 직전 테메를 복붙해온 given의 hit상태가 보장되어있으니, 편하게 다운캐스팅 + 구현체 변수로 받아놓고 -> 단독메서드 개발
- blackjack과 다르게, 예외없이 다음에 올 구현체 상태를 응답해야하므로 밖에서는 state변수를 재할당시켜 업데이트해줘야한다. 하지만, 단독메서드 개발을 위해, 구현체 변수로 받아둔 state를 returnType State의 응답으로 재할당이 불가능하다.
- 다음 상태 트리거 전략메서드는 공통메서드, 추메라면 -> 다양한 구현체 응답을 위해 State 응답이 맞으나 단독메서드 -> 단독구현체로 응답하도록 임시대응 한다.
- 16 hit상태에서 또 draw로 카드받을 수 있나? 받을 수 있다면 단독메서드로서 다운캐스팅후 구현체 변수로 받아놓고 단독메서드로서 가정하고 개발을 시작한다.
- 중요5) 마지막 구현체(hit)의 단독메서드 구현 와중에 -> 상카 아래 모든 구현체(hit, blackjack)들이 <구현은 다르지만 모두 호출 가능>한 공통메서드 -> 인터페이스에선 추메로 추상화하고, 직접 추상화이후 @Override 직접 붙이기
- 중요6 my) 추메로 추상화해도 구상체별로 개별 구현해줘야하는데??? —> 인페의 추메로 추상화 핵심은 코드 중복제거가 아니라 구현체 모두 호출가능시 구현체전용 단독메서드를 올려 다운캐스팅 제거+ 파라미터/응답/변수에서 추상체로 받아 일괄처리 가능해진다.
- 16 구현체 모두가 호출가능할 수 있다면, 인페-추메(전메)로 추상화해서 -> 다운캐스팅 제거후 추상체.전략메서드호출()로 호출이 가능해진다.. 그전에…
- 참고) 구현체 단독메서드 -> 추메/전메로 올리기 전 확인은 다이어그램+ creation mode + method로 하자
- 중요7) 구현체 단독메서드 -> 추메/전메로서 (선언부 시그니쳐만) 위로 올릴 때, 받는 변수/응답값/파라미터 등에 구현체가 있다면, 그것들도 추상체로 바꿔서 올려줘야한다.
- 참고) 인페의 추메/전메로 선언부를 올릴 때, 접근제한자 제거해서 default로 일단 시작하기
- 중요) 인페의 추메/전메로 수동 추상화 올린 다음엔하고 난 뒤, usage-impl 수를 확인해서 개별 구현남아있어야하니 @오버라이딩만 직접 가서 해준다.
- 중요) 추상화 끝난 단독메서드는 더이상 구현체만의 단독메드가 아니다 == 다운캐스팅 안해도 (오버라이딩된) 추상체변수로 받은 상태에서 추메/전메로서 호출가능해진다. -> 다운캐스팅 제거하고 추상체변수로 받도록 수정하기
- 17 hit 에서 메서드 호출가능해서 -> 모든 구현체 호출가능 -> 메서드 추상화 해준 이후 -> 응답으로서 또 여러 경우의수가 나온다. -> hit + ?? -> 복사해놓고 구체적으로 1case를 또 정해주고 빠른통과하도록 메서드짜기
- 18 hit에서 새로운 응답값(= 객체 상태)가 나오는 case에서는 테메이름 + case로 가정인자 조절 + expected로서 assert-isInstanceOf의 새객체.class등 미리 작성하고 하나씩 만들어나간다.
- 19 hit 상태에서 개별구현되고 있는 트리거 메서드에서 호출시 Bust가 응답되도록 메서드 로직 짜주기
- 중요) 개별구현중인 구현체 메서드에서 다른 구현체로 넘어가는 로직짜기 -> 이전 정보를 받아와 by 재료를 생성자를 통해 현재 상태를 나타내줄 정보들을 상태값(인변)로서 내부에 가지고 있어야만 상태패턴의 상태객체다.
- 일급컬렉션 정보(상태값)의 add/remove()는 내부 정보를 변화시키지말고 변화된 내부정보로 일급을 만들어 반환하여 불변 && 밖에서 반환된 것을 이용
- 22) 이전 카드정보들을 상태값으로 보유하고 있는 상황에서 -> 다음 상태로 가기위에 카드1장의 추가정보(메서드인자)를 받아 현재 정보(인변으로 보유)를 업데이트하자
- 23) 불변 일급컬렉션으로 증감시 새객체를 반환해주면 -> 외부에서 .add 해줘도 내부만 변화되는게 아니라 외부에서는 증감이 반영된 내부정보를 포장한 일급을 받아쓴다 -> 메세지 보내서 업데이트된 내부 정보에 대해 물어보기만 하면 된다.
- 24) 다른 구현체가 안되더라도, 같은 구현체로 응답(hit->hit) 되더라도 추가정보를 반영하여 응답된 업데이트된 정보 by 불변 일급를 담아서 -> 업데이트된 구현체로 응답되어야한다.
- 중요) 끝상태에서 -> 트리거 호출시 예외발생으로 종료까지 마무리 해줘야한다.
블랙잭
도메인 공부후 시작점 찾기
-
정제된 rawInput이 들어온다고 가정하고 했었다.
- 자동차 경주: new Car(
"이름"
)의 string 생성부터 -> 응답하는 메서드 테스트 - 로또: rawInput으로
1,2,3,4,5,6
의 list
- 자동차 경주: new Car(
-
블랙잭 로직을 시작하려면 재료
시작전 포장된 카드
->카드덱
이 먼저 필요하다- 카드덱이 카드를 2장씩 제공해야하는데,
포장된 카드
만 있으면 카드덱에서 뽑아서 줬다고 가정하고 메인로직 시작하면 된다.
- 카드덱이 카드를 2장씩 제공해야하는데,
- 카드를
"문자열", 숫자
의 정제된 rawInput으로 가정해도 되지만, 이것은input으로 들어오는게 아니라 원래 생성되어 있어야
하는 것이므로가정하고 -> 나중에 포장
하는게 아니라미리 만들고 시작
한다-
게다가
2개는 같이 붙어서 움직이는 것
이므로 ->미리 클래스로 포장
한다- 로또당첨번호 lotto + bonusNumber
- Player가 가지는 name + cards
-
게다가
캐싱
재료인 Card먼저 만들기
01 Card에는 2가지정보가 붙어서 움직인다.
-
test를 파서
필요한, 붙어다니는 2개의 정보
로 new Card( , ) 생성부터 해본다. -
2가지 정보는
제한된 상수
이므로Enum으로 생성
하고1가지 예시 enum객체
를 활용해서 생성한다.- enum의 예시 ->
enum클래스.대문자_상수
로 사용
@Test void create() { //new Card(new Suit(), new Denomination()); //1. 2가지 정보는 제한된 수의 상수 -> enum으로 받는다고 가정 -> 예시로 생성한다. new Card(Suit.HEART, Denomination.ACE); }
- enum의 예시 ->
-
enum 정보를 복사해오는데, 필드값이 지금 당장 필요하지 않으면 삭제하자.
public enum Denomination { ACE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING, ; }
package blackjack; public enum Suit { CLUBS, DIAMONDS, HEARTS, SPADES }
-
2가지 정보로
new 객체생성 템플릿 주생성자
를 일단 생성해줘야한다.- 캐싱을 하더라도 먼저 기본생성자부터 생성해주자.
public class Card { private final Suit suit; private final Denomination denomination; public Card(final Suit suit, final Denomination denomination) { this.suit = suit; this.denomination = denomination; } }
- 파라미터는 좀 길다면 보기 좋게 2번재 줄로 이동시켜도 된다.
객체 100% 새로 생성 new 단순템플릿
이 아니라 캐싱
등의 능동적 객체 생성 관리자
역할을 by 로직을 포함한 생성자 정펙매
로 할 수 있다.
02 class는 -
캐싱은 생성에 관한 것이며, 정적팩토리메서드를 통해 능동적으로
로직을 포함한 생성
으로 객체생성을 관리할 수 있다.@Test void create() { final Card card = new Card(Suit.HEARTS, Denomination.ACE); //2. [new + 재료]가 아니라, of/from등의 [정펙매 + 재료]로 생성해야 로직을 포함한 생성이 가능하다. Card.of(Suit.HEARTS, Denomination.ACE); }
- 메서드 생성후
파라미터 라인 + 명칭
수정해주고나중가면 final도 달아준다?
- 메서드 생성후
-
캐싱을 하기 위해서는 2가지만 해주면 된다.
- 미리
pr sf
로 클래스 변수 = 상수로서 cache map만들어주기- psf는 public sf/
prsf
은 private이다.
- psf는 public sf/
- 정펙매에서 생성된 CACHE_MAP +
.computeIfAbsent(, )
로 없을 때만new 100%생성 기본생성자로 생성
후 반환 캐싱될 객체는 VO로서 값 같으면 같은객체 취급
해주기 위해eq/hC 오버라이딩
- 미리
-
클래스 변수(상수)로 cache를 선언할 것이므로 -> 메소드 내에서 변수 추출로 형완성을 해가자
-
ctrl+alt+F
로 메서드내부에서 바로 필드(인변/클변으로 뽑을 수 있다)
-
-
2개의 변수를 동시에 가지고 다니는
inner class
를 선언한 뒤,그 class의객체
를 Key로 가지고 가도 된다.(사용한할 것임. 참고만)private static class Key { private final Suit suit; private final Denomination denomination; private Key(final Suit suit, final Denomination denomination) { this.suit = suit; this.denomination = denomination; } }
- 변수 추출시 다형성도 항상 확인하자
-
2개의 변수를 들고다니는 클래스Key를 만들어줘도, Card와 동일한 상태다(2개를 인변으로). 그냥
.name()
을 활용한 string+string의<String,
으로 unique한 1개의 key로 만들어준다. 캐싱의 valueType은 해당 객체로 준다. -
enum은
enum객체 변수명을 그대로 출력
해주는.name()
메서드를 제공해준다.System.out.println(Suit.HEARTS.name() + Denomination.ACE.name());
-
메소드내부에서 변수추출했지만,
scope이동
할 땐,pr
를 포함한sf
로 변경하여접근제한자를 수정
해주자..- 메서드내부에서는
pr
이 안먹는다. public메서드라면..
- 갯수를 미리 알고 있으면 map의 () 생성자에 넣어준다.
public class Card { private static final Map<String, Card> CACHE = new HashMap<>(52);
- 메서드내부에서는
-
캐쉬에 들어갈 key를
1.name() + 2.name()
의 스트링으로 작성해줬다. 만약, 그 key가 없을 경우,해당 key값으로 자동으로 넣어줄 value
를 지정해줘야한다.- 이 때, key값은 활용을 안하므로
value값을 만들어줄 Function 함수의 람다 시작인자
로서 자리에앞선 인자 key를 활용 안하므로 ignored
로 명명해서 작성해주자.
public static Card of(final Suit suit, final Denomination denomination) { return CACHE.computeIfAbsent(suit.name() + denomination.name(), ignored -> new Card(suit, denomination)); }
- 이 때, key값은 활용을 안하므로
기본생성자는 private
로 바꿔서 잠궈줘야한다.
03 캐싱되는 객체는 VO처럼 eq/hC 오버라이딩 해줘야한다. + 정펙매 사용하면 -
같은 값(2개 정보)를 가지면 -> 같은 객체로 판단되어야 재사용시 같은 것으로 확인이 된다.
generate
띄운 후eq
로 빠르게 검색해서 사용하기
-
리팩토리으로서 메서드 인자에, 메소드 연산을 하고 있다. -> 메서드추출
// return CACHE.computeIfAbsent(suit.name() + denomination.name(), // return CACHE.computeIfAbsent(toKey(suit, denomination),
-
정펙메 사용순간부터
기본 생성자
는private
으로 변경- 테스트 코드에 사용했던
new 기본생성자
를.of
로 변경
-
테스트 메서드명도 변경
- create ->
of
- create ->
- 테스트 코드에 사용했던
만든 카드 캐싱 테스트
참고) assertThat()의 import는 qualify static call.. 이후 add static import로 ??
캐싱 테스트: 만든것 .isSameAs( 방금 만든 것 )같은지
class CardTest {
@Test
void of() {
final Card card = Card.of(Suit.HEARTS, Denomination.ACE);
assertThat(card).isSameAs(Card.of(Suit.HEARTS, Denomination.ACE));
}
}
TDD 시작점 어디서부터
핵심 로직은?
- 카드 2장을 받는 것?
-
2장 받은 상태에서 판단을 한다.
- 21이면
블랙잭
- if문이 등장했다. -> 다형성/enum 등…
- 21보다 낮으면(21 아니면)
sit
- 그만 할 수 있다.
stay
- 추가적으로 받을 수 없다.
- hit시, 21보다 아래면
sit
상태로- 계속 받을 수 있다.
- 그만 할 수 있다.
- 21이면
-
행위는 사용자input에 의해 진행되지만, 특정
메서드(카드받기)에 의해 서로 바뀌는 <상호 연결된 상태값>
이라는 밀접한 관계를 가지고 있다.- 응답값이 상태이며,
- (동일)메서드에 의해 실시간으로 바뀌는 상태다..
-
if문이 엄청 나올 것이다 ->
추상화로 if문을 제거한다
- 객체생활
체조원칙
-> 체조는 어디에 좋은지 모르는데, 하다보면 좋아진다.
- 객체생활
시작점은 어디서?
- 재료인 카드 2장을 뽑은
상태
에서부터 시작하여상호 연결된 다른 상태로 바뀌는 메서드
를 호출하면서 시작한다.
01 핵심로직(like Service)의 클래스이름이 안정해졌으면 GameTest클래스 -> Game.start(재료)메서드로 시작
예상 응답값
으로 뭐가 나올지 먼저 고민
한 뒤 여러개면 만만한 1 case 선택 -> 재료:예상 인자값
도 맞춰서 정해준다.
02 재료를 넣고 -> 테메당 1case이므로 여러 응답(상태)이 나온다고 예상되면 -> 그 중 1개를 예상응답값으로 선택하고 -> 예상응답값에 맞는 재료를 넣어줘야한다.
-
메인로직 클래스(game)의 static메서드()에
정제된 rawInput(재료)
으로서 포장 카드 2장을 넣어야한다. -
메서드를 짜기 전, 예상 응답을 먼저 생각해보고
여러 응답값
이 나올 수 있다면,만만한 응답값부터 예상값
으로 정해놓고1 case testmethod
를 작성하자.blackjack은 ace처리
도 해줘야하기 때문에 만만하지 않다.- case가 정해지면 그에 따라 테스트 메서드 이름도 바꿔준다. (start-> hit)
@Test void hit() { //1. 재료: 카드 2장을 넣을 때 -> 응답: if max21 Blackjack or if Hit(orStay)가 나올 수 있다. // -> 먼저 만만한 것을 고른다. // -> 21 아래 값인 hit를 먼저 예상응답으로 정한다. Game.start(); }
-
예상응답값에 대한
예상인자
를 맞춰서 작성해줘야한다.-
hit
를 먼저 예상응답값으로 정했다면 -
hit를 만드는 재료 카드 2장
도 예상인자값으로 맞춰서 넣어줘야한다.
@Test void hit() { //1. 재료: 카드 2장을 넣을 때 -> 응답: if max21 Blackjack or if Hit(orStay)가 나올 수 있다. // -> 먼저 만만한 것을 고른다. // -> 21 아래 값인 hit를 먼저 예상응답으로 정한다. //2. 카드2 -> hit (예상응답값) -> 거기에 맞는 재료 넣어주기 Game.start(Card.of(Suit.SPADES, Denomination.TWO), Card.of(Suit.SPADES, Denomination.FIVE)); }
-
행위에 의해 다른 것으로 바뀔 수 있는 상태
라면 -> class의 객체로 미리 작성한다.
응답값이 상수/enum이
응답값 -> then assert문
을 미리 작성 -> 클래스와 메서드는 껍데기만 일단 작성해놓기
03 테스트는 내부로직 완성전에 참고) 앞으로 클래스가 상속예정없으면 다 final로 선언하는 버릇 들이기
-
메인로직 메서드 보유 클래스(Game)을 만들어주고 **응답값을미리 ** 작성해야한다.
- 클래스 생성만 해주고 넘어가야한다
- 이 때 class가 상속예정이 없으면 final로 선언하는 버릇을 들이자.
public final class Game { }
아직 작성안된 class의 객체
가 있을 예정이라면 Object
로 응답해주고 (실패용)return null;
하는 메서드 작성하기
응답값 -
빈 메서드 완성해주기
-
응답값 객체가 있을 예정이면
Object
로 일단 응답시키고return null;
로 작성하자return null;
로 초기 작성해줘야 실패를 해서 고친다.
- 순서가 있는 파라미터는
first, second
로 네이밍해놓자 - 파라미터는 줄맞춤 해도 된다.
public final class Game { public static Object start(final Card first, final Card second) { return null; } }
-
응답값 객체가 있을 예정이면
Object 응답객체
로 받아서 null이 넘어와 실패할 assert문
작성과 동시에 expected에 기입할 응답객체.class로 응답클래스
만들어주기
04
.isInstanceOf( 클래스.class)
로 확인하면서 class작성해주기
객체 응답은 -
Object [미완성응답객체]
로 받아주고 -> 확인을.isIntanceOf(Hit.class)
로 해주면서 클래스를 만든다@Test void hit() { // given & when : hit final Object hit = Game.start(Card.of(Suit.SPADES, Denomination.TWO), Card.of(Suit.SPADES, Denomination.FIVE)); // then assertThat(hit).isInstanceOf(Hit.class); }
public final class Hit { }
-
클래스를 채우거나 완성하지 않고 테스트를 돌린다.
-
return null;
의 미완성 상태라 실패한다.
-
05 첫번째 응답객체로 메서드 응답(응답Type+return) 바꿔주기
-
null이 넘어가는 상황을
현재 case의 응답객체
로 바꿔줘야 케이스가 통과될 것이다.- 기존
public final class Game { public static Object start(final Card first, final Card second) { return null; } }
- 변경 후
public final class Game { public static Hit start(final Card first, final Card second) { return new Hit(); } }
-
테스트 돌려보면 통과된다.
06 2번째 예상응답인 blackjack case로 만들어 주고 로직 추가해주기 -> given예상인자값도 바꿔주자!
바뀔 수 있는 응답객체의 변수Type을 Object로 유지
해주기
다형성 적용전까진, - 그래야
다형성 적용 전, case 추가해서 받아줘도 테스트들이 다 통과
될 것이다.
-
테메 복사 -> 테메이름 -> 변수이름 -> 예상 응답값 ->
예상인자값given
-> expeceed 모두 해당case로 바꿔주기 -
없으면 해당 클래스도 만들어준다. -> final + enter 활용
- tab누르면 추가가 아니라 단어 덮어쓰기 되서 class가 사라지더라.
public final class Blackjack { }
-
예상인자값도 바꿔주야한다.
- ace + jack으로 바꿔줌
-
테스트는 로직 추가/수정이 없으니 통과안한다.
07 복잡한 case통과를 위한 로직 추가해주기
- blackjack은 단순if문으로 통과할 순 없다.
-
카드의 합이 21이 될 때 return
new Blackjack()
해주면 되는데, 지금은 점수를 가지고 올 수 없다.- Card -> Denomination -> 점수를 가지고 와야함.
public static Hit start(final Card first, final Card second) { return new Hit(); }
|
필요에 의해 enum에 필드 추가해주기
-
여러개 선택은 커서가 물리는 중간에서 시작해서 -> 같이 움직인다.
- 1~2자리 숫자면, 2자리 숫자로 입력해서 지우는 식으로 가자.
참고) enum에 필드를 만들어줬다는 말은 밖에서 갖다 쓸거라는 말이다 -> getter가 될 준비를 한다.
-
나중에 필요에 의해 작성할것이지만, 미리 작성해줘도 된다.
- getValue대신 특정필드명이 있다면
.필드명()
으로 게터를 대신한다.
public int point() { return point; }
- getValue대신 특정필드명이 있다면
08 최대한 빨리 테스트 통과시키기
도메인vs같/다른도메인의 비교
는 서비스(메인로직 구동) 메서드 내부에서 일단 getter이용해서 하고 -> 도메인로직으로서 클래스를 추출하거나 해서 옮겨야한다.
참고)
같은 도메인끼리라도 내부 비교로직
이라면 현재 서비스 로직에서 일단 작성해주고 추출해줘야한다.
현재 getter를 쓰는 것에 대한 정당성 확보 ->
같은 도메인끼리 or 도메인1개
에 대해 단순 상수비교/ 메서드없이 단순집계
등는 개별 일괄처리 or 집계
라면 묶어서 -> 메세지 보내서 물어본다` -> 현재의 상황
but
[객체] 내부의 [포장변수 속 변수]
를 가져올 때도 getter
대신 .최종 나올 변수명()
으로 메서드 지어 들어가기
참고) - 예시) Player 속 Cards 속 cards에 접근하고 싶을 때
-
Player.getCards()
가 아니라Player.cards()
메세지를 보낸 뒤Cards 내부에서 return this.cards
로 보내주는 식으로 자기 변수처럼 바로 꺼내보도록 하자.get중간포장변수()가 아닌 .최종변수xxxx()
-
-
blackjack을 응답해주는 코드를 최대한 빠르게 작성해보자.
다시 응답을 Object형
으로 바꿔주고 -> 다형성을 생각
한다 -> 일단 테스트통과부터 확인한다.
if분기에 따라 서로 다른형의 객체를 응답해야할 떄 -> -
테스트가 실패한다. -> ace는 기본 1 point로 시작하고, 11로 계산처리를 안해줘서
public static Object start(final Card first, final Card second) { if (first.point() + second.point() == 21) { return new Blackjack(); } return new Hit(); }
ACE에 대한 처리
-
if문에 들어가있던
도메인끼리 연산
을 밖으로 빼서 처리후 넣어줘야한다.-
== 21
정답과의 비교는 빼고 처리해야한다.
public static Object start(final Card first, final Card second) { final int sum = first.point() + second.point(); if (sum == 21) { return new Blackjack(); } return new Hit(); }
-
개별 일괄처리가 필요할 땐 list
에 넣고 -> stream으로 일괄처리 -> 도메인끼리가 아니라 stream 도메인 개별 처리 -> 꺼내지말고 메세지보내서 처리
한다
참고) 2개이상의 같은 형이 -
받은 first와 second가 같은형의 같은 도메인이 때문에 ->
list(List.of())에 넣어서 일괄처리
한다. -
도메인 1개에 대한 것이므로 (같/다른 도메인
끼리의 연산
이 아닌 순간 메세지보내서 처리) 메세지를 보내서 ace있는지부터 확인한다.- 이넘에게 연산이라서
.is메서드()
로 물어봐야할 것 같지만 일단 넘어가보자.
여러개
에 대해isAce가 anyMatch
로 가지고 있다면,hasAce
가 된다.
public static Object start(final Card first, final Card second) { final boolean hasAce = List.of(first, second) .stream() .anyMatch(it -> it.isAce()); final int sum = first.point() + second.point(); if (sum == 21) { return new Blackjack(); } return new Hit(); }
-
intellij가
list에 담아서 stream
하지말고Stream.of()
로 한번에 묶어라고 한다.final boolean hasAce = Stream.of(first, second) .anyMatch(Card::isAce);
- 이넘에게 연산이라서
-
ace는 비록 1이지만, 합계 계산시
ace객체의 point를 11로 변경하지말
고21의 블랙잭 기준상수를 11로 낮추자
?public static Object start(final Card first, final Card second) { final int sum = first.point() + second.point(); final boolean hasAce = Stream.of(first, second) .anyMatch(Card::isAce); if (hasAce && sum == 11) { return new Blackjack(); } return new Hit(); }
- 테스트는 통과하게 된다.
다양한class의 객체들 응답시 Object로 응답? 메서드가 많은 일을한다?
- Object응답
-
메서드가 너무 많은 일을 한다.
- 같은형이라도,
도메인vs도메인 비교로직
이라면일단 getter 서비스로직에 작성
하되 ->추후 도메인로직으로 넘긴다
. - 하지만… 지금과 같은
일괄처리/내부변수단순집계-> 묶어서 메세지보내서 일괄처리 한다.
- 같은형이라도,
도메인1개or같은도메인끼리 상수or단순연산/집계
-> 묶어서 메세지보내 일괄처
리 or 내부변수 단순집계
로 일괄처리한다.
09 메서드가 길어져서 너무 많은 일 -> -
첫번째, 두번째 카드의 합계
는 같은 도메인이면서 단순집계다 -> 묵어서 일괄처리가 가능하다.
List.of()
나 Stream.of()
로 묶어서 일괄처리 or 단순집계가 가능하다.
같은 도메인이라면 -
같은 도메인이라면
List.of()
나Stream.of()
로 묶어서 일괄처리 or 단순집계가 가능하다.public static Object start(final Card first, final Card second) { final int sum = Stream.of(first, second) .mapToInt(it -> it.point()) .sum(); //final int sum = first.point() + second.point(); final boolean hasAce = Stream.of(first, second) .anyMatch(Card::isAce); if (hasAce && sum == 11) { return new Blackjack(); } return new Hit(); }
일괄처리/내부변수단순집계
가 2번이상 필요하면 -> List.of( , ,)
묶어놓고 개별 list.stream()
때린다.
public static Object start(final Card first,
final Card second) {
//1. 같은형의 [일괄처리/단순집계]가 2번이상 필요할 땐 -> 무조건 List.of()로 일단 묶어 변수로 뺀다.
final List<Card> cards = List.of(first, second);
// final int sum = Stream.of(first, second)
// 일괄처리1: 내부변수 단순집계
final int sum = cards.stream()
.mapToInt(it -> it.point())
.sum();
// final boolean hasAce = Stream.of(first, second)
// 일괄처리2: 일괄처리로 메세지보내기
final boolean hasAce = cards.stream()
.anyMatch(Card::isAce);
if (hasAce && sum == 11) {
return new Blackjack();
}
return new Hit();
}
서비스로직내 도메인로직의 class로 추출
같은형 객체 2개이상의 일괄처리 로직
-> 일급컬렉션으로 추출
10 서비스 로직 내 객체일괄처리를 위해 뽑아낸 List.of() -> 객체List -> 일괄처리가 메서드로 가는 일급컬렉션 추출과정이 된다.
public static Object start(final Card first,
final Card second) {
// 1) 도메인vs도메인 비교로직 후 새 결과값(생성자) 포장(class)후
// -> 그것의 추가 로직(메서드)이 아니더라도
// 2) 같은도메인 객체 2개이상의 묶어서 일괄처리(메세지보내 일괄처리 or 내부변수 변환후 단순집계)도
// -> 일괄처리하려면 List<객체>를 뽑아내야하고 (상태값, 인스턴스변수)
// -> 일괄처리로직이 도메인로직(메서드)가 되어서
// -> 포장하는 일급컬렉션을 만들어낸다.
final List<Card> cards = List.of(first, second);
final int sum = cards.stream()
.mapToInt(it -> it.point())
.sum();
final boolean hasAce = cards.stream()
.anyMatch(Card::isAce);
if (hasAce && sum == 11) {
return new Blackjack();
}
return new Hit();
}
- sum하는 부분/ hasAce로 뽑아내는 부분 -> 일괄처리로 간주했지만
어떠한 역할
이며도메인 class
를 만들어 위임해야한다.
서비스 메서드 내에서 발견된 역할과 객체List
-> 메서드 로직 내에서 일급컬렉션 추출
해보기
-
서비스 메서드 내
지만,기존 List생성 코드라인을 복붙
후 재료로서 사용해서 빨간줄 컬렉을 만든다.- 완성되면
객체list는 삭제
해준다.
- 완성되면
포장전 복붙 or 복사해놓은 상태로
최종메서드만 남기고 -> 내부처리코드를 잘라내 들고들어가
-> list자리에 -> 내부포장되어있던 value
만 바꿔넣어준다.
포장전 내부까는 코드였다면, -
객체list삭제로 인한 list.stream()에서의 list부분에 빨간줄처리해준다.
-
비교메서드
가 빨간줄이었다면,그대로 일급내부에서 생성
해도 됬찌만 -
getter/stream 등이
내부를 까는 메서드
가 빨간줄이라면복사후 지우면서 -> 최종메서드 남겨 메세지 던지고 -> 내부처리 코드를 들어가 내부처리한다.
-
기존
-
일급적용: 복사해놓고, 위에 올려놓고, 내부까는코드는 지우고, 최종만 남겨 메세지보낸다.
-
일급적용: 최종 메세지 메서드를 만들면서, 내부처리코드를 잘라내서 들고들어가자.
-
public final class Cards { private final List<Card> value; public Cards(final List<Card> value) { this.value = value; } public int sum() { return value.stream() .mapToInt(it -> it.point()) .sum(); } }
-
-
마찬가지로 hasAce로직도
-
복사 -> 내부처리코드 삭제 -> 최종만 남기고 -> 내부로 잘라내서 복사 ->
list자리에 일급컬렉내부value(list)로 바꿔주기
-
기존
-
처리
- 최종메서드명이 따로 없다면 뽑아놓은 변수명(
hasAce
)나최종 체인된 메서드(anyMatch + isAce)
로유추한 메서드로 메세지보
낸다.
-
내부로 들고들어갈 코드복사된 상태로-> 최종메서드로 이름바꿔주기
-
코드 복붙후
list 자리에 포장변수value
+ return 등 처리해주기
- 최종메서드명이 따로 없다면 뽑아놓은 변수명(
-
1번만 쓰인다면 지역변수 제거하고 inline화
public static Object start(final Card first,
final Card second) {
final Cards cards = new Cards(List.of(first, second));
if (cards.hasAce() && cards.sum() == 11) {
return new Blackjack();
}
return new Hit();
}
하나의 도메인
에 대한 여러 boolean문장이면, 메세지를 넘길 수 있다. (메서드추출이랑 다름)
참고) 너무빠른 리팩토링이지만, if문이 -
두 boolean문장이 cards에 관한 것이므로 -> cards에 메세지를 던질 수 있다.
-
메서드추출을 한다면? ->
한 도메인에 대한 것임을 고려하지 못한 체
해당 로직class내에서 추출한다. -> 메세지를 던지지 못한다.
내부처리로직 코드복사후 -> 최종메서드로 던지기
-> 밖에서 list가 아니라 일급그대로를 썼던 코드는? 내부에서 바로 메서드()
형태로 내부메서드를 이용하면 된다.
참고-연결) 메세지를 던지려면, -
여기서 최종메서드로 메세지 던지는 이름은
is
+Blackjack()
으로 한다.-
복사후, 바꿔주고 -> 내부로 들고들어가 -> 변수명만 처리해주면 되는데,
포장된 일급을 쓰던 코드
를 메세지보냈다. -> 일급변수명 빼고 내부에 존재하는 메서드를메서드()
로 호출만 해주면 ->내부의 this.value를 이용한 메서드
를 알아서 호출해서 쓴다. -
move staetment up/down
변수없이 호출되는 내부메서드
들보다 위쪽으로 올려준다.
-
추상화
or상위Type 생성으로 카테고라이징
메서드의 응답을 Object -> 응답되는 객체들을 뽑아내서
인터페이스
로 뽑아낸다.
11 서로 변하는 Blackjack 상태, Hit상태들을 상위타입으로 뽑아내기 -> 일단 상위카테고리는 -
State
인터페이스를 만든다.public interface State { }
-
다형성으로서 응답값을 상카인 interface로 응답해준다
public static State start(final Card first, final Card second) { final Cards cards = new Cards(List.of(first, second)); if (cards.isBlackjack()) { return new Blackjack(); } return new Hit(); }
상카로 카테고라이징하려고 인터페이스
를 만들었다면 -> 하위카테고리 구현체들이 impl 전
에 응답값/ 변수/파라미터 등의 길목 선언부 먼저 인터페이스로 바꿔주자 -> 거기로 향하는 예비 구현체들이 빨간줄 -> 쉽게 + 자동 impl
된다
참고) -
길목(변수,파라미터,응답값의 선언부)
응답값을 인터페이스
로 먼저 바꿔줬더니, 응답될 예비구현체들이 구현되려고 빨간줄이 뜬다.
-
묶여서 카테고라이징
되는 놈들은 인터페이스를 모두 구현한다public final class Hit implements State { } public final class Blackjack implements State { }
밖에서 받아주는 변수
도 Object에서 -> 다형성-상카-인터페이스
로 + 변수명도 인터페이스명
으로 바꿔주자
메서드의 응답값을 바꿔줬다면 -> 까먹지 말고 - 테스트코드라면 단순하게 현재파일에서
Ctrl+H
로 찾아서replace ALL
해주면 된다.- 찾은 갯수만 잘 확인해서 바꿔주자.
-
변수명도 바꿔주자.
public class GameTest { @Test void hit() { // given & when : hit final State state = Game.start(Card.of(Suit.SPADES, Denomination.TWO), Card.of(Suit.SPADES, Denomination.JACK)); // then assertThat(state).isInstanceOf(Hit.class); } @Test void blackjack() { // given & when : blackjack final State state = Game.start(Card.of(Suit.SPADES, Denomination.ACE), Card.of(Suit.SPADES, Denomination.JACK)); // then assertThat(state).isInstanceOf(Blackjack.class); }
중요1) 왜? 추클이 아니라 인터페이스?로 상카뽑아 카테고라이징)
-
어떤 메서드 or 변수들이 추상화
or공통메서드
로 뽑힐지 모르며.카테고라이징
이 목적이다-
상카뽑는 목적이면
인터페이스
부터 뽑는다.추클
로 바뀔 수도 있다.
-
상카뽑는 목적이면
중요2) 제한된 수의 상태값들인데, 왜 Enum이 아니라 클래스 객체로 상태 응답?
-
hit과 blackjack사이에
서로 연결되어 <메서드호출로 인해> 서로 바뀔 수 있는 묶인 상태들
이다. -> **서로 바뀌더라도 인페구현의 다형성으로 들어가 [상태를 바꿔주는 같은 이름의 메서드]를 계속 호출할 수 있는 상태값
으로상태 패턴 객체
를 선택할 수 밖에 없다. ** -
추가로 enum도 가능한
형태 뿐만 아니라 다음 상태로 바뀌기 위한 판단
을 위해내부에 인변=상태로 가지고 있어야한다
.- 이넘은 상태값을 가지면서 공유할 수 없다.
- TDD의 D는 디자인이 아니다 -> 미리 설계되어있고 development만 한다.
참고) 상속/추상화/카테고리 작업후엔 클래스 다이어그램 보기
-
프로덕션을 봐야한다. -> 프로덕션의 패키지폴더를 클릭한 상태로
단축키 ctrl+alt+shift + U
+edge create mode
는 기본적으로 켜두자
기본 2장받고 가능한 상태:Hit or Blackjack
-> 다음엔?? -> 둘 중에 더 만만한 것에서부터
상태 변화를 이어나간다.
다음 상태로 이어가기 -> 12 hit 과 blackjack 두 상태 중 blackjack은 다음이 게임종료라 구현이 더 쉽우니 blackjack에서 이어나간다.
특정상태에서 이어나가는 테스트메소드의 작성
-
직전까지 작성된
blackjack이 된 상태
응답 테스트코드를그대로 복붙한다
-> 테메이름에현재상태 + 이어질 작업
을 더해준다.@Test void blackjackDraw() { // given : blackjack -> 더이상 게임을 할 수 없다. (hit에서 이어지는 것보다 쉽다) final State state = Game.start(Card.of(Suit.SPADES, Denomination.ACE), Card.of(Suit.SPADES, Denomination.JACK)); }
예외발생이 정답인 메서드호출
-> 메서드 호출시 끝남 = 메서드호출시 예외 -> 생성자처럼 assertThrows() 문에서 해당예외 명시하며 + 호출
하며 테스트
-
**blackjack에서 카드받는다고 하면 ->
호출시 예외발생으로 작업이 끝나야
하므로 **생성자처럼예외발생이 정답인 호출
이 된다.@Test void blackjackDraw() { // given : blackjack -> 더이상 게임을 할 수 없다. (hit에서 이어지는 것보다 쉽다) final State state = Game.start(Card.of(Suit.SPADES, Denomination.ACE), Card.of(Suit.SPADES, Denomination.JACK)); // 1. blackjack은 끝난 상태로 -> [상호 상태 변화 전략메서드()] 호출시 예외가 발생하도록 짜야한다. // -> blackjack상태에서 .상호상태변화 전략메서드()호출시 -> 문제가 생겨야한다. state.draw(); }
-
생각해보니, 상태변화에는
카드1장 받기의 재료(인자)
가 필요하다.@Test void blackjackDraw() { // given : blackjack -> 더이상 게임을 할 수 없다. (hit에서 이어지는 것보다 쉽다) final State state = Game.start(Card.of(Suit.SPADES, Denomination.ACE), Card.of(Suit.SPADES, Denomination.JACK)); // 1. blackjack상태에서 .상호상태변화 전략메서드()호출시 -> 문제가 생겨야한다. // 2. 상태변화에는 card1장이 필요하다. state.draw(Card.of(Suit.SPADES, Denomination.TEN));
-
예외발생이 정답인 호출
이라면,assert문 내부에서 예외명시하며 호출
한다@Test void blackjackDraw() { // given : blackjack -> 더이상 게임을 할 수 없다. (hit에서 이어지는 것보다 쉽다) final State state = Game.start(Card.of(Suit.SPADES, Denomination.ACE), Card.of(Suit.SPADES, Denomination.JACK)); // 1. blackjack상태에서 .상호상태변화 전략메서드()호출시 -> 문제가 생겨야한다. // 2. 상태변화에는 card1장이 필요하다. //state.draw(Card.of(Suit.SPADES, Denomination.TEN)); // 3. 예외발생이 통과인 호출 -> assert문 내부에서 예외명시하며 호출 assertThrows(IllegalStateException.class, () -> state.draw(Card.of(Suit.SPADES, Denomination.TEN))); }
특정 구현체[끝 상태]에서 [상태변화의 전략메서드] 호출인지 / 아니면 단독메서드 호출인지 고민해보기
13 게임끝의 상태에서 더 진행하는 상태변화 메서드 호출시 에러발생인데, blackjack상태에서도 재료(인자, card1장)를 받는 공통 전략메서드를 부를 수 있나?
@Test
void blackjackDraw() {
// given : blackjack -> 더이상 게임을 할 수 없다. (hit에서 이어지는 것보다 쉽다)
final State state = Game.start(Card.of(Suit.SPADES, Denomination.ACE),
Card.of(Suit.SPADES, Denomination.JACK));
// 1. blackjack상태에서 .상호상태변화 전략메서드()호출시 -> 문제가 생겨야한다.
// 2. 상태변화에는 card1장이 필요하다.
// 4. 근데, blackjack은 끝난 상태인데 게임상 재료로서 들어올 수 있나??? (읹
//state.draw(Card.of(Suit.SPADES, Denomination.TEN));
// 3. 예외발생이 통과인 호출 -> assert문 내부에서 예외명시하며 호출
}
특정 구현체만 가능한 단독 메서드
라면
만약, 공통전략메서드가 아니라 -
만약, blackajck에서 가능한지? 가 아니라
-
하위카테고리 중 1개인 blackjack에서만 가능하다면
- 억지로 쓴다면
(특정카테고리)
다운 캐스팅후 써야한다.
- 억지로 쓴다면
현재 특정상태(구현체) 1개만 호출 가능한 메서드
라고 가정하고 해당 구현체에만 메서드가 생기도록 구현체변수 -> 다운캐스팅 -> 메서드호출() 빨간줄생성
하여 코드를 짜보자.
중요3) 상카(추상체)가 잡힌이후 메서드의 추가는 (모든 카테고리 구현체 다 가능할 것 같은데, 혹시 모르니)
hit와 blackjack
모든 카테고리가 호출가능할 것 같지만, 보수적으로 blackjack에서만 호출가능한 단독메서드라고 가정
해놓고 짜보자.
14 draw(Card card)가
변수 먼저 특정 구현체Type으로 받아주고
-> (= 우항 빨간줄 다운캐스팅)
해서 -> 단독메서드 호출()
하여 ->단독 빨간줄 생성
해줘야한다.
특정 구현체만 호출가능한 메서드를 호출하고 싶다면, 다형성으로 받던 곳에서 -
상카(추상체)가 있는 상황의
구현체(blackjack)에서, **특정 구현체만 호출가능한 메서드를 호출한다? **-
다형성(추상체)으로 받는 변수
먼저 바꿔주고 -
ide빨간줄로 (우항에서 다운캐스팅)`하여 구현체로 받아서 단독메서드를 호출해야한다.
@Test void blackjackDraw() { //1. 구현체 blackjack만의 단독메서드라고 가정 -> 다운캐스팅해서 받아야한다. // final State state = Game.start(Card.of(Suit.SPADES, Denomination.ACE), // Card.of(Suit.SPADES, Denomination.JACK)); final Blackjack state = (Blackjack) Game.start(Card.of(Suit.SPADES, Denomination.ACE), Card.of(Suit.SPADES, Denomination.JACK)); assertThrows(IllegalStateException.class, () -> state.draw(Card.of(Suit.SPADES, Denomination.TEN))); }
-
구현체로 받은 상태에서 빨간줄 create method -> 구현체에서 정의 or 추상체에서 정의 선택할 수 있다.
-
다운 캐스팅 이후 구현체로 받은 상태에서 메서드 호출 -> 구현체에만 메서드 생성해보자.
구현체로 받은 상태에서 빨간줄 create method
하면, 구현체에만 정의할 수 있게 선택할 수 있다.
-
draw() 호출시 에러나야 통과
하지만, 일단은 응답값으로 추상체를 넣고 정의해주자.(결국엔 추상체Type응답 -> 다형성으로 응답하여 -> 상호 바뀔 수 있는 카테고리내 상태 아무거나로 가능하게 바뀔 것임.)public final class Blackjack implements State { public State draw(final Card card) { throw new IllegalStateException(); } }
다운캐스팅 or 구현체변수
상태에서는 단독메서드 생성
이 가능하다.
my) 인터페이스로 추상화했다면, -
다운캐스팅 + 구현체 변수
가 거슬리지만, 예외발생으로 통과가 잘되는 것 같다.@Test void blackjackDraw() { // final State state = Game.start(Card.of(Suit.SPADES, Denomination.ACE), // Card.of(Suit.SPADES, Denomination.JACK)); //1. 구현체 blackjack만의 단독메서드라고 가정 -> 다운캐스팅해서 받아야한다. final Blackjack state = (Blackjack) Game.start(Card.of(Suit.SPADES, Denomination.ACE), Card.of(Suit.SPADES, Denomination.JACK)); //2. 다운캐스팅해서 blackjack에만 구현되도록 했지만. assertThrows(IllegalStateException.class, () -> state.draw(Card.of(Suit.SPADES, Denomination.TEN))); }
고민 - state가 card를 .draw()로 받아도 되느냐?
재료로 받아서 시작되거나 재료없이 빈재료로 내부초기화
되어서 시작 하며 -> 추가정보 등 내부 상태값이 업데이트 되고, 그 변화된 상태값을 재료
로해서 업데이트된 새 객체
를 반환해주려면 상태값으로 가져야한다.
my) 상태값(인변)을 가진다-> - 어떤 값을 받았다 -> 메서드호출로 업데이트 했다. ->
인변 , 상태값으로 가지고 있어야 그것을 재료
로 해서 new 새객체(업데이트재료
)로 업데이트된 객체를 생성할 수 있게 된다.- 상태값은 객체의 이전값을 유지 + 업데이트해서 새객체 반환을 할 수 있는 것이다.
상태값
은 재료로 들어와 포장
되며 (가공된 뒤 적은 수로)내부에서 가지고 있어야, 변화를 반영한 새객체도 생성시에도 재료로서 생성시 쓰인다
라고 할 수 있다.
my) 요약: 클래스의
실시간 반영되는 상태값(인변)
가지며 + 전략메서드
를 통해 바로 바뀌거나, 필요한 추가정보를 현 정보들과 같이 판단하여
다른 구현체로 바뀐다
중요4 my) 추상체로서 여러 구현체 state를 가지는 state를 사용하는 순간부터 state 변화를 판단하는 정보들은 state가 내부에서 - player는
name
과cards
를 가질 수 있다.- cards에 담긴 정보를 메세지를 보내 물어봐서-> 현재
state
를 판단한다.
- cards에 담긴 정보를 메세지를 보내 물어봐서-> 현재
- player는
name
과 현재 상태이자 메서드호출시 다른 것으로 바뀔 수 있는state
를 추상체를 가질 수 있다.-
state를 사용하는 순간부터 state판단에 필요한 정보들(
cards
등)들을 state 내부에서 관리한다.- state는 메소드를 호출을 통해 실시간으로 현재 상태에 맞는 구현체를 응답하여 상태를 바꾼다. 그 상태변화의 트리거가 추상체의 전략메서드
.draw()
다. - 상태를 바꾸는 트리거 메서드는 전략메서드로서 공통이며
상태 변화를 유발하는 추가정보(card 1장)
을 인자로 받을 수 있다.
- state는 메소드를 호출을 통해 실시간으로 현재 상태에 맞는 구현체를 응답하여 상태를 바꾼다. 그 상태변화의 트리거가 추상체의 전략메서드
-
state를 사용하는 순간부터 state판단에 필요한 정보들(
15 blackjack상태는 끝났다면, hit상태에서 진행해보자.
진행되는 테스트는 테메를 복사후 -> 확인된 given을 유지한체 다음 상태를 유발한다
-
테메 복사후, 테스트_이름만
기존상태+트리거
로 바꾼다.- given만
hit
상태를 유지
@Test void hit2() { // given & when : hit final State state = Game.start(Card.of(Suit.SPADES, Denomination.TWO), Card.of(Suit.SPADES, Denomination.JACK)); }
- given만
특정상태에서 먼저, 상태변화 트리거인 메서드를 호출할 수 있는지(예외발생 안하는지)부터 고민한다.
상태변화 트리거 메서드를 호출
하더라도 에러안나고 호출가능
= 응답값 제대로 반환받음(isInstanceOf)
증명해야한다.
현재 hit상태에서의 목적: 카드1장받는
구현체 변수로 받아놓고
단독메서드로서 가정하고 개발을 시작한다.
16 hit상태에서 또 draw로 카드받을 수 있나? 받을 수 있다면 단독메서드로서 다운캐스팅후 -
인터페이스의 상카를 가졌더라도, 보수적으로
구현체 단독메서드
로서 일단 개발한다.- 구현체로 다운캐스팅
- 좌항을 구현체 변수로 받아주고 -> 구현체 단독메서드로서 개발
@Test void hit2() { // given & when : hit -> draw -> hit //1. 구현체 (Hit)로 다운캐스팅한다. final Hit state = (Hit) Game.start(Card.of(Suit.SPADES, Denomination.TWO), Card.of(Suit.SPADES, Denomination.JACK)); }
반드시 좌변 구현체 변수로 받아야 구현체-단독메서드
개발이 가능하다.
다운캐스팅 하더라도, 우항에 체이닝으로 개발하면, 다운캐스팅 안먹고 응답되는 추상체로 메서드 개발됨. -> -
구현체 변수로 변경이후 메서드 체이닝을 통해, 바로 이어서 단독메서드를 호출하여 단독메서드를 개발해보자.
- 아래는 체이닝으로 했더니.. 다운캐스팅해도, 좌변 변수 받아도 단독메서드 빨간줄 생성 안됨.
참고) 어차피 직전 테메를 복붙해온 given의 hit상태가 보장되어있으니, 편하게 다운캐스팅 + 구현체 변수로 받아놓고 -> 단독메서드 개발
-
일단 단독메서드 개발시
- 응답값은 추상체 State로 가야할 것이다.
-
첫 개발이니까
return null;
을 주고 실패하는 코드로 돌리자
public final class Hit implements State { public State draw(final Card card) { return null; } }
예외없이 다음에 올 구현체 상태
를 응답해야하므로 밖에서는 state변수를 재할당시켜 업데이트
해줘야한다. 하지만, 단독메서드 개발을 위해, 구현체 변수로 받아둔 state
를 returnType State의 응답으로 재할당이 불가능하다.
blackjack과 다르게,
공통메서드, 추메라면 -> 다양한 구현체 응답을 위해 State
응답이 맞으나 단독메서드 -> 단독구현체로 응답
하도록 임시대응 한다.
다음 상태 트리거 전략메서드는
모든 구현체(hit, blackjack)들이 <구현은 다르지만 모두 호출 가능>한 공통메서드 -> 인터페이스에선 추메
로 추상화하고, 직접 추상화이후 @Override 직접 붙이기
중요5) 마지막 구현체(hit)의 단독메서드 구현 와중에 -> 상카 아래
코드 중복제거
가 아니라 구현체 모두 호출가능
시 구현체전용 단독메서드를 올려 다운캐스팅 제거
+ 파라미터/응답/변수에서 추상체로 받아 일괄처리 가능
해진다.
중요6 my) 추메로 추상화해도 구상체별로 개별 구현해줘야하는데??? —> 인페의 추메로 추상화 핵심은
구현체 모두가 호출가능
할 수 있다면, 인페-추메(전메)로 추상화해서 -> 다운캐스팅 제거후 추상체.전략메서드호출()
로 호출이 가능해진다.. 그전에…
16
다이어그램+ creation mode + method
로 하자
참고) 구현체 단독메서드 -> 추메/전메로 올리기 전 확인은 - 현재 hit와 blakcjack의 구현체 모두 단독메서드(draw)들을
구현은 달라도 호출가능
이라면 올릴 준비를 한다.
받는 변수/응답값/파라미터 등에 구현체
가 있다면, 그것들도 추상체로 바꿔
서 올려줘야한다.
중요7) 구현체 단독메서드 -> 추메/전메로서 (선언부 시그니쳐만) 위로 올릴 때, -
구현체의 단독메서드 -> (선언부 시그니쳐만) 추상체의 전메/추메로 올릴 때 -> 복붙해서 가져간 뒤
구현체로 응답
이 보인다면추상체
로 바꿔줘야한다.-
hit의 draw() 단독메서드를 올린다고 가정하보자.
-
올리기 위해
추상부인 선어부만
복사 -
선언부 복붙후, 구현체 응답이 보이면 -> 추상체로 변경
-
- 추메 선언부에서
추상체
응답으로 바꿔두면,-
기존 구현체
구상체
응답으로 개별구현이 계속 허용된다.
-
기존 구현체
추메/전메로 선언부
를 올릴 때, 접근제한자 제거
해서 default
로 일단 시작하기
참고) 인페의
인페의 추메/전메
로 수동 추상화 올린 다음
엔하고 난 뒤, usage-impl 수
를 확인해서 개별 구현남아있어야하니 @오버라이딩만
직접 가서 해준다.
중요)
추상화 끝난 단독메서드
는 더이상 구현체만의 단독메드가 아니다 == 다운캐스팅 안해도 (오버라이딩된) 추상체변수로 받은 상태에서 추메/전메로서 호출가능
해진다. -> 다운캐스팅 제거하고 추상체변수로 받도록 수정하기
중요) -
기존 Hit 구현체에만 단독이라고 믿어서 개발했던 단독메서드가
- 추상화 -> 추메/전메가 되었고 + 오버라이딩 @도 달게되었다면
- 다운캐스팅을 제거한
추상체변수
로 받아 -> 바로 호출이 된다.
메서드 호출가능
해서 -> 모든 구현체 호출가능 -> 메서드 추상화 해준 이후 -> 응답으로서 또 여러 경우의수가 나온다. -> hit + ?? -> 복사해놓고 구체적으로 1case를 또 정해주고 빠른통과하도록 메서드짜기
17 hit 에서
변화전 given현재 상태
의 테메를 그만큼 복사해놓고, case를 정해놓고 진행해나가자
참고) 다음 가능한 상태가 여러개로 나올 경우, - hit -> draw(card) -> ???
hit
orstay
중에 만만한 것으로 가면 된다.- hit부터 한다고 가정한다.
-
given된 상태의 테메를 복사해서
- 예상응답값: hit
- 가정인자: 다음 응답도 hit가 되도록 +1(ACE)를 draw()한다.
@Test void hitToHit() { // given & when : hit -> draw -> 2,10,1 -> hit State state = Game.start(Card.of(Suit.SPADES, Denomination.TWO), Card.of(Suit.SPADES, Denomination.JACK)); state = state.draw(Card.of(Suit.SPADES, Denomination.ACE)); // draw호출이 가능해서 assertThat(state).isInstanceOf(Hit.class); }
-
이제 case에 맞게 메서드를 수정한다.
-
일단 new Hit()만 반환해주면 된다.
@Override public Hit draw(final Card card) { return new Hit(); }
-
-
hit담에 hit가 나오므로 통과한다.
18 hit에서 새로운 응답값(= 객체 상태)가 나오는 case에서는 테메이름 + case로 가정인자 조절 + expected로서 assert-isInstanceOf의 새객체.class등 미리 작성하고 하나씩 만들어나간다.
-
hit -> hit
뿐만 아니라 다른 경우의 수인hit -> Bust
를 위해 기존hit
가 확인된 테스를복사
해뒀었다. ->테메이름을 변경하자
- 테메이름을 변경하자 ->
기존 + 다음상태case
- 테메이름을 변경하자 ->
-
주어진 hit상태에서
가정인자들도 다음상태를 유도하는 것
으로 바꿔주자 -
예상값이 Bust의 상태객체가 나와야한다.
- 빨간줄로 만들어가자
class생성(fi or ab)후 해당 추상체를 impl -> 그에 따라 추메/전메를 impl
하여 카테고리에 추가해준다.
참고) 카테고라이징(추상체 인페보유)된 상태들에 -> 새 객체를 추가한다면? -> Bust 상태에서 추메/전메를 impl하면 다음 상태로는 뭘 줘야할까? 끝나야할까? 일단은 return null;로 비워두기
-
아직 버스트에서 draw가능한지 / 다음 상태는 뭘로 줄지가 안정해졌으므로
구현체 단독메서드
지만 일단State응답 -> return null;
로 비워둔다.
public final class Bust implements State {
@Override
public State draw(final Card card) {
return null;
}
}
hit 상태에서
개별구현되고 있는 트리거 메서드
에서 호출시 Bust가 응답
되도록 메서드 로직
짜주기
19 -
ctrl+F12
를 통해,추상체 변수.추메()
로 보이는 구현체 Hit의 메서드로 찾아간다.- 현재는 구현체hit에서 호출시 new Hit()만 반환하게 되어있다.
다른 구현체로 넘어가는 로직
짜기 -> 이전 정보를 받아와 by 재료를 생성자를 통해
현재 상태를 나타내줄 정보들을 상태값(인변)
로서 내부에 가지고 있어야만 상태패턴의 상태객체다.
중요) 개별구현중인 구현체 메서드에서 @Override
public Hit draw(final Card card) {
// 0. 현재는 무조건 hit -> draw -> hit를 응답하지만
return new Hit();
// 1. if 21이 넘으면 -> Bust를 응답해줘야한다.
}
현 상태 -> 다음 상태로 넘어가는데 필요한 정보들
을 상태값(인변)
으로 가지고 있어야하며 -> 트리거 메서드에 의해 실시간으로 바뀌는 상태값
으로서 State가 내부에서 직접 상태값
으로 이미 가지고 있어야만 한다.
20 필요에 의해 -
hit 상태에서
-
if 21이 넘으면
bust객체로 응답- 21이 넘는지는
내부에 필요정보인 cards
를 들고 있어야만 가능하다.
- 21이 넘는지는
@Override public Hit draw(final Card card) { // 0. 현재는 무조건 hit -> draw -> hit를 응답하지만 return new Hit(); // 1. if 21이 넘으면 -> Bust를 응답해줘야한다. // 2. draw()시 21이 넘는지 안넘는지 판단하기 위해서는 // -> 들어오는 card에 대해, 현재 cards의 정보가 필요하다. }
-
상태값(인변)
으로 이미 정보를 가진다
? -> 상태값 필드(인변) + 생성자에서 재료받아 초기화
형태가 있어야 이전 정보를 가져와 실시간 업데이트 가능한 정보
로 보유한 셈이 된다.
중요) 현재 -
일단 필요한 정보로서
상태값(인변)
을 private final로 가지자public final class Hit implements State { //3. card1장을 받기전에, [현재 상태를 상태값 cards]으로 가지고 있어야한다. // -> 상태값(인변)을 가진다? -> 생성자에서 재료받아 초기화한다. private final Cards cards;
public final class Hit implements State { //3. card1장을 받기전에, [현재 상태를 상태값 cards]으로 가지고 있어야한다. // -> 상태값(인변)을 가진다? -> 생성자에서 재료받아 초기화한다. private final Cards cards; public Hit(final Cards cards) { this.cards = cards; }
상태값(정보)를 추가
를 위해 필드+재료받는 생성자
를 추가했더니 기존의 재료없이 기본생성자
를 이용한 것들이 에러가 난다.
21)
테스트에서 미리 많이 쓴 경우
-> 재료받는 생성자가 생기는 순간 재료받는 생성자가 주 생성자
-> 위에 this를 활용해서 재료 안받고 -> 내부 빈 재료로 정보 초기화
하는 부새성자를 추가해주자.
참고) new()로 생성자정의없이 기본생성자로 -
재료없는 생성자 생성방법은 -> genearte 후 consructor ->
Select None
으로 생성하면 된다.public Hit() { }
-
재료받는
생성자가 주생성자
이며, 재료 생성말고빈 재료로 정보 초기화
해주자.-
그 전에.. 사용처를 보니
new Hit()
의재료없는 기본생성자
로 사용한 경우가1usage로 현재 여기 Hit class내부 밖
인 상황이다. -> 기존 테스트 유지를 위한 생성자 추가 생성을 할 필요가 없는 상황임 -
F12눌러서 따라가도 현재 상황
-
그렇다면 굳이, 코드 유지를 위한
재료안받고 빈재료로 정보초기화
해주는 기본생성자가 필요하지 않다.
-
추가정보(by트리거 메서드 인자)
를 받아 현재 정보(인변으로 보유)
를 업데이트해서 판단한다
중요)
변화된 내부정보로 일급을 만들어 반환
하여 불변 && 밖에서 반환된 것을 이용
일급컬렉션 정보(상태값)의 add/remove()는 내부 정보를 변화시키지말고
추가정보(메서드인자)를 받아 현재 정보(인변으로 보유)를 업데이트
하자
22) 이전 카드정보들을 상태값으로 보유하고 있는 상황에서 -> 다음 상태로 가기위에 카드1장의
불변 일급
으로 만들어서 -> 변경된 list(정보)로 새 일급을 새로 만들어 반환
해주면 -> getter로 상태값을 열람할 필요가 없어진다.
일급 내부변수를 변경시키려한다면(list.add/remove) -> -
일급.add( )
후 상태값 확인시 ->불변이면서 add가 새 일급을 응답
하도록불변 일급컬렉션
만들어주기- 비슷한 사례:
-
Count VO
의+=
로 내부 상태값 변화시키고 그게 필요 -
일급컬렉션
의add
로 내부 상태값 변화시키고 그게 필요
-
- 비슷한 사례:
-
기존 상태값은 변경하기 전에, 복사를 먼저 한다.
-
나쁜예:
기존 상태값에 .add()로 자체변경을 먼저 가하면 안된다.
-
기존 상태값은 건들지말고, 변경을 가할
복사본을 먼저 만든다
-
-
복사된 상태값에 변경을 가한다.
public void add(final Card card) { //1. 기존 상태값을 변경없이 먼저 복사한다. (기존 상태값을 .add()로 바로 변경하지 않는다) final List<Card> newValue = new ArrayList<>(value); //2. 복사된 상태값에 증감을 가한다. newValue.add(card); }
-
증감이 반영된 복사된 새 상태값을 외부이므로 상태값이 아닌
포장된 일급
객체로 응답해줘야한다.public Cards add(final Card card) { final List<Card> newValue = new ArrayList<>(value); newValue.add(card); //3. 증감이 반영된 복사 상태값을 -> **외부에서 일급**으로 쓰이니 // -> [새 일급으로 포장해서 응답]** return new Cards(newValue); }
참고) 외부에서 내부(증감)변화의 메세지를 보낸 뒤 -> 그 내부상태를 가져다 쓰고 싶다면 -> 불변 포장객체로 응답해서 밖에서 사용가능하게 하라
외부에서 .add 해줘도 내부만 변화되는게 아니라
외부에서는 증감이 반영된 내부정보를 포장한 일급
을 받아쓴다 -> 메세지 보내서 업데이트된 내부 정보에 대해 물어보기
만 하면 된다.
23) 불변 일급컬렉션으로 증감시 새객체를 반환해주면 -> -
일급.add( 추가정보 )
로 내부만 변화될 것 같지만불변 일급
은내부에서 변화가반영된 새 일급
을 응답해주니 그 정보를 밖에서 이용하면 된다.
-
이제 변화가 반영된 정보를 포장한 일급 컬렉션으로 메세지를 보내서 현재 상태를 물어보면 된다.
@Override public Hit draw(final Card card) { final Cards currentCards = cards.add(card); // 21이면 블랙잭 // 21넘으면 Bust // 21이하면 return new Hit(); }
-
변화된 정보를 담은 일급
에게 21넘으면 Bust인지 물어보자.- 물어볼 때, 내부에서의
this.value
는변화가 반영된 정보
상태임을 인지하자.
-
this.value
나value
등을 사용하지 않는 같은 class에서 정의되었으며, 내부에서는 잘 사용되고 있는이미 정의해놓은 메서드()
를 호출해서물음에 답(합)
을 구할 수 있다.
public boolean isBust() { return sum() > 21; }
- 물어볼 때, 내부에서의
-
이제 또다른
Hit 구현체
에서 정의한draw()
는또다른 상태의 구현체도 return
메서드가 되었다.
추가정보를 반영하여 응답된 업데이트된 정보 by 불변 일급
를 담아서 -> 업데이트된 구현체
로 응답되어야한다.
24) 다른 구현체가 안되더라도, 같은 구현체로 응답(hit->hit) 되더라도
@Override
public State draw(final Card card) {
final Cards currentCards = cards.add(card);
// 21이면 블랙잭
// 21넘으면 Bust
if (currentCards.isBust()) {
return new Bust();
}
// 21이하면
return new Hit(currentCards);
}
-
다시 테스트로 돌아와서,
hit -> bust
로 잘 업데이트 되는 것을 확인할 수 있다.@Test void hitBust() { // given & when : hit -> draw -> 2,10,10 -> bust State state = Game.start(Card.of(Suit.SPADES, Denomination.TWO), Card.of(Suit.SPADES, Denomination.JACK)); state = state.draw(Card.of(Suit.SPADES, Denomination.TEN)); assertThat(state).isInstanceOf(Bust.class); }
중요) 끝상태에서 -> 트리거 호출시 예외발생으로 종료까지 마무리 해줘야한다.
- hit or blackjack
- blackjack -> 뽑을 시 예외발생해서 종료
- hit
- hit
- bust -> 뽑을 시 예외발생해서 종료 « 먼저 처리해주자
- blackjack?
25) bust에 온 상태의 테메를 복붙하여 bust까지를 세팅해놓고 -> 뽑으면 종료되는 case를 처리하자.
-
bust까지 온 상태의 테메를 복붙한다.
- 테메이름을 다음상태는 없으니
현상태 + 뽑았다
정도로 짓는다.
- 테메이름을 다음상태는 없으니
업데이트 트리거 메서드를 체이닝
으로 호출해서 한번만 할당해놓자
참고) 원하는 상태까지
구현체 상태 업데이트 트리거 메서드(draw)
는 체이닝으로 원하는 상태 만들 때
까지 갈 수 있다.
참고) 추상체 응답Type의 -
재할당으로 업데이트 하는 순간 -> assert문에서 람다캡처링에 걸려 메서드호출이 안되더라
-
bust도달까지를 given으로 주고, 추가로 카드를 하나 더 뽑기 전에 ->
추상체 변수에 담기전에, 체이닝으로 트리거 메서드 호출
하도록 해주자.-
참조변수 재할당시 -> assertThrows의 람다캡처링 때문에 호출이 불가능 한 문제 발생
-
기존
State state = Game.start(Card.of(Suit.SPADES, Denomination.TWO),//2 Card.of(Suit.SPADES, Denomination.JACK));//10 state = state.draw(Card.of(Suit.SPADES, Denomination.TEN)); //10 -> 여기까지 bust
-
체이닝으로 한번만 할당된 체로 bust 세팅완료
State state = Game.start(Card.of(Suit.SPADES, Denomination.TWO),//2 Card.of(Suit.SPADES, Denomination.JACK))//10 .draw(Card.of(Suit.SPADES, Denomination.TEN)); //10 -> 여기까지 bust by 메서드체이닝으로 업데이트
-
-
테스트시 예외발생안해서 통과안되어야한다. 아직 로직을 안짠 실패하는 코드 상태이므로
@Test void BustDraw() { // given : hit -> draw -> 2,10,10 -> bust State state = Game.start(Card.of(Suit.SPADES, Denomination.TWO),//2 Card.of(Suit.SPADES, Denomination.JACK))//10 .draw(Card.of(Suit.SPADES, Denomination.TEN)); //10 -> 여기까지 bust by 체이닝 // 예외발생해야 통과 메서드호출은, when으로 미리 호출하지 않고 -> assert문에서 바로 호출한다. // 2,10,10 bust -> 3 받을 시 예외발생해서 종료되어야한다. assertThrows(IllegalStateException.class, () -> state.draw(Card.of(Suit.SPADES, Denomination.THREE))); }
구현체 메서드의 로직개발
시 추상체변수.메서드()의 외형
이라도 현 구현체 상태에 잘맞게 ctrl+f12로 잘타고가서 개발
하자
중요) 추상체 변수로 받은 - 겉으로 보기엔 추상체 메서드를 개발하는 것 같다
- 하지만 현 구현체(Bust)를 잘타고 들어가서 개발해야한다.
-
Bust의 .draw()메서드로 가서 예외발생로직을 짜주자.
public final class B ust implements State { @Override public State draw(final Card card) { throw new IllegalStateException(); } }
-
테스트가 통과된다(호출시 예외발생으로 종료)
Null이거나 Thr
라면 물음표?
가 찍혀있게 된다. ( 업데이트 안됨)
참고) 다이어그램에서, return이 -
return을 억지로 new Bust()로 준 상태ㅁ
-
return null;
로 준 상태