📜 제목으로 보기

블랙잭

도메인 공부후 시작점 찾기

  1. 정제된 rawInput이 들어온다고 가정하고 했었다.
    1. 자동차 경주: new Car("이름")의 string 생성부터 -> 응답하는 메서드 테스트
    2. 로또: rawInput으로 1,2,3,4,5,6list
  2. 블랙잭 로직을 시작하려면 재료 시작전 포장된 카드 -> 카드덱이 먼저 필요하다
    • 카드덱이 카드를 2장씩 제공해야하는데, 포장된 카드만 있으면 카드덱에서 뽑아서 줬다고 가정하고 메인로직 시작하면 된다.
  3. 카드를 "문자열", 숫자의 정제된 rawInput으로 가정해도 되지만, 이것은 input으로 들어오는게 아니라 원래 생성되어 있어야하는 것이므로 가정하고 -> 나중에 포장하는게 아니라 미리 만들고 시작한다
    • 게다가 2개는 같이 붙어서 움직이는 것이므로 -> 미리 클래스로 포장한다
      • 로또당첨번호 lotto + bonusNumber
      • Player가 가지는 name + cards

캐싱

재료인 Card먼저 만들기

01 Card에는 2가지정보가 붙어서 움직인다.

image-20220318164548576

  1. test를 파서 필요한, 붙어다니는 2개의 정보로 new Card( , ) 생성부터 해본다.

    image-20220318164709923

  2. 2가지 정보는 제한된 상수이므로 Enum으로 생성하고 1가지 예시 enum객체를 활용해서 생성한다.

    • enum의 예시 -> enum클래스.대문자_상수로 사용

    image-20220318164915814

     @Test
     void create() {
         //new Card(new Suit(), new Denomination());
         //1. 2가지 정보는 제한된 수의 상수 -> enum으로 받는다고 가정 -> 예시로 생성한다.
         new Card(Suit.HEART, Denomination.ACE);
     }
    

    image-20220318164935719

  3. 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
     }
    

    image-20220318165459732

  4. 2가지 정보로 new 객체생성 템플릿 주생성자를 일단 생성해줘야한다.

    • 캐싱을 하더라도 먼저 기본생성자부터 생성해주자.

    image-20220318165639618 image-20220318165855439

    image-20220318165916660

     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번재 줄로 이동시켜도 된다. image-20220318170043125
02 class는 객체 100% 새로 생성 new 단순템플릿이 아니라 캐싱등의 능동적 객체 생성 관리자역할을 by 로직을 포함한 생성자 정펙매로 할 수 있다.
  1. 캐싱은 생성에 관한 것이며, 정적팩토리메서드를 통해 능동적으로 로직을 포함한 생성으로 객체생성을 관리할 수 있다.

    image-20220318170451390

     @Test
     void create() {
         final Card card = new Card(Suit.HEARTS, Denomination.ACE);
        
         //2. [new + 재료]가 아니라, of/from등의 [정펙매 + 재료]로 생성해야 로직을 포함한 생성이 가능하다.
         Card.of(Suit.HEARTS, Denomination.ACE);
        
     }
    

    image-20220318170526576

    • 메서드 생성후 파라미터 라인 + 명칭 수정해주고 나중가면 final도 달아준다?

    image-20220318170604749 image-20220318170619909

  2. 캐싱을 하기 위해서는 2가지만 해주면 된다.
    1. 미리 pr sf 로 클래스 변수 = 상수로서 cache map만들어주기 image-20220318170833496
      • psf는 public sf/ prsf은 private이다.
    2. 정펙매에서 생성된 CACHE_MAP + .computeIfAbsent(, )없을 때만 new 100%생성 기본생성자로 생성 후 반환
    3. 캐싱될 객체는 VO로서 값 같으면 같은객체 취급해주기 위해 eq/hC 오버라이딩
  3. 클래스 변수(상수)로 cache를 선언할 것이므로 -> 메소드 내에서 변수 추출로 형완성을 해가자

    • ctrl+alt+F로 메서드내부에서 바로 필드(인변/클변으로 뽑을 수 있다) image-20220318171257327
  4. 2개의 변수를 동시에 가지고 다니는 inner class를 선언한 뒤, 그 class의객체를 Key로 가지고 가도 된다.(사용한할 것임. 참고만) image-20220318171319942 image-20220318171330909 image-20220318171449611 image-20220318171501964

     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;
         }
     }
    
    • 변수 추출시 다형성도 항상 확인하자 image-20220318171553378
  5. 2개의 변수를 들고다니는 클래스Key를 만들어줘도, Card와 동일한 상태다(2개를 인변으로). 그냥 .name()을 활용한 string+string의 <String, 으로 unique한 1개의 key로 만들어준다. 캐싱의 valueType은 해당 객체로 준다.

    image-20220318172008820image-20220318172025741

  6. enum은 enum객체 변수명을 그대로 출력해주는 .name()메서드를 제공해준다.

    image-20220318172550026

     System.out.println(Suit.HEARTS.name() + Denomination.ACE.name());
    

    image-20220318172653763

  7. 메소드내부에서 변수추출했지만, scope이동할 땐, pr를 포함한 sf로 변경하여 접근제한자를 수정해주자..

    • 메서드내부에서는 pr이 안먹는다. public메서드라면..

    image-20220318172155926 image-20220318172207359

    • 갯수를 미리 알고 있으면 map의 () 생성자에 넣어준다.
     public class Card {
        
         private static final Map<String, Card> CACHE = new HashMap<>(52);
    
  8. 캐쉬에 들어갈 key를 1.name() + 2.name()의 스트링으로 작성해줬다. 만약, 그 key가 없을 경우, 해당 key값으로 자동으로 넣어줄 value를 지정해줘야한다.

    • 이 때, key값은 활용을 안하므로 value값을 만들어줄 Function 함수의 람다 시작인자로서 자리에 앞선 인자 key를 활용 안하므로 ignored로 명명해서 작성해주자.

    image-20220318173110744 image-20220318173128008 image-20220318173153952

     public static Card of(final Suit suit,
                           final Denomination denomination) {
         return CACHE.computeIfAbsent(suit.name() + denomination.name(), ignored -> new Card(suit, denomination));
     }
    
03 캐싱되는 객체는 VO처럼 eq/hC 오버라이딩 해줘야한다. + 정펙매 사용하면 기본생성자는 private로 바꿔서 잠궈줘야한다.
  • 같은 값(2개 정보)를 가지면 -> 같은 객체로 판단되어야 재사용시 같은 것으로 확인이 된다.

    • generate 띄운 후 eq로 빠르게 검색해서 사용하기

    image-20220318173559643 image-20220318173609372 image-20220318173653496

  • 리팩토리으로서 메서드 인자에, 메소드 연산을 하고 있다. -> 메서드추출

      //
      return CACHE.computeIfAbsent(suit.name() + denomination.name(), 
        
      //
      return CACHE.computeIfAbsent(toKey(suit, denomination),
    

    image-20220318174218060

  • 정펙메 사용순간부터 기본 생성자private으로 변경

    image-20220318174601860

    • 테스트 코드에 사용했던 new 기본생성자.of로 변경

    image-20220318174646703 image-20220318174701030

    • 테스트 메서드명도 변경
      • create -> of

    image-20220318174720381 image-20220318174727220

만든 카드 캐싱 테스트

참고) assertThat()의 import는 qualify static call.. 이후 add static import로 ??

image-20220318174844069

image-20220318174906024

image-20220318175006487

image-20220318174930443

image-20220318175022431

캐싱 테스트: 만든것 .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 시작점 어디서부터

핵심 로직은?

  1. 카드 2장을 받는 것?
  2. 2장 받은 상태에서 판단을 한다.
    • 21이면 블랙잭
      • if문이 등장했다. -> 다형성/enum 등…
    • 21보다 낮으면(21 아니면) sit
      • 그만 할 수 있다. stay
        • 추가적으로 받을 수 없다.
      • hit시, 21보다 아래면 sit상태로
        • 계속 받을 수 있다.
  3. 행위는 사용자input에 의해 진행되지만, 특정 메서드(카드받기)에 의해 서로 바뀌는 <상호 연결된 상태값>이라는 밀접한 관계를 가지고 있다.
    • 응답값이 상태이며,
    • (동일)메서드에 의해 실시간으로 바뀌는 상태다..
    • if문이 엄청 나올 것이다 -> 추상화로 if문을 제거한다
      • 객체생활 체조원칙 -> 체조는 어디에 좋은지 모르는데, 하다보면 좋아진다.

시작점은 어디서?

  • 재료인 카드 2장을 뽑은 상태에서부터 시작하여 상호 연결된 다른 상태로 바뀌는 메서드를 호출하면서 시작한다.

01 핵심로직(like Service)의 클래스이름이 안정해졌으면 GameTest클래스 -> Game.start(재료)메서드로 시작

image-20220318180804265

image-20220318180844082

image-20220318180918002

02 재료를 넣고 -> 예상 응답값으로 뭐가 나올지 먼저 고민한 뒤 여러개면 만만한 1 case 선택 -> 재료:예상 인자값도 맞춰서 정해준다.

테메당 1case이므로 여러 응답(상태)이 나온다고 예상되면 -> 그 중 1개를 예상응답값으로 선택하고 -> 예상응답값에 맞는 재료를 넣어줘야한다.
  1. 메인로직 클래스(game)의 static메서드()에 정제된 rawInput(재료)으로서 포장 카드 2장을 넣어야한다.

  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();
     }
    
  3. 예상응답값에 대한 예상인자를 맞춰서 작성해줘야한다.

    • hit를 먼저 예상응답값으로 정했다면
    • hit를 만드는 재료 카드 2장도 예상인자값으로 맞춰서 넣어줘야한다.

    image-20220318182533609

     @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));
     }
    

    image-20220318182633631

응답값이 상수/enum이 행위에 의해 다른 것으로 바뀔 수 있는 상태라면 -> class의 객체로 미리 작성한다.

03 테스트는 내부로직 완성전에 응답값 -> then assert문을 미리 작성 -> 클래스와 메서드는 껍데기만 일단 작성해놓기

참고) 앞으로 클래스가 상속예정없으면 다 final로 선언하는 버릇 들이기

image-20220318183627812

  1. 메인로직 메서드 보유 클래스(Game)을 만들어주고 **응답값을미리 ** 작성해야한다.

    • 클래스 생성만 해주고 넘어가야한다
    • 이 때 class가 상속예정이 없으면 final로 선언하는 버릇을 들이자.
     public final class Game {
        
     }
    

    image-20220318183452321 image-20220318183500342

응답값 아직 작성안된 class의 객체가 있을 예정이라면 Object로 응답해주고 (실패용)return null;하는 메서드 작성하기
  1. 빈 메서드 완성해주기

    image-20220318183703651

    image-20220318183919815

    • 응답값 객체가 있을 예정이면 Object로 일단 응답시키고 return null;로 작성하자
      • return null;로 초기 작성해줘야 실패를 해서 고친다.
    • 순서가 있는 파라미터는 first, second로 네이밍해놓자
    • 파라미터는 줄맞춤 해도 된다. image-20220318184018832
     public final class Game {
        
         public static Object start(final Card first,
                                    final Card second) {
             return null;
         }
     }
    

    image-20220318184116638

04 Object 응답객체로 받아서 null이 넘어와 실패할 assert문 작성과 동시에 expected에 기입할 응답객체.class로 응답클래스만들어주기

객체 응답은 .isInstanceOf( 클래스.class)로 확인하면서 class작성해주기

image-20220318184327974

  1. 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);
     }
    

    image-20220318184613356

     public final class Hit {
     }
        
    
  2. 클래스를 채우거나 완성하지 않고 테스트를 돌린다.

    • return null;의 미완성 상태라 실패한다.

    image-20220318184743965

05 첫번째 응답객체로 메서드 응답(응답Type+return) 바꿔주기

  1. 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();
         }
     }
    

    image-20220318185508352

  2. 테스트 돌려보면 통과된다. image-20220318185642510

06 2번째 예상응답인 blackjack case로 만들어 주고 로직 추가해주기 -> given예상인자값도 바꿔주자!

다형성 적용전까진, 바뀔 수 있는 응답객체의 변수Type을 Object로 유지해주기
  • 그래야 다형성 적용 전, case 추가해서 받아줘도 테스트들이 다 통과될 것이다.
  1. 테메 복사 -> 테메이름 -> 변수이름 -> 예상 응답값 -> 예상인자값given -> expeceed 모두 해당case로 바꿔주기

    image-20220318190028753

  2. 없으면 해당 클래스도 만들어준다. -> final + enter 활용

    • tab누르면 추가가 아니라 단어 덮어쓰기 되서 class가 사라지더라.
     public final class Blackjack {
     }
        
    
  3. 예상인자값도 바꿔주야한다.

    • ace + jack으로 바꿔줌

    image-20220318190417465

  4. 테스트는 로직 추가/수정이 없으니 통과안한다.

07 복잡한 case통과를 위한 로직 추가해주기

  • blackjack은 단순if문으로 통과할 순 없다.
  1. 카드의 합이 21이 될 때 return new Blackjack() 해주면 되는데, 지금은 점수를 가지고 올 수 없다.

    • Card -> Denomination -> 점수를 가지고 와야함.
     public static Hit start(final Card first,
                             final Card second) {
        
         return new Hit();
     }
    

    image-20220318201952571| image-20220318202000526 image-20220318202107693

필요에 의해 enum에 필드 추가해주기
  • 여러개 선택은 커서가 물리는 중간에서 시작해서 -> 같이 움직인다.

    • 1~2자리 숫자면, 2자리 숫자로 입력해서 지우는 식으로 가자.

    image-20220318202232174 image-20220318202246281 image-20220318202322234 image-20220318202343405 image-20220318202400364

참고) enum에 필드를 만들어줬다는 말은 밖에서 갖다 쓸거라는 말이다 -> getter가 될 준비를 한다.
  • 나중에 필요에 의해 작성할것이지만, 미리 작성해줘도 된다.

    • getValue대신 특정필드명이 있다면 .필드명()으로 게터를 대신한다.
      public int point() {
          return point;
      }
    

08 최대한 빨리 테스트 통과시키기

참고) 도메인vs같/다른도메인의 비교 는 서비스(메인로직 구동) 메서드 내부에서 일단 getter이용해서 하고 -> 도메인로직으로서 클래스를 추출하거나 해서 옮겨야한다.
현재 getter를 쓰는 것에 대한 정당성 확보 -> 같은 도메인끼리라도 내부 비교로직이라면 현재 서비스 로직에서 일단 작성해주고 추출해줘야한다.
but 같은 도메인끼리 or 도메인1개에 대해 단순 상수비교/ 메서드없이 단순집계 등는 개별 일괄처리 or 집계라면 묶어서 -> 메세지 보내서 물어본다` -> 현재의 상황
참고) [객체] 내부의 [포장변수 속 변수]를 가져올 때도 getter대신 .최종 나올 변수명()으로 메서드 지어 들어가기
  • 예시) Player 속 Cards 속 cards에 접근하고 싶을 때
    • Player.getCards()가 아니라 Player.cards() 메세지를 보낸 뒤 Cards 내부에서 return this.cards로 보내주는 식으로 자기 변수처럼 바로 꺼내보도록 하자. get중간포장변수()가 아닌 .최종변수xxxx()

image-20220318203311839

image-20220318203422914

image-20220318203430707

  1. blackjack을 응답해주는 코드를 최대한 빠르게 작성해보자.

    image-20220318203740957

    image-20220318203815022

if분기에 따라 서로 다른형의 객체를 응답해야할 떄 -> 다시 응답을 Object형으로 바꿔주고 -> 다형성을 생각한다 -> 일단 테스트통과부터 확인한다.

image-20220318203853166

image-20220318203902241

  • 테스트가 실패한다. -> 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();
      }
    

    image-20220318204252096

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();
      }
    

    image-20220318204436577 image-20220318204446330

참고) 2개이상의 같은 형이 개별 일괄처리가 필요할 땐 list에 넣고 -> stream으로 일괄처리 -> 도메인끼리가 아니라 stream 도메인 개별 처리 -> 꺼내지말고 메세지보내서 처리한다
  1. 받은 first와 second가 같은형의 같은 도메인이 때문에 -> list(List.of())에 넣어서 일괄처리한다. image-20220318204647953

  2. 도메인 1개에 대한 것이므로 (같/다른 도메인끼리의 연산이 아닌 순간 메세지보내서 처리) 메세지를 보내서 ace있는지부터 확인한다.

    image-20220318212054279image-20220318212026907

    image-20220318212310522

    • 이넘에게 연산이라서 .is메서드()로 물어봐야할 것 같지만 일단 넘어가보자.

    image-20220318212728957

    • 여러개에 대해 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();
     }
        
    

    image-20220318212830673

    • intellij가 list에 담아서 stream하지말고 Stream.of()로 한번에 묶어라고 한다.

        final boolean hasAce = Stream.of(first, second)
            .anyMatch(Card::isAce);
      

      image-20220318213047307

  3. ace는 비록 1이지만, 합계 계산시 ace객체의 point를 11로 변경하지말21의 블랙잭 기준상수를 11로 낮추자?

    image-20220318213518180

     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();
     }
        
    
    • 테스트는 통과하게 된다.

    image-20220318213542349

다양한class의 객체들 응답시 Object로 응답? 메서드가 많은 일을한다?

  1. Object응답
  2. 메서드가 너무 많은 일을 한다.
    1. 같은형이라도, 도메인vs도메인 비교로직이라면일단 getter 서비스로직에 작성하되 -> 추후 도메인로직으로 넘긴다.
    2. 하지만… 지금과 같은 일괄처리/내부변수단순집계-> 묶어서 메세지보내서 일괄처리 한다.

09 메서드가 길어져서 너무 많은 일 -> 도메인1개or같은도메인끼리 상수or단순연산/집계 -> 묶어서 메세지보내 일괄처리 or 내부변수 단순집계로 일괄처리한다.

  • 첫번째, 두번째 카드의 합계는 같은 도메인이면서 단순집계다 -> 묵어서 일괄처리가 가능하다.

    image-20220318214950281

같은 도메인이라면 List.of()Stream.of()로 묶어서 일괄처리 or 단순집계가 가능하다.
  1. 같은 도메인이라면 List.of()Stream.of()로 묶어서 일괄처리 or 단순집계가 가능하다.

    image-20220318215109703 image-20220318215443384

     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() 때린다.

image-20220318215718661

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로 추출

10 서비스 로직 내 같은형 객체 2개이상의 일괄처리 로직 -> 일급컬렉션으로 추출

객체일괄처리를 위해 뽑아낸 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 -> 메서드 로직 내에서 일급컬렉션 추출해보기
  1. 서비스 메서드 내지만, 기존 List생성 코드라인을 복붙후 재료로서 사용해서 빨간줄 컬렉을 만든다.

    image-20220318221457532 image-20220318221544994

    • 완성되면 객체list는 삭제해준다.
포장전 내부까는 코드였다면, 포장전 복붙 or 복사해놓은 상태로 최종메서드만 남기고 -> 내부처리코드를 잘라내 들고들어가 -> list자리에 -> 내부포장되어있던 value만 바꿔넣어준다.
  1. 객체list삭제로 인한 list.stream()에서의 list부분에 빨간줄처리해준다.

    • 비교메서드가 빨간줄이었다면, 그대로 일급내부에서 생성해도 됬찌만

    • getter/stream 등이 내부를 까는 메서드가 빨간줄이라면 복사후 지우면서 -> 최종메서드 남겨 메세지 던지고 -> 내부처리 코드를 들어가 내부처리한다.

      • 기존image-20220318221622576

      • 일급적용: 복사해놓고, 위에 올려놓고, 내부까는코드는 지우고, 최종만 남겨 메세지보낸다.

        image-20220318221959211

      • 일급적용: 최종 메세지 메서드를 만들면서, 내부처리코드를 잘라내서 들고들어가자. image-20220318222123398

        image-20220318222134814

        image-20220318222145315

        image-20220318222159850

        image-20220318222315996

     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();
         }
     }
    
  2. 마찬가지로 hasAce로직도

    • 복사 -> 내부처리코드 삭제 -> 최종만 남기고 -> 내부로 잘라내서 복사 -> list자리에 일급컬렉내부value(list)로 바꿔주기

    • 기존 image-20220318222532313

    • 처리

      • 최종메서드명이 따로 없다면 뽑아놓은 변수명(hasAce)나 최종 체인된 메서드(anyMatch + isAce)유추한 메서드로 메세지보낸다.

      image-20220318222700866

      • 내부로 들고들어갈 코드복사된 상태로-> 최종메서드로 이름바꿔주기

        image-20220318222825288 image-20220318222846589

      • 코드 복붙후 list 자리에 포장변수value + return 등 처리해주기 image-20220318222905820 image-20220318222929855 image-20220318222948715

1번만 쓰인다면 지역변수 제거하고 inline화

image-20220318223051734 image-20220318223105901

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();
}
참고) 너무빠른 리팩토링이지만, if문이 하나의 도메인에 대한 여러 boolean문장이면, 메세지를 넘길 수 있다. (메서드추출이랑 다름)
  • 두 boolean문장이 cards에 관한 것이므로 -> cards에 메세지를 던질 수 있다.

    image-20220318223320994

  • 메서드추출을 한다면? -> 한 도메인에 대한 것임을 고려하지 못한 체 해당 로직class내에서 추출한다. -> 메세지를 던지지 못한다.

    image-20220318223428131

참고-연결) 메세지를 던지려면, 내부처리로직 코드복사후 -> 최종메서드로 던지기 -> 밖에서 list가 아니라 일급그대로를 썼던 코드는? 내부에서 바로 메서드()형태로 내부메서드를 이용하면 된다.
  • 여기서 최종메서드로 메세지 던지는 이름은 is + Blackjack()으로 한다.

    image-20220318223558182

    • 복사후, 바꿔주고 -> 내부로 들고들어가 -> 변수명만 처리해주면 되는데, 포장된 일급을 쓰던 코드를 메세지보냈다. -> 일급변수명 빼고 내부에 존재하는 메서드를 메서드()로 호출만 해주면 -> 내부의 this.value를 이용한 메서드를 알아서 호출해서 쓴다. image-20220318223726992

      image-20220318224006121 image-20220318224014942

    • move staetment up/down 변수없이 호출되는 내부메서드들보다 위쪽으로 올려준다.

      image-20220318224127252

메서드의 응답을 Object -> 응답되는 객체들을 뽑아내서 추상화or상위Type 생성으로 카테고라이징

11 서로 변하는 Blackjack 상태, Hit상태들을 상위타입으로 뽑아내기 -> 일단 상위카테고리는 인터페이스로 뽑아낸다.

image-20220318224419960

  1. State인터페이스를 만든다.

    image-20220318224516912

     public interface State {
     }
        
    
  2. 다형성으로서 응답값을 상카인 interface로 응답해준다 image-20220318224916162 image-20220318224930931

     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된다
  • 길목(변수,파라미터,응답값의 선언부)응답값을 인터페이스로 먼저 바꿔줬더니, 응답될 예비구현체들이 구현되려고 빨간줄이 뜬다.image-20220318225141060

    image-20220318225328352

    image-20220318225446165

  1. 묶여서 카테고라이징 되는 놈들은 인터페이스를 모두 구현한다

     public final class Hit implements State {
     }
        
     public final class Blackjack implements State {
     }
    
메서드의 응답값을 바꿔줬다면 -> 까먹지 말고 밖에서 받아주는 변수Object에서 -> 다형성-상카-인터페이스로 + 변수명도 인터페이스명으로 바꿔주자

image-20220318225618247

  • 테스트코드라면 단순하게 현재파일에서 Ctrl+H로 찾아서 replace ALL해주면 된다.
    • 찾은 갯수만 잘 확인해서 바꿔주자. image-20220318230004779 image-20220318230152638
  • 변수명도 바꿔주자.

    image-20220318234206968 image-20220318234232296

      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는 기본적으로 켜두자 image-20220318231631297 image-20220318231641974 image-20220318231847153

    image-20220318231824388

다음 상태로 이어가기 -> 기본 2장받고 가능한 상태:Hit or Blackjack -> 다음엔?? -> 둘 중에 더 만만한 것에서부터상태 변화를 이어나간다.

12 hit 과 blackjack 두 상태 중 blackjack은 다음이 게임종료라 구현이 더 쉽우니 blackjack에서 이어나간다.

특정상태에서 이어나가는 테스트메소드의 작성
  1. 직전까지 작성된 blackjack이 된 상태응답 테스트코드를 그대로 복붙한다 -> 테메이름에 현재상태 + 이어질 작업을 더해준다. image-20220318232657059

     @Test
     void blackjackDraw() {
         // given : blackjack -> 더이상 게임을 할 수 없다. (hit에서 이어지는 것보다 쉽다)
         final State state = Game.start(Card.of(Suit.SPADES, Denomination.ACE),
                                            Card.of(Suit.SPADES, Denomination.JACK));
     }
    
예외발생이 정답인 메서드호출 -> 메서드 호출시 끝남 = 메서드호출시 예외 -> 생성자처럼 assertThrows() 문에서 해당예외 명시하며 + 호출하며 테스트
  1. **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();
    }
    
  2. 생각해보니, 상태변화에는 카드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));
    
  3. 예외발생이 정답인 호출이라면, 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장)를 받는 공통 전략메서드를 부를 수 있나?

image-20220318235424442

@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에서만 가능하다

    • 억지로 쓴다면 (특정카테고리)다운 캐스팅후 써야한다.

    image-20220318235657518

중요3) 상카(추상체)가 잡힌이후 메서드의 추가는 (모든 카테고리 구현체 다 가능할 것 같은데, 혹시 모르니) 현재 특정상태(구현체) 1개만 호출 가능한 메서드라고 가정하고 해당 구현체에만 메서드가 생기도록 구현체변수 -> 다운캐스팅 -> 메서드호출() 빨간줄생성하여 코드를 짜보자.

14 draw(Card card)가 hit와 blackjack 모든 카테고리가 호출가능할 것 같지만, 보수적으로 blackjack에서만 호출가능한 단독메서드라고 가정해놓고 짜보자.

특정 구현체만 호출가능한 메서드를 호출하고 싶다면, 다형성으로 받던 곳에서 변수 먼저 특정 구현체Type으로 받아주고 -> (= 우항 빨간줄 다운캐스팅)해서 -> 단독메서드 호출()하여 ->단독 빨간줄 생성해줘야한다.
  1. 상카(추상체)가 있는 상황의 구현체(blackjack)에서, **특정 구현체만 호출가능한 메서드를 호출한다? **

    1. 다형성(추상체)으로 받는 변수 먼저 바꿔주고

    2. ide빨간줄로 (우항에서 다운캐스팅)`하여 구현체로 받아서 단독메서드를 호출해야한다.

      image-20220319003742240

      image-20220319003752055

      image-20220319003758956

       @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 추상체에서 정의 선택할 수 있다.
  1. 다운 캐스팅 이후 구현체로 받은 상태에서 메서드 호출 -> 구현체에만 메서드 생성해보자. image-20220319004006637

    • 구현체로 받은 상태에서 빨간줄 create method하면, 구현체에만 정의할 수 있게 선택할 수 있다.

    image-20220319004058831

  2. draw() 호출시 에러나야 통과하지만, 일단은 응답값으로 추상체를 넣고 정의해주자.(결국엔 추상체Type응답 -> 다형성으로 응답하여 -> 상호 바뀔 수 있는 카테고리내 상태 아무거나로 가능하게 바뀔 것임.)

     public final class Blackjack implements State {
         public State draw(final Card card) {
             throw new IllegalStateException();
         }
     }
    
my) 인터페이스로 추상화했다면, 다운캐스팅 or 구현체변수 상태에서는 단독메서드 생성이 가능하다.
  1. 다운캐스팅 + 구현체 변수가 거슬리지만, 예외발생으로 통과가 잘되는 것 같다.

     @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)));
     }
    

    image-20220319004448804

고민 - state가 card를 .draw()로 받아도 되느냐?

my) 상태값(인변)을 가진다-> 재료로 받아서 시작되거나 재료없이 빈재료로 내부초기화되어서 시작 하며 -> 추가정보 등 내부 상태값이 업데이트 되고, 그 변화된 상태값을 재료로해서 업데이트된 새 객체를 반환해주려면 상태값으로 가져야한다.
  • 어떤 값을 받았다 -> 메서드호출로 업데이트 했다. -> 인변 , 상태값으로 가지고 있어야 그것을 재료로 해서 new 새객체(업데이트재료)로 업데이트된 객체를 생성할 수 있게 된다.
    • 상태값은 객체의 이전값을 유지 + 업데이트해서 새객체 반환을 할 수 있는 것이다.
my) 요약: 클래스의 상태값재료로 들어와 포장되며 (가공된 뒤 적은 수로)내부에서 가지고 있어야, 변화를 반영한 새객체도 생성시에도 재료로서 생성시 쓰인다라고 할 수 있다.

중요4 my) 추상체로서 여러 구현체 state를 가지는 state를 사용하는 순간부터 state 변화를 판단하는 정보들은 state가 내부에서 실시간 반영되는 상태값(인변)가지며 + 전략메서드를 통해 바로 바뀌거나, 필요한 추가정보를 현 정보들과 같이 판단하여 다른 구현체로 바뀐다

  • player는 namecards를 가질 수 있다.
    • cards에 담긴 정보를 메세지를 보내 물어봐서-> 현재 state를 판단한다.
  • player는 name과 현재 상태이자 메서드호출시 다른 것으로 바뀔 수 있는state를 추상체를 가질 수 있다.
    • state를 사용하는 순간부터 state판단에 필요한 정보들(cards 등)들을 state 내부에서 관리한다.
      • state는 메소드를 호출을 통해 실시간으로 현재 상태에 맞는 구현체를 응답하여 상태를 바꾼다. 그 상태변화의 트리거가 추상체의 전략메서드 .draw()다.
      • 상태를 바꾸는 트리거 메서드는 전략메서드로서 공통이며 상태 변화를 유발하는 추가정보(card 1장)을 인자로 받을 수 있다.

15 blackjack상태는 끝났다면, hit상태에서 진행해보자.

진행되는 테스트는 테메를 복사후 -> 확인된 given을 유지한체 다음 상태를 유발한다
  1. 테메 복사후, 테스트_이름만 기존상태+트리거 로 바꾼다.

    • given만 hit상태를 유지

    image-20220319011659132

     @Test
     void hit2() {
         // given & when : hit
         final State state = Game.start(Card.of(Suit.SPADES, Denomination.TWO),
                                        Card.of(Suit.SPADES, Denomination.JACK));
     }
    
특정상태에서 먼저, 상태변화 트리거인 메서드를 호출할 수 있는지(예외발생 안하는지)부터 고민한다.

현재 hit상태에서의 목적: 카드1장받는 상태변화 트리거 메서드를 호출하더라도 에러안나고 호출가능 = 응답값 제대로 반환받음(isInstanceOf)증명해야한다.

16 hit상태에서 또 draw로 카드받을 수 있나? 받을 수 있다면 단독메서드로서 다운캐스팅후 구현체 변수로 받아놓고 단독메서드로서 가정하고 개발을 시작한다.

  1. 인터페이스의 상카를 가졌더라도, 보수적으로 구현체 단독메서드로서 일단 개발한다.

    1. 구현체로 다운캐스팅
    2. 좌항을 구현체 변수로 받아주고 -> 구현체 단독메서드로서 개발
     @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));
     }
    

    image-20220319013558950

다운캐스팅 하더라도, 우항에 체이닝으로 개발하면, 다운캐스팅 안먹고 응답되는 추상체로 메서드 개발됨. -> 반드시 좌변 구현체 변수로 받아야 구현체-단독메서드 개발이 가능하다.
  1. 구현체 변수로 변경이후 메서드 체이닝을 통해, 바로 이어서 단독메서드를 호출하여 단독메서드를 개발해보자.

    • 아래는 체이닝으로 했더니.. 다운캐스팅해도, 좌변 변수 받아도 단독메서드 빨간줄 생성 안됨.

    image-20220319013208680

참고) 어차피 직전 테메를 복붙해온 given의 hit상태가 보장되어있으니, 편하게 다운캐스팅 + 구현체 변수로 받아놓고 -> 단독메서드 개발

image-20220319013737152

image-20220319013843569 image-20220319013857906

  1. 일단 단독메서드 개발시

    • 응답값은 추상체 State로 가야할 것이다.
    • 첫 개발이니까 return null;을 주고 실패하는 코드로 돌리자 image-20220319015029459
     public final class Hit implements State {
        
         public State draw(final Card card) {
             return null;
         }
     }
    
blackjack과 다르게, 예외없이 다음에 올 구현체 상태를 응답해야하므로 밖에서는 state변수를 재할당시켜 업데이트해줘야한다. 하지만, 단독메서드 개발을 위해, 구현체 변수로 받아둔 state를 returnType State의 응답으로 재할당이 불가능하다.

image-20220319015153224

image-20220319015210037

image-20220319015506270

다음 상태 트리거 전략메서드는 공통메서드, 추메라면 -> 다양한 구현체 응답을 위해 State 응답이 맞으나 단독메서드 -> 단독구현체로 응답하도록 임시대응 한다.

image-20220319015500313

image-20220319015523699

중요5) 마지막 구현체(hit)의 단독메서드 구현 와중에 -> 상카 아래 모든 구현체(hit, blackjack)들이 <구현은 다르지만 모두 호출 가능>한 공통메서드 -> 인터페이스에선 추메로 추상화하고, 직접 추상화이후 @Override 직접 붙이기

중요6 my) 추메로 추상화해도 구상체별로 개별 구현해줘야하는데??? —> 인페의 추메로 추상화 핵심은 코드 중복제거가 아니라 구현체 모두 호출가능구현체전용 단독메서드를 올려 다운캐스팅 제거+ 파라미터/응답/변수에서 추상체로 받아 일괄처리 가능해진다.

16 구현체 모두가 호출가능할 수 있다면, 인페-추메(전메)로 추상화해서 -> 다운캐스팅 제거후 추상체.전략메서드호출()로 호출이 가능해진다.. 그전에…

참고) 구현체 단독메서드 -> 추메/전메로 올리기 전 확인은 다이어그램+ creation mode + method로 하자

image-20220319103028632

  1. 현재 hit와 blakcjack의 구현체 모두 단독메서드(draw)들을 구현은 달라도 호출가능이라면 올릴 준비를 한다. image-20220319100923432 image-20220319102850048
중요7) 구현체 단독메서드 -> 추메/전메로서 (선언부 시그니쳐만) 위로 올릴 때, 받는 변수/응답값/파라미터 등에 구현체가 있다면, 그것들도 추상체로 바꿔서 올려줘야한다.
  1. 구현체의 단독메서드 -> (선언부 시그니쳐만) 추상체의 전메/추메로 올릴 때 -> 복붙해서 가져간 뒤 구현체로 응답이 보인다면 추상체로 바꿔줘야한다.

    • hit의 draw() 단독메서드를 올린다고 가정하보자.

      image-20220319103202924

    • 올리기 위해 추상부인 선어부만 복사 image-20220319103508849

      image-20220319103529471

    • 선언부 복붙후, 구현체 응답이 보이면 -> 추상체로 변경

      image-20220319103342056 image-20220319103613495

  • 추메 선언부에서 추상체응답으로 바꿔두면, image-20220319104939378
    • 기존 구현체 구상체응답으로 개별구현이 계속 허용된다. image-20220319104949144
참고) 인페의 추메/전메로 선언부를 올릴 때, 접근제한자 제거해서 default로 일단 시작하기

image-20220319103701531 image-20220319103710134

중요) 인페의 추메/전메수동 추상화 올린 다음엔하고 난 뒤, usage-impl 수를 확인해서 개별 구현남아있어야하니 @오버라이딩만 직접 가서 해준다.

image-20220319103807992 image-20220319103826404

image-20220319104434927 image-20220319104442530

image-20220319104457959

중요) 추상화 끝난 단독메서드는 더이상 구현체만의 단독메드가 아니다 == 다운캐스팅 안해도 (오버라이딩된) 추상체변수로 받은 상태에서 추메/전메로서 호출가능해진다. -> 다운캐스팅 제거하고 추상체변수로 받도록 수정하기
  • 기존 Hit 구현체에만 단독이라고 믿어서 개발했던 단독메서드가

    • 추상화 -> 추메/전메가 되었고 + 오버라이딩 @도 달게되었다면
    • 다운캐스팅을 제거한 추상체변수로 받아 -> 바로 호출이 된다.

    image-20220319112025073

    image-20220319112043336

    image-20220319112150915

17 hit 에서 메서드 호출가능해서 -> 모든 구현체 호출가능 -> 메서드 추상화 해준 이후 -> 응답으로서 또 여러 경우의수가 나온다. -> hit + ?? -> 복사해놓고 구체적으로 1case를 또 정해주고 빠른통과하도록 메서드짜기

참고) 다음 가능한 상태가 여러개로 나올 경우, 변화전 given현재 상태의 테메를 그만큼 복사해놓고, case를 정해놓고 진행해나가자

image-20220319105911817

  • hit -> draw(card) -> ??? hit or stay 중에 만만한 것으로 가면 된다.
    • hit부터 한다고 가정한다.
  1. given된 상태의 테메를 복사해서

    • 예상응답값: hit
    • 가정인자: 다음 응답도 hit가 되도록 +1(ACE)를 draw()한다.

    image-20220319110005291

     @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);
     }
    
  2. 이제 case에 맞게 메서드를 수정한다.

    • 일단 new Hit()만 반환해주면 된다.

      image-20220319110116470

      image-20220319110207366

        @Override
        public Hit draw(final Card card) {
            return new Hit();
        }
      
  3. hit담에 hit가 나오므로 통과한다.

18 hit에서 새로운 응답값(= 객체 상태)가 나오는 case에서는 테메이름 + case로 가정인자 조절 + expected로서 assert-isInstanceOf의 새객체.class등 미리 작성하고 하나씩 만들어나간다.

  1. hit -> hit 뿐만 아니라 다른 경우의 수인 hit -> Bust를 위해 기존hit가 확인된 테스를 복사해뒀었다. -> 테메이름을 변경하자
    • 테메이름을 변경하자 -> 기존 + 다음상태case image-20220319110505821
  2. 주어진 hit상태에서 가정인자들도 다음상태를 유도하는 것으로 바꿔주자

    image-20220319110613092

  3. 예상값이 Bust의 상태객체가 나와야한다.

    • 빨간줄로 만들어가자

    image-20220319110902749

참고) 카테고라이징(추상체 인페보유)된 상태들에 -> 새 객체를 추가한다면? -> class생성(fi or ab)후 해당 추상체를 impl -> 그에 따라 추메/전메를 impl하여 카테고리에 추가해준다.

image-20220319110954732

image-20220319111104419

image-20220319111126241

image-20220319111148461

image-20220319111152672

Bust 상태에서 추메/전메를 impl하면 다음 상태로는 뭘 줘야할까? 끝나야할까? 일단은 return null;로 비워두기
  • 아직 버스트에서 draw가능한지 / 다음 상태는 뭘로 줄지가 안정해졌으므로 구현체 단독메서드지만 일단 State응답 -> return null;로 비워둔다.

    image-20220319111328522

public final class Bust implements State {
    @Override
    public State draw(final Card card) {
        return null;
    }
}

image-20220319111406656

19 hit 상태에서 개별구현되고 있는 트리거 메서드에서 호출시 Bust가 응답되도록 메서드 로직 짜주기

  1. ctrl+F12를 통해, 추상체 변수.추메()로 보이는 구현체 Hit의 메서드로 찾아간다. image-20220319112708880
    • 현재는 구현체hit에서 호출시 new Hit()만 반환하게 되어있다. image-20220319112733945

중요) 개별구현중인 구현체 메서드에서 다른 구현체로 넘어가는 로직짜기 -> 이전 정보를 받아와 by 재료를 생성자를 통해 현재 상태를 나타내줄 정보들을 상태값(인변)로서 내부에 가지고 있어야만 상태패턴의 상태객체다.

image-20220319112955070

@Override
public Hit draw(final Card card) {
    // 0. 현재는 무조건 hit -> draw -> hit를 응답하지만
     return new Hit();

    // 1. if 21이 넘으면 -> Bust를 응답해줘야한다.
}

20 필요에 의해 현 상태 -> 다음 상태로 넘어가는데 필요한 정보들상태값(인변)으로 가지고 있어야하며 -> 트리거 메서드에 의해 실시간으로 바뀌는 상태값으로서 State가 내부에서 직접 상태값으로 이미 가지고 있어야만 한다.

  1. hit 상태에서

    • if 21이 넘으면 bust객체로 응답
      • 21이 넘는지는 내부에 필요정보인 cards를 들고 있어야만 가능하다.
     @Override
     public Hit draw(final Card card) {
         // 0. 현재는 무조건 hit -> draw -> hit를 응답하지만
         return new Hit();
        
         // 1. if 21이 넘으면 -> Bust를 응답해줘야한다.
         // 2. draw()시 21이 넘는지 안넘는지 판단하기 위해서는
         // -> 들어오는 card에 대해, 현재 cards의 정보가 필요하다.
     }
    
중요) 현재 상태값(인변)으로 이미 정보를 가진다? -> 상태값 필드(인변) + 생성자에서 재료받아 초기화형태가 있어야 이전 정보를 가져와 실시간 업데이트 가능한 정보로 보유한 셈이 된다.
  1. 일단 필요한 정보로서 상태값(인변)을 private final로 가지자

     public final class Hit implements State {
        
         //3. card1장을 받기전에, [현재 상태를 상태값 cards]으로 가지고 있어야한다.
         // -> 상태값(인변)을 가진다? -> 생성자에서 재료받아 초기화한다.
         private final Cards cards;
    

    image-20220319142409469

    image-20220319142447588

     public final class Hit implements State {
        
         //3. card1장을 받기전에, [현재 상태를 상태값 cards]으로 가지고 있어야한다.
         // -> 상태값(인변)을 가진다? -> 생성자에서 재료받아 초기화한다.
         private final Cards cards;
        
         public Hit(final Cards cards) {
             this.cards = cards;
         }
    

21) 상태값(정보)를 추가를 위해 필드+재료받는 생성자를 추가했더니 기존의 재료없이 기본생성자를 이용한 것들이 에러가 난다.

image-20220319142605889

참고) new()로 생성자정의없이 기본생성자로 테스트에서 미리 많이 쓴 경우 -> 재료받는 생성자가 생기는 순간 재료받는 생성자가 주 생성자 -> 위에 this를 활용해서 재료 안받고 -> 내부 빈 재료로 정보 초기화 하는 부새성자를 추가해주자.
  1. 재료없는 생성자 생성방법은 -> genearte 후 consructor -> Select None으로 생성하면 된다. image-20220319142754680 image-20220319142835558

     public Hit() {
            
     }
    
  2. 재료받는 생성자가 주생성자이며, 재료 생성말고 빈 재료로 정보 초기화해주자.

    • 그 전에.. 사용처를 보니 new Hit()재료없는 기본생성자로 사용한 경우가 1usage로 현재 여기 Hit class내부 밖인 상황이다. -> 기존 테스트 유지를 위한 생성자 추가 생성을 할 필요가 없는 상황임

      image-20220319143753073

    • F12눌러서 따라가도 현재 상황 image-20220319143812822

    • 그렇다면 굳이, 코드 유지를 위한 재료안받고 빈재료로 정보초기화해주는 기본생성자가 필요하지 않다. image-20220319143915211

중요) 추가정보(by트리거 메서드 인자)를 받아 현재 정보(인변으로 보유)를 업데이트해서 판단한다

일급컬렉션 정보(상태값)의 add/remove()는 내부 정보를 변화시키지말고 변화된 내부정보로 일급을 만들어 반환하여 불변 && 밖에서 반환된 것을 이용

22) 이전 카드정보들을 상태값으로 보유하고 있는 상황에서 -> 다음 상태로 가기위에 카드1장의 추가정보(메서드인자)를 받아 현재 정보(인변으로 보유)를 업데이트하자

image-20220319144417304 image-20220319144431645

image-20220319144852636

일급 내부변수를 변경시키려한다면(list.add/remove) -> 불변 일급으로 만들어서 -> 변경된 list(정보)로 새 일급을 새로 만들어 반환해주면 -> getter로 상태값을 열람할 필요가 없어진다.
  • 일급.add( )후 상태값 확인시 -> 불변이면서 add가 새 일급을 응답하도록 불변 일급컬렉션만들어주기
    • 비슷한 사례:
      • Count VO+=내부 상태값 변화시키고 그게 필요
      • 일급컬렉션add내부 상태값 변화시키고 그게 필요
  1. 기존 상태값은 변경하기 전에, 복사를 먼저 한다.

    • 나쁜예: 기존 상태값에 .add()로 자체변경을 먼저 가하면 안된다. image-20220319145045191

    • 기존 상태값은 건들지말고, 변경을 가할 복사본을 먼저 만든다 image-20220319145426067

  2. 복사된 상태값에 변경을 가한다. image-20220319145546755

     public void add(final Card card) {
         //1. 기존 상태값을 변경없이 먼저 복사한다. (기존 상태값을 .add()로 바로 변경하지 않는다)
         final List<Card> newValue = new ArrayList<>(value);
         //2. 복사된 상태값에 증감을 가한다.
         newValue.add(card);
     }
    
  3. 증감이 반영된 복사된 새 상태값을 외부이므로 상태값이 아닌 포장된 일급객체로 응답해줘야한다. image-20220319145820601

     public Cards add(final Card card) {
         final List<Card> newValue = new ArrayList<>(value);
         newValue.add(card);
         //3. 증감이 반영된 복사 상태값을 -> **외부에서 일급**으로 쓰이니
         // -> [새 일급으로 포장해서 응답]**
         return new Cards(newValue);
     }
    
참고) 외부에서 내부(증감)변화의 메세지를 보낸 뒤 -> 그 내부상태를 가져다 쓰고 싶다면 -> 불변 포장객체로 응답해서 밖에서 사용가능하게 하라

23) 불변 일급컬렉션으로 증감시 새객체를 반환해주면 -> 외부에서 .add 해줘도 내부만 변화되는게 아니라 외부에서는 증감이 반영된 내부정보를 포장한 일급을 받아쓴다 -> 메세지 보내서 업데이트된 내부 정보에 대해 물어보기만 하면 된다.

  1. 일급.add( 추가정보 )로 내부만 변화될 것 같지만 image-20220319150115971

    • 불변 일급내부에서 변화가반영된 새 일급을 응답해주니 그 정보를 밖에서 이용하면 된다.

    image-20220319150143247

  2. 이제 변화가 반영된 정보를 포장한 일급 컬렉션으로 메세지를 보내서 현재 상태를 물어보면 된다.

     @Override
     public Hit draw(final Card card) {
         final Cards currentCards = cards.add(card);
         // 21이면 블랙잭
        
         // 21넘으면 Bust
        
         // 21이하면 
        
         return new Hit();
     }
    
  3. 변화된 정보를 담은 일급에게 21넘으면 Bust인지 물어보자. image-20220319150826546

    • 물어볼 때, 내부에서의 this.value변화가 반영된 정보상태임을 인지하자.

    image-20220319150904789

    • this.valuevalue등을 사용하지 않는 같은 class에서 정의되었으며, 내부에서는 잘 사용되고 있는 이미 정의해놓은 메서드()를 호출해서 물음에 답(합)을 구할 수 있다. image-20220319151102688
     public boolean isBust() {
         return sum() > 21;
     }
    
  4. 이제 또다른 Hit 구현체에서 정의한 draw()또다른 상태의 구현체도 return 메서드가 되었다.

    image-20220319151255832 image-20220319151309571 image-20220319151317627

    image-20220319151357381

24) 다른 구현체가 안되더라도, 같은 구현체로 응답(hit->hit) 되더라도 추가정보를 반영하여 응답된 업데이트된 정보 by 불변 일급를 담아서 -> 업데이트된 구현체로 응답되어야한다.

image-20220319151513823 image-20220319151600709

@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를 처리하자.

  1. bust까지 온 상태의 테메를 복붙한다.

    • 테메이름을 다음상태는 없으니 현상태 + 뽑았다정도로 짓는다.

    image-20220319152317273

참고) 원하는 상태까지 업데이트 트리거 메서드를 체이닝으로 호출해서 한번만 할당해놓자
참고) 추상체 응답Type의 구현체 상태 업데이트 트리거 메서드(draw)체이닝으로 원하는 상태 만들 때까지 갈 수 있다.
  • 재할당으로 업데이트 하는 순간 -> assert문에서 람다캡처링에 걸려 메서드호출이 안되더라

    image-20220319155227262

  1. 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 메서드체이닝으로 업데이트
      
  2. 테스트시 예외발생안해서 통과안되어야한다. 아직 로직을 안짠 실패하는 코드 상태이므로

     @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로 잘타고가서 개발하자

  • 겉으로 보기엔 추상체 메서드를 개발하는 것 같다 image-20220319160038854
  • 하지만 현 구현체(Bust)를 잘타고 들어가서 개발해야한다. image-20220319160118814
  1. Bust의 .draw()메서드로 가서 예외발생로직을 짜주자.

    image-20220319160150981 image-20220319160212076

     public final class B ust implements State {
         @Override
         public State draw(final Card card) {
             throw new IllegalStateException();
         }
     }
    
  2. 테스트가 통과된다(호출시 예외발생으로 종료)

참고) 다이어그램에서, return이 Null이거나 Thr라면 물음표?가 찍혀있게 된다. ( 업데이트 안됨)
  • return을 억지로 new Bust()로 준 상태ㅁ

    image-20220319160401453

  • return null;로 준 상태 image-20220319160447468