📜 제목으로 보기

세팅

패키지 생성 (각종 설정 후)

  1. 프로젝트 생성 했다면

    1. main>java
    2. test>java 밑에 패키지 생성(lotto)부터 해주기

    image-20220304161611485 image-20220304161630257

시작점 고르기 (game, service)

  1. 시작점 찾기
    • 로또번호부터 구현할 수 있느냐?
    • 로또부터?
    • 정제된 rawInput을 받은 Game, Service부터 구현해서 객체/도메인도출을 천천히
      • 처음부터 객체 도출은 어려울 수 있다.

Service method TDD

run()테메에서 static한 Service.start()메서드로 메인흐름 시작하기

  1. LottoServiceTest를 만들고

  2. 테스트 메소드명도 편하게 run()

  3. Service클래스의 static메소드명run() start()로 만들면서 시작한다.

    image-20220304162256650

요구사항에 따른 [정제 rawInput]을 Controller내 service가 가정 인자로 사용하여 -> Service 흐름 시작하기

  • 테스트해야하므로 메인흐름을 가진 메서드는 controller로 응답까지 해줘야한다.

    • service는 상태를 안가지므로 객체 - 예상객체 비교X, 응답값 - 예상값으로 비교해야한다.
      public class LottoServiceTest {
          @Test
          void run() {
              //사용자의 로또번호
              //당첨번호
              // 보너스번호
              // -> 당첨 등수 응답
              LottoService.start();
          }
      }
    
    • 로직에 필요한 인자(정제된 input)를 가상으로 넣어주기
      LottoService.start(
          List.of(1, 2, 3, 4, 5, 6),
          List.of(1, 2, 3, 4, 5, 6),
          7
      );
    
    • 로직결과로 응답까지 해줘야 테스트가 된다.

가정인자에 따른 1 case만 1응답하도록 짠 뒤 테스트

가정인자( 6개 일치) = 1case를 넣어줬으니 -> 1응답을 반환하도록 일단 1 case만 짠다.

  1. 테스트할 메서드는 처음에는 무조건 응답되도록 void를 수정한다. 어떤 형으로 응답할지를 생각해서 정의시 기입한다.

    image-20220304170059584

  2. 3개이상의 긴 인자는 콤마 다음부터 내려서 이쁘게 정리해주자.

     public class LottoService {
        
        
         public static int start(final List<Integer> integers, final List<Integer> integers1, final int i) {
        
         }
     }
        
    
     public class LottoService {
        
        
         public static int start(final List<Integer> integers, 
                                  final List<Integer> integers1, 
                                  final int i) {
        
         }
     }
        
    
  3. 메서드 파라미터 이름을 바꿔준다. image-20220304170312486

  4. 아직 도메인을 몰라서 클래스로 분리 못하겠다면, 메인 로직을 rawInput 가지고 짠다.

     public static int start(final List<Integer> userLotto,
                             final List<Integer> winningLotto,
                             final int bonusNumber) {
        
     }
     }
    
  5. 1case에 대한 1응답을 해주는 로직을 완성한다.

    • if가 포함된 1응답이라면, return 0; 같은 default 응답도 넣어준다.
     public static int start(final List<Integer> userLotto,
                             final List<Integer> winningLotto,
                             final int bonusNumber) {
        
         // 몇개 일치하는지 돌면서 누적시켜야한다
         // - 돌아가야하는 놈을 우측 파라미터+for문에
         // - 전체를 가지고 contains 확인해야하는 놈을 좌변에
         int matchCount = 0;
         for (Integer lotto : userLotto) {
             if (winningLotto.
                 contains(lotto)) {
                 matchCount += 1;
             }
         }
        
         // 일단 가정 인자에 따른 1 case통과 코드만 짠다.
         // 나중에는 matchCount 갯수에 따라 싹다 분기해서 응답해야한다.(리팩 대상)
         if (matchCount == 6) {
             return 1;
         }
         return 0; // 1case 응답인데, if를 포함해서 응답했다. -> 1외 default return 0;도 해주자.
     }
    
  6. 1응답이 나오는지 test한다.

     public class LottoServiceTest {
         @Test
         void run() {
             final int rank = LottoService.start(
                 List.of(1, 2, 3, 4, 5, 6),
                 List.of(1, 2, 3, 4, 5, 6),
                 7
             );
        
             assertThat(rank).isEqualTo(1);
         }
     }
        
    

[테메] 복붙후 2case에 맞게 가정인자+예상값 바꾼 테스트 메서드 만들기

복붙 전, 각 case로 테메명 바꿔주고 난 뒤 복붙하기
  • 현재 run()으로 되어있는 테스트메서드명case를 반영해서 바꿔주고 난 뒤 복붙

    • 복붙하는 이유: 기존 테스트가 계속 돌아가도록 하기 위함.
      @Test
      //void run() {
      void rank1() {
          final int rank = LottoService.start(
              List.of(1, 2, 3, 4, 5, 6),
              List.of(1, 2, 3, 4, 5, 6),
              7
          );
        
          assertThat(rank).isEqualTo(1);
      }
    
복붙후 가정인자+예상값을 2case에 맡게 수정
  1. 1등 반환case 1개 이후에는 -> 복붙후 가정인자를 바꾼 case변경을 하면서 새로 짜야한다.

    1. 테스트메서드 명을 case에 맡게 복사해준다.
    2. 가정인자 및 예상값을 -> 2case에 맞게 수정해준다.
     @Test
     void rank2() {
         final int rank = LottoService.start(
             List.of(1, 2, 3, 4, 5, 6),
             List.of(1, 2, 3, 4, 5, 7), // 2등에 맞게 가정인자 수정
             6 // 2등에 맞게 가정인자 수정
         );
        
         assertThat(rank).isEqualTo(2); // 2등에 맞게 예상값 수정
     }
    

[메서드 복붙]후 2case 예상값을 응답하도록 메서드2를 수정

  1. 메서드2로 복붙한다.

    • 단축키 shift + alt + →를 활용해서 복사하고
    • 단축니 shift + alt + F로 정렬해서
     public static int start2(final List<Integer> userLotto,
                              final List<Integer> winningLotto,
                              final int bonusNumber) {
    
  2. 메서드2가 case2예상값을 만들도록 짠다.

     public static int start2(final List<Integer> userLotto,
                              final List<Integer> winningLotto,
                              final int bonusNumber) {
         int matchCount = 0;
         for (Integer lotto : userLotto) {
             if (winningLotto.contains(lotto)) {
                 matchCount += 1;
             }
         }
        
         if (matchCount == 6) {
             return 1;
         }
         if (matchCount == 5 && userLotto.contains(bonusNumber)) {
             return 2;
         }
        
         return 0;
     }
    
  3. 테메2가 메서드2를 사용하도록 수정한 뒤 테스트한다.

     @Test
     void rank2() {
         final int rank = LottoService.start2(
    

[기존 테메]도 [메서드2]로 돌아가는지 확인한다.

@Test
void rank1() {
    final int rank = LottoService.start2(

[메서드2]를 [메서드]로 완전히 대체한다.

  1. 기존 메서드 회색 비활성화된 메서드를 안전삭제(alt+del)한다. image-20220304174635847
  2. 메서드2 -> 메서드(F2 2번)로 이름을 바꿔서 완전히 대체한다. image-20220304174650074

[2case이후 테메]는 복붙후 한번에 처리해보자.

[다case]테메명 + @ParaemeterizedTest를 통해 가정인자+ 예상값들을 변경시키며, 그에 맞는 응답을 하도록 [메서드 원본]을 수정해서 통과되도록 만들자.
  1. 가정인자가 2개, 예상값 1개, 총 3개 인자가 case에 따라 바뀐다.

    1. List.of()
    2. bonusNumber

    3. expected

    @CsvSource 혹은 @MethodSource를 통해 여러 파라미터를 받아야한다.

     @ParameterizedTest
     @MethodSource("provideWinningLottoAndExpected")
     void rank3_to_5(){
    
     public static Stream<Arguments> provideWinningLottoAndExpected() {
         return Stream.of(
             Arguments.of(List.of(1, 2, 3, 4, 5, 7), 8, 3),  // 3등
             Arguments.of(List.of(1, 2, 3, 4, 7, 8), 9, 4) ,  // 4등
             Arguments.of(List.of(1, 2, 3, 7, 8, 9), 10, 5)  // 5등
         );
     }
    
     @ParameterizedTest
     @MethodSource("provideWinningLottoAndExpected")
     void rank3_to_5(final List<Integer> winningLotto, final int bonusNumber, final int expected) {
         // 파라미터 추가 + 가정인자/예상값 다 변경 -> 테스트시 에러나야 정상
         final int rank = LottoService.start(
             List.of(1, 2, 3, 4, 5, 6),
             winningLotto,
             bonusNumber
         );
        
         assertThat(rank).isEqualTo(expected);
     }
    
  2. 바뀐 가정인자 + 예상값에 대응하도록 원본 메서드를 1,2case에 비추어 수정

     if (matchCount == 6) {
         return 1;
     }
     if (matchCount == 5 && userLotto.contains(bonusNumber)) {
         return 2;
     }
     if (matchCount == 5) {
         return 3;
     }
     if (matchCount == 4) {
         return 4;
     }
     if (matchCount == 3) {
         return 5;
     }
    

service 메서드 완성후 회고

  • 여기까지 통과시키고 난 뒤, 가정인자로 들어오는 놈들에 대해 도메인 지식이 쌓였을 것이다.
    • 가정인자로 들어오는 List에 대해 개별Integer는 개별 객체 LottoNumber라는 객체로도메인 분리할 수 있겠다.
    • LottoNumber List 6개를 모아서 Lotto라는 객체로 도메인 분리모아볼 수 있겠다.

구현된 Service method의 가정인자 리팩토링

  • 도메인 지식을 높이기 위해서, 가정하고 넣어주는 인자rawInput인체로 빨리 돌아가기만 하는 코드를 짠 상태다.

  • 유지보수, 계속 봐야하는 코드라면 리팩토링해야한다.

입력인자 예외처리에 의한 도메인 추출 인지

  • 테스트코드가 가정인자+예상값의 변화 없이 개별 도메인 생성자 호출자체에서 문제이기 때문에, 개별도메인으로 가서 테스트 된다.
[입력인자] [가장 작은 단위]의 숫자(로또번호)가 범위를 벗어나는 경우
  1. 기존 rank1~5 테스트라도 복붙해서 메서드명을 over로 바꿔고 범위를 벗어나는 경우에 대한 가정인자 + 예상값이 아니라 user입력인자를 바꿔서 만들어주자.

    • 사용자 입력에 대한 예외에 대한 테스트의 경우, service메서드의 가정인자 변화가 문제가 아니라 입력인자실수가 반영된 것이기 때문에 객체로 도메인 분리해서 도메인 검증해야한다.
     @Test
     void over() {
         final int rank = LottoService.start(
             List.of(1, 2, 3, 4, 5, 100), // 사용자 입력인자만 예외인자로 변화 -> 100 입력
             List.of(1, 2, 3, 4, 5, 7), // 가정인자 변화X
             6
         );
        
         assertThat(rank).isEqualTo(2); // 예상값 변화X
     }
    
[입력인자] 가장 작은 단위가 상호작용한 [일급]에서 중복 문제
  1. 테메 복붙후 이름변경duplicate

  2. 입력인자에서 중복이 되도록 입력

    • 예외 테스트 는 가정인자 + 예상값의 변화가 없는 상태임.
     @Test
     void duplicate() {
         final int rank = LottoService.start(
             List.of(1,1,1,1,1,1), // 사용자 입력인자만 예외인자로 변화 -> 1이 list내 중복 됨.
             List.of(1, 2, 3, 4, 5, 7), // 가정인자 변화X
             6
         );
        
         assertThat(rank).isEqualTo(2); // 예상값 변화X
     }
    

Service.메서드() 사용시 발견된 값들의 예외처리 -> 개별 도메인에서 테스트하면서 처리되어야함.

  • 도메인을 풍부하게 -> 도메인으로 추출 분리도메인 내에서 검증

  • 서비스메서드 사용하면서, 그 service(메인로직) 흐름내에서 if로 하지말자. 자기 도메인내에서 if없이 개별로 처리되어야한다.

    • 잘못된 예(service에서 인자로 넘어온 도메인들의 예외처리)
      public static int start(final List<Integer> userLotto,
                              final List<Integer> winningLotto,
                              final int bonusNumber) {
        
          if (userLotto.get() > 45) {
              throw new IllegalArgumentException("범위 초과");
          }
    

메서드인자 리팩토링1) 최소단위 값 검증 때문에 포장하도록 Service.메서드() 수정

Service.메서드() [최소단위 값] 1개만 new 생성자()로 싸서, Class와 ClassTest를 정의

  1. Service.메서드() 로직을 사용하던 곳에서 예외발생 상황을 가정했던 곳에서 new 생성자()를 씌워서 Class -> ClassTest까지 만들어보자.

     @Test
     void over() {
         final int rank = LottoService.start(
             List.of(1, 2, 3, 4, 5, new LottoNumber(100)),  // 검증이 필요한 곳 -> class로 도메인분리 -> 생성자부터 씌워서 만들어나가기
    
  2. 값을 1개만 포장하는 경우, 변수명을 value로 잡아서 getValue()등 이름을 어색하지 않게 하자.

    • 생성자로 클래스 생성후, 상태값 직접 지정해줘야한다.
     package lotto;
        
     public class LottoNumber {
         public LottoNumber(final int value) {
         }
     }
    
     public class LottoNumber {
            
         private final int value;
        
         public LottoNumber(final int value) {
             this.value = value;
         }
     }
    
  3. classTest도 바로 만들기

     class LottoNumberTest {
        
     }
    

Service.method()의 [포장]된 [입력인자]버전의 메소드를 만들기 위해 메소드1복붙

  • 기존테스트를 통과하는 메소드2의 개발이 아니라 인자가 달라지는 동일명의 메소드를 개발하는 것이므로 메소드명 동일하게 복붙한다.

    • 기존 포장안한 테스트가 그대로 통과한 상태를 유지할 수 있다.
    • 포장이 실패할시 바로 돌아갈 수 있다.
    • 메소드2로 지어도 상관없을 것 같긴하다.

    image-20220304221024997

메소드 파라미터 정의를 원시형 -> 포장형으로 바꿔주기

  1. 메소드의 파라미터 정의부를 원시형을 모두 포장 형으로 바꿔준다.

    • int, Integer -> LottoNumber
     public static int start(final List<LottoNumber> userLotto,
                             final List<LottoNumber> winningLotto,
                             final LottoNumber bonusNumber) {
    
  2. 내부도 다 타입을 바꿔준다.

    • 에러가 안나는 듯 하지만, 포장class에서도 기본 제공되는 contains()는 비교/포함여부 되려면 equals/hashCode가 오버라이딩 되어있어야 에러가 안난다.
    • 최소단위 포장은 equals/hashCode생각 image-20220304221640210
     public class LottoService {
        
         public static int start(final List<LottoNumber> userLotto,
                                 final List<LottoNumber> winningLotto,
                                 final LottoNumber bonusNumber) {
        
             int matchCount = 0;
             for (LottoNumber lotto : userLotto) {
                 if (winningLotto.contains(lotto)) {
                     matchCount += 1;
                 }
             }
    

포장용 Service메소드에서 사용되는 값들 모두 포장해서 전달해주기

  1. 현재 생성을 위해 1개만 포장된 상태다. 수동으로 다 포장해줘야한다.

     @Test
     void over() {
         final int rank = LottoService.start(
             List.of(1, 2, 3, 4, 5, new LottoNumber(100)), // 사용자 입력인자만 예외인자로 변화 -> 100입니다.
             List.of(1, 2, 3, 4, 5, 7), // 가정인자 변화X
             6
         );
        
         assertThat(rank).isEqualTo(2); // 예상값 변화X
     }
    
    • 포장 후
     @Test
     void over() {
         final int rank = LottoService.start(
             List.of(new LottoNumber(1), new LottoNumber(2), new LottoNumber(3), 
                     new LottoNumber(4), new LottoNumber(5),new LottoNumber(100)), 
             List.of(new LottoNumber(1), new LottoNumber(2), new LottoNumber(3), 
                     new LottoNumber(4), new LottoNumber(5),new LottoNumber(7)), 
             new LottoNumber(6)
         );
        
         assertThat(rank).isEqualTo(2); // 예상값 변화X
     }
    

Exception 나는 것을 의도하여 포장했다면 에러가 나야한다 assertThat -> assertThatThrownBy()의 테스트 코드 바꾸기

  1. 응답 vs 예상값비교 구조에서 () -> exception발생구조로 테스트메서드를 바꿔주자.

     @Test
     void over() {
         assertThatThrownBy(() -> LottoService.start(
             List.of(new LottoNumber(1), new LottoNumber(2), new LottoNumber(3),
                     new LottoNumber(4), new LottoNumber(5), new LottoNumber(100)),
             List.of(new LottoNumber(1), new LottoNumber(2), new LottoNumber(3),
                     new LottoNumber(4), new LottoNumber(5), new LottoNumber(7)),
             new LottoNumber(6)
         )).isInstanceOf(IllegalArgumentException.class);
     }
    

포장한 도메인 생성자에 예외처리 검증 넣어주기

  1. 예외 발생 안했다 -> 예외발생시키는 검증을 도메인에 넣어준다.

    public LottoNumber(final int value) {
        validate(value);
        this.value = value;
    }
        
    private void validate(final int value) {
        if (value < 1 || value > 45) {
            throw new IllegalArgumentException("로또 번호의 범위가 아닙니다.");
        }
    }
    

[Service.method()에서의 검증]이 끝났으면 [단일 도메인에서 본격 검증]하러 classTest로 직접가서 [경계값 테스트]하기

  • 시간 관계상 Service.method()에서 통채로 테스트하지만, 포장ClassTest에서 직접 경계값 주위로 테스트해야한다.
class LottoNumberTest {

    @ParameterizedTest
    @ValueSource(ints = {0, 46, -1, 100})
    void range(final int value) {
        assertThatThrownBy(() -> new LottoNumber(value))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining("범위");

    }
}

예외 발생을 위한 포장 회고

  • Service.메서드()에서 테스트했지만, LottoNumber로는 더이상 over되는 번호는 오지않겠구나.
    • 심리적 안정감을 얻었다.
포장 입력인자 메소드 사용시 애로사항
  • Service.메서드() 사용시 모든 원시값을 new 포장( )으로 일일히 감싸줘야한다.

      LottoService.start(
          List.of(new LottoNumber(1), new LottoNumber(2), new LottoNumber(3),
                  new LottoNumber(4), new LottoNumber(5), new LottoNumber(100)),
          List.of(new LottoNumber(1), new LottoNumber(2), new LottoNumber(3),
                  new LottoNumber(4), new LottoNumber(5), new LottoNumber(7)),
          new LottoNumber(6)
      ));
    
  • 포장된 입력인자가 아니라 -원시값 입력인자를 그대로 사용하되, 메소드 내부에서 한꺼번에 포장객체로 변환시키자

[정제된 rawInput을 받던 테매들이 사용하던 service.method()] 내부 개조로 한번에 포장해서 포장인자 메서드 호출해주기

기존에 작성된[ 정제된 rawInput을 받던 메소드]의 내용물은 삭제
  • 기존 테스트를 위해 정제된 원시값을 받던 메소드

      public static int start(final List<Integer> userLotto,
                              final List<Integer> winningLotto,
                              final int bonusNumber) {
        
          int matchCount = 0;
          for (Integer lotto : userLotto) {
              if (winningLotto.contains(lotto)) {
                  matchCount += 1;
              }
          }
    
원시값 인자 네이밍 -> 내부stream으로 한번에 포장 -> return 포장 인자 메서드() 호출
  • **기존 내용물은 삭제(포장된 것으로 처리되도록 통일해야됨) **

      public static int start(final List<Integer> userLotto,
                              final List<Integer> winningLotto,
                              final int bonusNumber) {
        
      }
    
  • **원시값 인자 +raw네이밍 붙이기 **

    • 각 원시인자 기존 네이밍 -> 포장인자로 갈 때 기존 네이밍으로 받아줘야 -> ``포장메서드 호출시 기존 네이밍으로 가기 때문에 **원시 인자 네이밍 앞에 raw`를 붙혀준다**
      public static int start(final List<Integer> rawUserLotto,
                              final List<Integer> rawWinningLotto,
                              final int rawBonusNumber) {
        
      }
    
    • return 포장된 인자를 받는 새 메소드()호출
      • 응답은 그대로 나가야하므로 return 유지
      public static int start(final List<Integer> rawUserLotto,
                              final List<Integer> rawWinningLotto,
                              final int rawBonusNumber) {
        
          final List<LottoNumber> userLotto = rawUserLotto.stream()
              .map(LottoNumber::new)
              .collect(Collectors.toList());
        
          final List<LottoNumber> winningLotto = rawWinningLotto.stream()
              .map(LottoNumber::new)
              .collect(Collectors.toList());
        
          return start(userLotto, winningLotto, new LottoNumber(rawBonusNumber));
      }
    
의의: Service를 사용하는 client를 편안하게 해줘야한다.(내부로 가기전에 포장해서 호출해주는 놈이 있다면) = client는 원시값도 입력받아 도메인은 모르게 해주는게 낫다
  • client쪽, view쪽, 중요하지 않은쪽, 잘변하는 쪽도메인, 포장값 사용없이 원시값으로만 service.메서드()를 호출하고, 내부에서 포장 좋다.
    • 포장을 서비스내에서 한번 거쳐서 하는 방법을 여기선 안내해줬다.
    • 원래 중요하지 않은쪽이 -> 중요한 것을 생성/사용/리턴하는 방향은 허용된다.
  • 생각이 다를 수 있다.
    • 외부에서 무조건 감싸서 로직에 보내줘야한다라고 생각할 수도 있다.
    • but 사용하는 client입장에서는.. 내부포장으로 편안한게 좋다
  • my) controller에서 도메인 사용해도 되지만
    • 내부 포장만 해주는 로직이 따로 존재한다면, 원시값을 넘겨도 될 듯?

메서드 인자 리팩토링2) 최소단위 [묶음]의 예외 처리 -> 일급 포장하여 검증

초과된 [포장 최소단위의 갯수 문제] -> [포장인자 메소드]를 가진 [테매]를 복붙하여 Exception 발생시키기

  • 현상황에서 포장인자를 받던 메소드를 복붙하여 포장객체의 갯수 문제를 발생시켜보자.

    • 아무래도.. 최소단위객체의 범위문제에서 작성된 메소드를 복붙해서 고쳐야함… image-20220304233416210
      @Test
      void lotto7() {
          assertThatThrownBy(() -> LottoService.start(
              List.of(new LottoNumber(1), new LottoNumber(2), new LottoNumber(3),
                      new LottoNumber(4), new LottoNumber(5), new LottoNumber(6), new LottoNumber(7)), // 갯수 초과
              List.of(new LottoNumber(1), new LottoNumber(2), new LottoNumber(3),
                      new LottoNumber(4), new LottoNumber(5), new LottoNumber(6)),
              new LottoNumber(7)
          )).isInstanceOf(IllegalArgumentException.class);
      }
        
    
  • 갯수를 초과했음에도 불구하고 아직 묶음에 대한 검증이 안되기 때문에 예외발생이 안된다.

    image-20220304233624719

  • 앞에서도 그렇고, 예외처리를 Service.method() 내부에서 if로 편하게 해주고 싶은데, 여기서 하면 모든 인자들에 대한 예외처리가 if로 한곳에 쌓일 것이다.

    • **각 검증은 도메인 추출된 class를 만들어서 그곳에서 한다. **
      public static int start(final List<LottoNumber> userLotto,
                              final List<LottoNumber> winningLotto,
                              final LottoNumber bonusNumber) {
        
              // 여기서 갯수처리를 해주고 싶으나..
              if (userLotto.size() != 6) {
                  throw new 예외
              }
              // 2번째 인자도 예외처리를 로직실행 메서드 내부에서 인자를 받아서 처리하고 싶으나..
              if (winningLotto.size() != 6) {
                  throw new 예외
              }
          	// 인자들마다 모든 검증들이 if로 여기(사용메서드)에 쌓여갈 것이다. 따른 검증 역시 여기서 해야한다. 
        
        
    

[객체의 갯수or중복 등 복수 검증]을 위한 List<도메인>을 포장하는 일급포장의 시작

복수검증 문제가 발생하는 곳에가서 new 일급()부터 시작
  1. Service.method()에서 원시값 List -> 단일객체 List가 인자로 들어가는 곳에서 **new 일급( ) ** -> Class -> ClassTest까지 생성해준다.

     @Test
     void lotto7() {
         assertThatThrownBy(() -> LottoService.start(
             new Lotto(List.of(new LottoNumber(1), new LottoNumber(2), 
    
  2. 생성자로 들어오는 이름도value로 네이밍하여 생성자에서 초기화

    • ClassName. 가 PREFIX인 것 및 .getValue() 생각하기
     public class Lotto  {
         public Lotto(final List<LottoNumber> lottoNumbers) {
         }
     }
    
     public class Lotto {
         private final List<LottoNumber> value;
        
         public Lotto(final List<LottoNumber> value) {
             this.value = value;
         }
     }
    
  3. 나머지도 List -> 일급으로 들어가도록 service.method수정단일>

    image-20220305002755888

들어오는 포장인자(List<단일>) 메서드 복붙 백업후 -> 일급인자로 받도록 method 파라미터 정의부 개조
  1. 기존 List<단일>으로 들어오던 메서드 전체를 복붙한다.
    • 기존 테스트 유지?를 위해, 일급으로 바꾸기전 List도 들어올 수 있도록 백업해둔다. ![image-20220305003021893](https://raw.githubusercontent.com/is3js/screenshots/main/image-20220305003021893.png)단일>
  2. 기존 List<단일>으로 들어오던 메서드의 파라미터 정의부정의해준 일급들어오도록 수정한다.

     public class LottoService {
        
         public static int start(final List<LottoNumber> userLotto,
                                 final List<LottoNumber> winningLotto,
                                 final LottoNumber bonusNumber) {
    
    • 수정후
     public static int start(final Lotto userLotto,
                             final Lotto winningLotto,
                             final LottoNumber bonusNumber) {
    
  3. 내부 빨간줄 수정 ( 단일List -> 일급으로 인한 -> 기본적으로 앞에 .getValue()의 getter가 요구됨.)

    • 일단 getter로 다 처리해놓자. 리팩토링 요구됨. 객체 메세지 던져서 통신.

    • 상태값을 value로 네이밍해줬어도, .getValue()의 빨간줄을 리팩토링으로 생성 후 return this.value;까지 직접 입력해줘야된다.

      image-20220305003505785 image-20220305003513722 image-20220305003902488 image-20220305003913582 image-20220305003918375

    • 앞에 getter를 붙이면 빨간줄이 사라진다.

      image-20220305004044030 image-20220305004054296

  4. 일급용 메서드가 완성되었으면

[일급 인자를 받는 Service.method()]가 완성되었으면, [일급이라는 도메인 내부 생성자에 단일List간 복수검증]을 넣어준다.

  1. 일급 도메인인 Lotto의 생성자에서 상태값 초기화 전에 검증을 넣어준다.

     public class Lotto {
         private final List<LottoNumber> value;
        
         public Lotto(final List<LottoNumber> value) {
             this.value = value;
         }
    
    • 도메인 검증 추가
     public class Lotto {
         private final List<LottoNumber> value;
        
         public Lotto(final List<LottoNumber> value) {
             if (value.stream().distinct().count() != 6) {
                 throw new IllegalArgumentException("로또 번호는 6개입니다.");
             }
             this.value = value;
         }
    
  2. 도메인 생성자에 넣어준 검증(중복) 을, Service.method()의 인자검증으로 통채로

         @Test
         void duplicate() {
             assertThatThrownBy(() -> LottoService.start(
                 new Lotto(List.of(new LottoNumber(1), new LottoNumber(1), new LottoNumber(1),
                     new LottoNumber(1), new LottoNumber(1), new LottoNumber(1))),
                 new Lotto(List.of(new LottoNumber(1), new LottoNumber(2), new LottoNumber(3),
                     new LottoNumber(4), new LottoNumber(5), new LottoNumber(6))),
                 new LottoNumber(7)
             )).isInstanceOf(IllegalArgumentException.class)
         }
    

포장(단일)에 포장(일급) 후 client가 너무 불편? -> Client를 배려해줘라.

일급 생성자가 받는 new일급(List<단일>)의 인자를 -> 단일... 가변인자로 받고 내부에서 List.Of대신 모아주는 포장용 생성자 추가

  1. 현재 일급까지 포장해야하는 Service메서드 테매

     @Test
     void duplicate() {
         assertThatThrownBy(() -> LottoService.start(
             new Lotto(List.of(new LottoNumber(1), new LottoNumber(1), new LottoNumber(1),
                               new LottoNumber(1), new LottoNumber(1), new LottoNumber(1))),
             new Lotto(List.of(new LottoNumber(1), new LottoNumber(2), new LottoNumber(3),
                               new LottoNumber(4), new LottoNumber(5), new LottoNumber(6))),
             new LottoNumber(7)
         )).isInstanceOf(IllegalArgumentException.class)
     }
    
    • 인자 1개만 떼서 보기
     new Lotto(List.of(new LottoNumber(1), new LottoNumber(1), new LottoNumber(1),
                       new LottoNumber(1), new LottoNumber(1), new LottoNumber(1))),
    
[Client] 일급 생성자에 들어가는 List.of() 혹은 Set.of() 제거해주는 -> [메서드내부] 가변인자(알아서 배열) -> this + Arrays.stream().collect(Collectors.toList or toSet ())) )
  1. client에서 List.of(단일,객체)나 Set.of(단일, 객체)대신 단일객체를 콤마로 나열만 해서 넘겨주면, 알아서 List or Set으로 내부에서 모아주는

    • 내부에서 포장해준 뒤, 포장 인자용 메서드를 다시 한번 호출해주는
      • 메서드 파라미터 -> 인자만 복사해서 껍데기 메서드 생성 -> 내부에서 포장후 호출 다시 포장인자메서드를 재호출
      • 일급 생성자 -> List.Of, Set.of를 제거하여 생성자를 추가 생성 -> 가변인자를 받도록 정의 -> 배열을 stream collect로 List나 set으로 포장만해서 this( , )로 미리 정의해둔 && 뒤에 존재하는 포장인자용 생성자 호출

    image-20220305083636986

    image-20220305083654611 image-20220305083703228 image-20220305083816209 image-20220305083956293 image-20220305084022308

     public class Lotto {
         private final List<LottoNumber> value;
        
         public Lotto(final LottoNumber... value) {
             this(Arrays.stream(value).collect(Collectors.toList()));
         }
        
         public Lotto(final List<LottoNumber> value) {
             if (value.stream().distinct().count() != 6) {
                 throw new IllegalArgumentException("로또 번호는 6개입니다.");
             }
             this.value = value;
         }
        
    
    • 이제 new Lotto() 일급의 생성자에서 2가지 다 받을 수 있게 된다. List.of( , ,, ) 대신 ->그냥 , , ,의 가변인자를 넣어줘도 알아서 List로 포장하게 된다.
     @Test
     void duplicate() {
         assertThatThrownBy(() -> LottoService.start(
             new Lotto(new LottoNumber(1), new LottoNumber(1), new LottoNumber(1),
                       new LottoNumber(1), new LottoNumber(1), new LottoNumber(1)),
             new Lotto(new LottoNumber(1), new LottoNumber(2), new LottoNumber(3),
                       new LottoNumber(4), new LottoNumber(5), new LottoNumber(6)),
             new LottoNumber(7)
         )).isInstanceOf(IllegalArgumentException.class);
     }
    

가변인자(List.of제거) + 단일객체생성자 제거의 일급 내부에 2번의 포장 생성자도 추가해서 client를 편하게 해주자.

  1. new 일급( ) 생성자에 client에서 List.Of() 제거하고 가변인자만 받도록 내부에서 포장해주는 생성자를 추가해줬었다.

     //1. new Lotto( 단일객체, 가변으로) -> List.Of제거
     public Lotto(final LottoNumber... value) {
         this(Arrays.stream(value).collect(Collectors.toList()));
     }
    
  2. 이번에는 client 단일객체 생성자 제거하고 단일객체 생성에 필요한 raw원시값을 가변인자로만 받도록 수정해보자.image-20220305084817763 image-20220305085319446 image-20220305085359886 image-20220305085457733 image-20220305085526086 image-20220305085606702

    • stream에서 mapToObj가 뜨는 이유는 integer가 아닌 int -> 객체로 가기 때문 -> 애초에 파라미터로 들어오는 원시값들을 int… 대신 Integer...로 box된 타입으로 받을 수 있다. image-20220305085731026
     public class Lotto {
         private final List<LottoNumber> value;
        
         //2. new Lotto( 원시값, 가변으로) -> List.Of() + new단일()생성자 제거
         public Lotto(final Integer... value) {
             this(Arrays.stream(value).map(LottoNumber::new).collect(Collectors.toList()));
         }
        
         //1. new Lotto( 단일객체, 가변으로) -> List.Of제거
         public Lotto(final LottoNumber... value) {
             this(Arrays.stream(value).collect(Collectors.toList()));
         }
        
         public Lotto(final List<LottoNumber> value) {
             if (value.stream().distinct().count() != 6) {
                 throw new IllegalArgumentException("로또 번호는 6개입니다.");
             }
             this.value = value;
         }
    
  3. 이제 단계별로 생성자를 만들어서, 기존 테스트의 인자들이 모두 돌아가게 만들었다. image-20220305085911955

    • 이제 원시값으로 일급을 만들 수 있게 된다.

    image-20220305090007614

     @Test
     void duplicate() {
         assertThatThrownBy(() -> LottoService.start(
             new Lotto(1, 2, 3, 4, 5, 6),
             new Lotto(1,2,3,4,5,6),
             new LottoNumber(7)
         )).isInstanceOf(IllegalArgumentException.class);
     }
    
  • client입장에서 코드를 사용하게 엄청 편해진다.

my) 메서드의 [인자만 포장후 호출용 메서드] or 일급 등의 생성자를 [인자만 다르게 정의하고 내부는 포장해서] client를 편하게 만들어줬다.

  • 가변인자 사용시 주의해야한다.

    • 가변인자는 배열을 내부적으로 만들어서 사용하기 때문에, 메모리 잇슈가 있을 수 있다.

    • 그래서 .of()메서드들은 11개부터 가변인자를 사용하고 10개까지는 인자를 하드코딩으로 받는다.

일급 테스트는 Service.Method()에서 완성된 [가변인자생성자] 시리즈가 완성된 후에 해주자.

[Service.method()에서의 단일/일급 퉁치는 검증] + [일급의 포장용 가변인자 생성자] 끝났으면 [단일 도메인Test]에서 본격 [경계값 테스트]하기

  1. Lotto라는 일급도메인 테스트를 만든다.

    image-20220305090450950

  2. Service.method()에서 퉁치며 만들던 갯수/중복 테스트를 case별로 테매명에 반영하면서 만들어본다.

    • 가변인자는 parameter로 내려보내질 못하네
    • 갯수먼저 검사 -> 중복 검사
      • 중복부터하면? distinct.count() != 6에서 list의 size검사도 같이 일어나 중복에 걸리게 된다.
     @Test
     void count_valid() {
         assertDoesNotThrow(() -> new Lotto(1, 2, 3, 4, 5, 6));
     }
        
     @Test
     void count_under() {
         assertThatThrownBy(() -> new Lotto(1, 2, 3, 4, 5))
             .isInstanceOf(IllegalArgumentException.class)
             .hasMessageContaining("6개");
     }
        
     @Test
     void count_over() {
         assertThatThrownBy(() -> new Lotto(1, 2, 3, 4, 5, 6, 7))
             .isInstanceOf(IllegalArgumentException.class)
             .hasMessageContaining("6개");
     }
        
     @Test
     void duplicate() {
         assertThatThrownBy(() -> new Lotto(1, 1, 3, 4, 5, 6))
             .isInstanceOf(IllegalArgumentException.class)
             .hasMessageContaining("6개");
     }
    

Service.메서드의 이름 수정

  • start -> match image-20220305004131691

일급으로 바꿨더니, 서비스 메서드 내부에 도메인 로직이? -> 메서드 분리로 역할 분리

  • getter가 있으면 도메인내부로직임을 의심한다.

일급vs일급 or 일급vs단일의 로직(도메인 로직-특히 getter써놓은부분)이 Service.method()내부에 있는지 확인한다. getter가 있으면 의심해본다.

  1. Lotto와 Lotto를 비교하는 로직이 보인다. 같은 도메인끼리의 비교를 서비스내에서 할 필요가 없다. 같은 놈끼리는 더더욱 메세지를 보내서 getter의 존재없이 비교되도록 + 도메인을 풍부하게 해준다.

    • 비교로직을 도메인으로 넣고, matchCount만 받아오자.
     public class LottoService {
        
         public static int start(final Lotto userLotto,
                                 final Lotto winningLotto,
                                 final LottoNumber bonusNumber) {
        
             int matchCount = 0;
             for (LottoNumber lotto : userLotto.getValue()) {
                 if (winningLotto.getValue().contains(lotto)) {
                     matchCount += 1;
                 }
             }
    
  2. matchCount는 서비스가 가지고 있어야하는데, 비교해서 누적하는 부분만 도메인 로직이다. image-20220305095245511

서비스내 도메인 로직을 도메인 내부 메서드로 옮긴다.

  1. getter로 꺼내서 억지로 하던 것이, 파라미터로 서로를 비교해도 바로 상태값을 갖다 쓸 수 있다. 일단 옮길 도메인내 메서드부터 만든다.

    • 서비스에서 갖다쓸 예정이니 서비스에서 빨간줄로 만들어도 될듯?

    image-20220305095554197image-20220305095608289 image-20220305095640695

    • 같은 것끼리의 비교는 other로 파라미터를 네이밍해주자
      • 파라미터 other(일급)이 other.value(같은도메인 getter)로 list가 되면서 찢어진다.

    image-20220305105047887

    • 돌면서 누적하는 것은 초기화 후 누적하는 폼가변변수를 쓸 수 밖에 없다?

    image-20220305105122911 image-20220305100147074

     public int match(final Lotto other) {
         int matchCount = 0;
         for (LottoNumber lotto : other.value) {
             if (value.contains(lotto)) {
                 matchCount += 1;
             }
         }
        
         return matchCount;
     }
    

포장과정에서 빠르게 처리하여 생긴 포장.getter.메서드()가 서비스메서드내 보인다면, 도메인로직으로 (내부 value.메서드()가 되도록)넘겨준다.

image-20220305100242729

  1. 서비스메서드내 포장(도메인).getter가 보인다.

    • 넘겨줄 수 없을까?
    • getter()를 지워 getValue().메서드를 포장.메서드()로 빨간줄을 생성하여 대체한다.

    image-20220305100829501

    image-20220305100853434

    • 파라미터(매개변수) 이름으로 bonusNumbe는 너무 구체적이다. 팀원입장에서 시그니쳐보고 쓸건데, 좀더 추상적이면서, 넓게… 메서드 입장에서는 구체적인 bonus라는 것을 모른다. LottoNumber 역시 클래스랑 완전 동일해서.. value를 일급이 이미썼으니 -> number로 네이밍 해준다.

    image-20220305101124747

    • 바깥에서 포장.getValue().contains로 사용됬던 것을 -> 포장.contains메서드 -> 도메인 내부에서 getter없이 value.contains로 사용하도록 만든다.

    image-20220305101234690

  2. 서비스로직에 geValue()를 달고 있는 것ㅇ 도메인 로직으로 옮겨가며되면, 포장(도메인).getValue()가 비활성화 되며 -> alt+del로 안전삭제 해준다. image-20220305103035213 image-20220305103046257

일급과 단일의 메서드가 정의됬다면 -> 일급vs일급에서도 사용될 가능성이 있다.

  • 서비스 로직에 있던 일급.contains(단일)의 메서드가 정의되었다면 image-20220305103300809

    • 일급vs단일은 일급내에서 파라미터/우항이 찢어지는 일급vs일급 비교메서드에서 쓰일 가능성이 높다.

    image-20220305105315418

    • 정의했으면, 찾아서 대체해준다.
  • 객체없이 호출되는 메서드 -> 순수묶음 or [내부 포장된 변수](현재는 일급을 get한 단일List)가 쓰이는 내부 메서드

    • contains( xxx)
    • (현재 도메인의 포장하는 변수)가 생략된체로 cotains()하고 있구나 생각
      public int match(final Lotto other) {
          int matchCount = 0;
          for (LottoNumber lottoNumber : other.value) {
              //value:[일급]       lotto:찢어진 일급이 돌고 있는 [단일]
              // [일급 내부] 메서드 contains는
              // [일급의 getter(단일List)된 value가 내부에서 쓰이는] 메서드로서,
              // 들어오는 단일에 대해 [현재 일급의 단일List]가 <파라미터 단일>을 포함하는지
              // my) 객체없는 메서드 -> 순수묶음 or [내부 포장된 변수](현재는 일급을 get한 단일List)가 쓰이는 내부 메서드
              //            if (value.contains(lotto)) {
              // 객체없이 호출된다? 내부상태가 쓰이는 내부 정의 메서드구나~!
              if (contains(lottoNumber)) {
                  matchCount += 1;
              }
          }
        
          return matchCount;
      }
        
      public boolean contains(final LottoNumber number) {
          return value.contains(number);
      }
    

응답값도 포장할 수 있다. 만약, 제한된 종류 + if분기마다 달라지는 응답값라면 ENUM응답 포장후 응답에 필요한 input들로 정펙매 를 호출한 뒤 내부로 분기 옮겨 응답한다. 이후 if분기를 처리해줄 < input->필드들 - 응답값>을 매핑한다.

  • int를 포장한 뒤 -> 관련된 if분기 로직을 포장한 도메인으로 넘기면, 메서드 if문이 사라진다.,

image-20220306183403967

제한된 종류의 응답 값포장은 Enum을 활용한다. 메서드 응답을 빨간줄로 바꿔 생성하자.

  • 만약, 제한되지 않았다면? class Rank로, 현재는 제한된 종류의 값 응답이므로 Enum으로 매핑해서 if를 대신하게 해보자.

    image-20220306183754213

  • 연속된 상수라면, Enum명_ 순서를 활용하면 된다.

    • 1,2,3,4,5등 -> 을 Enum으로 만들고 등_1,2,3,4,5; 로 선언해보자.
      package lotto;
        
      public enum Rank {
        
          RANK_1,
          RANK_2,
          RANK_3,
          RANK_4,
          RANK_5;
      }
        
    

응답에 대한 분기로직을 어떻게 응답class/응답Enum으로 넘길까?

image-20220306184557340

메서드 응답부에서 원하는 포장된 응답값인 Enum객체을 뽑아 응답해주는 return Enum.정펙매of (,)완성하기
  • 결과적으로 정펙매Enum.from() or Enum.of( , , )로 Enum객체를 응답해야한다.

    • 원하는 Enum객체가 응답될 수 있도록 필요한 인자들을 생각해서 받아들인다.
    • 실제로 입력값이 될 것들을 가지고 있는 사용부인 메서드 응답부에서 부터 시작
  1. 응답부에서 Enum.정팩메로 해당하는 포장응답값return해 줄 예정이다.

    • 필요한 input들이 뭐가 있는지 살펴본다.
    • 메서드호출의 결과값이 있다면, input이 명확하도록 따로 변수로 뽑아주자.

    image-20220306190059628 image-20220306190115387

    • 정팩메로는 Enum이라는 포장된 응답값을 가져올 것이기 때문에 return과 같이 작성한다. image-20220306190225323

    image-20220306190457221

값 응답 로직을 -> 포장응답값 정펙매로 잘라내서 처리하기
  1. 이제 값 응답로직을 그대로 잘라붙혀넣기 한다. image-20220306190736124

    • 이동후

    image-20220306190810106

정팩메 내부로 옮긴 분기에 대해, 응답[값]들을 [Enum객체]로 수정해주기 -> 없는 값(0, None)에 대해서도 OUT,NONE객체 만들어서 응답해주기

  • 아직 바꾸기 전 응답로직

      if (matchCount == 6) {
          return 1;
      }
      if (matchCount == 5 && matchBonus) {
          return 2;
      }
      if (matchCount == 5) {
          return 3;
      }
      if (matchCount == 4) {
          return 4;
      }
      if (matchCount == 3) {
          return 5;
      }
        
      return 0;
    
  • 수정 + NONE객체 추가후

      package lotto;
        
      public enum Rank {
        
          RANK_1,
          RANK_2,
          RANK_3,
          RANK_4,
          RANK_5,
          OUT;
        
          public static Rank of(final int matchCount,
                                final boolean matchBonus) {
        
              if (matchCount == 6) {
                  return RANK_1;
              }
              if (matchCount == 5 && matchBonus) {
                  return RANK_2;
              }
              if (matchCount == 5) {
                  return RANK_3;
              }
              if (matchCount == 4) {
                  return RANK_4;
              }
              if (matchCount == 3) {
                  return RANK_5;
              }
        
              return OUT;
          }
      }
        
    
      public static Rank match(final Lotto userLotto,
                               final Lotto winningLotto,
                               final LottoNumber bonusNumber) {
        
          final int matchCount = userLotto.match(winningLotto);
          final boolean matchBonus = userLotto.contains(bonusNumber);
        
          return Rank.of(matchCount, matchBonus);
      }
        
    

메서드 [포장된 응답] 테스트 작성하기

여러분기므로 1case에 대한 -> 1예상값 마다 테매를 작성해야한다.
@Test
void match_1() {
    //given, when
    final Rank rank = LottoService.match(
        new Lotto(1, 2, 3, 4, 5, 6),
        new Lotto(1, 2, 3, 4, 5, 6),
        new LottoNumber(7)
    );

    //then
    assertThat(rank).isEqualTo(Rank.RANK_1);
}

if분기를 enum 매핑으로 제거하기

정팩메 input으로 들어올 데이터들 中 1개를 골라 (매핑 예정인) Enum객체의 필드로 넣어준다.
  • 정펙매로 여러 데이터가 들어와 사용가 들어오지만 of( matchCount, matchBonus)

    image-20220306222111985

    • 매핑에 사용될 쉬운 변수 1개만 선택하여, 정펙매 내부 values().stream.filter 분기에서 비교할 것이다. Enum객체가 필드로 가지고 있으면 되며, 그 필드 기준으로 매핑값도 차후 배정된다.
  1. 쉽게 Enum의 필드에 넣기 위해서는 일단 ENUM(1) 객체 옆에 빨간줄로 원시형 예제작성해준다. image-20220306222323949

    image-20220306222358375

  2. 생성자를 생성해주면서 input과 똑같이 네이밍빨간줄 this.필드명 = 필드; 초기화를 이용해서 필드를 생성한다.

    image-20220306222528608

    image-20220306222623598

    image-20220306222631104

  3. 나머지 객체도 데이터를 넣어준다.

     public enum Rank {
        
         RANK_1(6),
         RANK_2(5),
         RANK_3(5),
         RANK_4(4),
         RANK_5(3),
         OUT(0);
        
         private final int matchCount;
        
         Rank(final int matchCount) {
             this.matchCount = matchCount;
         }
        
         public static Rank of(final int matchCount,
                               final boolean matchBonus) {
        
             if (matchCount == 6) {
                 return RANK_1;
             }
    

Enum정팩메 내 [제한된종류의 모든 if분기]를 values().stream + filter with input vs 매핑field로 분기 없애기

특수한 경우, 정팩매에서 ENUM매핑값 뽑기도 전에, early return해준다.
  • 특수한 경우로서 2등과 3등을 먼저 필드는 안됬지만 사용해야하는 변수 matchBonus홀로 true인 2등을 early return한다.

      public static Rank of(final int matchCount,
                            final boolean matchBonus) {
        
          if (matchBonus && matchCount == 5) {
              return RANK_2;
          }
    
Enum내에서 values()는 Enum객체배열을 응답하며 거기에 배열.stream자동완성 사용하여 EnumStream 만들어 객체전체를 돌리기

image-20220306223256714

image-20220306223352829

image-20220306223432081

제한된 종류의 if분기values().stream (for) + filter(it -> input vs 매핑field 비교) 대체하기
public static Rank of(final int matchCount,
                      final boolean matchBonus) {

    if (matchBonus && matchCount == 5) {
        return RANK_2;
    }

    Arrays.stream(values())
        .filter(it -> matchCount == it.matchCount)
        .findAny()
        .orElse(OUT);
  • 기존 if분기들 이제 삭제 image-20220306224423339
enum sense: 각 enum객체끝은 마지막도 콤마(,) 그 다음라인(;) + .findAny()후 .orElse( NONE객체 )
public enum Rank {

    RANK_1(6),
    RANK_2(5),
    RANK_3(5),
    RANK_4(4),
    RANK_5(3),
    OUT(0),
    ;
return Arrays.stream(values())
    .filter(it -> matchCount == it.matchCount)
    .findAny()
    .orElse(OUT);

Bi(비.아이.) Predicate< , >input된 2개 값을 다 사용하는 1개의 람다식필드 && filter속 예비 조건식(.test(a,b)로 실행)을 만들어 -> [enum의 1 필드 매핑]에 사용되는 Type을 함수형인터페이스로 사용하여 -> 정펙매 내부 들어오는 2개값 모두 사용하는 enum 만들기

  • BiPredicate Type은 1개 함수를 가진 인터페이스 = 함수형 인터페이스2개 인자를 받아 실행대기 중인 조건식을 제공해주고 -> .test( a, b)를 통해 외부인자를 받아서 조건식을 실행한다.
    • .test(a, b)에 정팩메로 들어온 input2개를 넣어주면 된다.
아직 BiPredicate 적용안한 기존 코드 (상금도 매핑 안된 상태)
public enum Rank {

    RANK_1(6),
    RANK_2(5),
    RANK_3(5),
    RANK_4(4),
    RANK_5(3),
    OUT(0),
    ;

    private final int matchCount;

    Rank(final int matchCount) {
        this.matchCount = matchCount;
    }

    public static Rank of(final int matchCount,
                          final boolean matchBonus) {

        if (matchBonus && matchCount == 5) {
            return RANK_2;
        }

        return Arrays.stream(values())
            .filter(it -> matchCount == it.matchCount)
            .findAny()
            .orElse(OUT);
    }

}
Enum필드에 2인자 람다식인 BiPredicate를 넣어서 생성하기
  1. 이번엔 Enum()객체안에 input 중 1개 6,5,4.. 대신에 input 2필드 다 써서 1개 람다식을 만들어 예제로 넣어주자.
    • 이왕이면 다중커서로 한번에 다 입력해주자. image-20220306231741730 image-20220306231807230 image-20220306231833577 image-20220306231947811
아쉽게도 BiPredicate는 직접 필드입력 -> private final 필드 변수 생성 -> 생산자 자동완성로 가야한다. (원래는 예제 -> 생산자 -> 빨간줄 this.필드 생성)
  • 원래 객체( 원시값 등 쉬운 Type)을 빨간줄 필드로 넣으면, 생성자에 파라미터 공짜 -> this.로 필드 공짜 생성
  1. BiPredicate 작성

    • 받은 인자 2개는 if분기에 쓰였던 input값 2개다. -> 원래 Enum의 매핑 쉬운 상수 응답값 6, 5, 4...와 비교해야한다.
     public enum Rank {
        
         RANK_1(((matchCount, matchBonus) -> matchCount == 6)),
         RANK_2((matchCount, matchBonus) -> matchCount == 5 && matchBonus),
         RANK_3((matchCount, matchBonus) -> matchCount == 5 && !matchBonus),
         RANK_4((matchCount, matchBonus) -> matchCount == 4),
         RANK_5((matchCount, matchBonus) -> matchCount == 3),
         OUT((matchCount, matchBonus) -> matchCount < 3),
         ;
    
  2. 필드 직접 입력 private final

    • 제네릭 타입은 들어올 인자들의 Type(정팩메로 들어올 Type)을 Boxing한 타입 + 네이밍은 condition으로 실행될 조건식임을 인지시킴
     private final BiPredicate<Integer, Boolean> condition;
    

    image-20220306233622030

  3. 생성자 자동완성 image-20220306233747924 image-20220306233758792

values().stream + filter분기에서 .test( , )로 정펙매 인자를 받아 조건식 실행
public static Rank of(final int matchCount,
                      final boolean matchBonus) {
    return Arrays.stream(values())
        .filter(it -> it.condition.test(matchCount, matchBonus))
        .findAny()
        .orElse(OUT);
}

응답값의 포장(리팩토링)이 다 끝났으면, 자체 도메인Test(Enum은 정펙매 테스트)

@Test
void rank_1() {
    final Rank rank = Rank.of(6, false);

    assertThat(rank).isEqualTo(Rank.RANK_1);
}

@Test
void rank_2() {
    final Rank rank = Rank.of(5, true);

    assertThat(rank).isEqualTo(Rank.RANK_2);
}

@ParameterizedTest
@CsvSource({"5,false,RANK_3", "4, false, RANK_4", "3, false, RANK_5"})
void rank_3_5(final int matchCount, final boolean matchBonus, final Rank expected) {
    final Rank rank = Rank.of(matchCount, matchBonus);

    assertThat(rank).isEqualTo(expected);
}

리팩토링(가변변수 0에 조건만족시 갯수 누적)

직전까지의합 0가변변수에 조건 만족시 add 1 -> stream + filter + count

  • 변경 전: 0이라는 int 가변변수를 먼저 선언해놓고 -> 돌면서 조건만족시 -> 가변변수에 += 1;하는 행위
// 일급 Lotto

public int match(final Lotto other) {
    int matchCount = 0;
    for (LottoNumber lottoNumber : other.value) {
        if (contains(lottoNumber)) {
            matchCount += 1;
        }
    }

    return matchCount;
}
  • stream의 filter + count로 원하는 조건의 갯수를 누적된 상태로 바로 뽑을 수 있다.
// 미리 가변 변수 int = 0;을 선언하지말고 조건만족시를 다 count()로 갯수만 반환받는다.
// - long이 기본 타입이라.. 캐스팅 해야한다.
public int match(final Lotto other) {
    // 우항, 파라미터를 찢는 놈이라고 했으니, other.value.stream()으로 찢었다.
    return (int) other.value.stream()
        .filter(it -> contains(it))
        .count();
}

stream없이 for+if 에서 if를 메서드분리 -> 누적변수없이도 응답 가능=갯수니까 return 1 or 0;

  • 변경 전

      public int match(final Lotto other) {
          int matchCount = 0;
          for (LottoNumber lottoNumber : other.value) {
              if (value.contains(lottoNumber)) {
                  matchCount += 1;
              }
          }
        
          return matchCount;
      }
    
for문 내부 if는 메서드 추출로 통채로 빼기 -> if가 바깥쪽 가변변수 조작한다면? return = 메서드추출()로 받아진다.

image-20220307003010553

  • 원래 if 내부에서 바깥의 가변변수를 건들였으니 if가 있던 자리 -> 바깥 가변변수 = 메서드추출()로 return받도록 짜여졌다.

    • 메서드추출했는데 변수에 리턴받는다? -> 내부에서 바깥의 가변변수를 건들였던 것을 뱉어준 것
      public int match(final Lotto other) {
          int matchCount = 0;
          for (LottoNumber lottoNumber : other.value) {
              matchCount = getMatchCount(matchCount, lottoNumber);
          }
        
          return matchCount;
      }
        
      private int getMatchCount(int matchCount, final LottoNumber lottoNumber) {
          if (value.contains(lottoNumber)) {
              matchCount += 1;
          }
          return matchCount;
      }
    
메서드추출로 튀어나온 (내부에서 사용되던 바깥선언의) 가변변수는 무조건 가변 = 메서드()형태다
  • 원래 if문 내부에서 가변변수++, 가변변수+=1를 해줬었는데… 추출된 것은 무조건 변수 = 메서드()형태인 이유는

    • 추출된 메서드의 파라미터로 가변변수가 들어간다.

        for (LottoNumber lottoNumber : other.value) {
            matchCount = getMatchCount(matchCount, lottoNumber);
        }
      
    • 가변변수가 파라미터에서 선언되는 이름만 똑같은 지역변수가 되는데, 이름은 같아도 바깥 가변변수와는 전혀 다른 것이다.

      image-20220307003630353

갯수 증가가변++/가변+=1 은 메서드추출시 파라미터(=지역변수)로 안가도 return 1 or 0만 해주면 되므로 [추출메서드에서 파라미터 삭제] -> 이후 가변 = 이 아닌 가변 += 으로 바꿔주면 된다.
  • 파라미터=지역변수를 없애고, 지역변수++자리를 return 1로 아니면 return 0;으로 대체하자.

      private int getMatchCount(int matchCount, final LottoNumber lottoNumber) {
          if (value.contains(lottoNumber)) {
              matchCount++;
          }
          return matchCount;
      }
    
      // 지역변수 지우고, 걸리면 갯수증가용 1 , 안걸리면 0 을 반환
      private int getMatchCount(final LottoNumber lottoNumber) {
          if (value.contains(lottoNumber)) {
              //matchCount++;
              return 1;
          }
          return 0;
      }
    
  • 메서드 추출 기능 자체의 문제가 가변+= 메서드리턴()이 안되서 -> 가변을 파라미터로 받은 뒤, 지역변수++, 지역변수+= 후 return되는 문제였다.

    • 가변 = 메서드 추출()부분을 가변 += 메서드 추출()-1or0리턴;으로 바꿔주자.
      public int match(final Lotto other) {
          int matchCount = 0;
          for (LottoNumber lottoNumber : other.value) {
              //matchCount = getMatchCount(matchCount, lottoNumber);
              //matchCount = getMatchCount(lottoNumber);
              matchCount += getMatchCount(lottoNumber);
          }
        
          return matchCount;
      }