Readable Code: 읽기 좋은 코드를 작성하는 사고법 - Section 4 : 객체 지향 적용하기

모깅·2024년 8월 31일
post-thumbnail

상속과 조합

조합은 하나의 객체가 자신이 필요한 객체를 가져다가 필드에 선언해서 사용하는 형태를 의미한다.

결합도란 한쪽에서 수정을 가했을 때 다른 쪽에서 수정이 많이 일어나고 있다면 결합도가 높은것이다.


  • 유연한 설계를 위해 조합으로 바꾸어 보자.

  • Cell에 있는 속성을 CellState로 옮기자.

  • private으로 바꾸자.
  • 필드와 관련된 메서드들을 Cell로부터 가져오자.


  • EmptyCell이 생성되는 순간 CellState도 생성이 된다.

  • 부모에 있던 필드들을 이용했던 getSing() 메서드를 CellState에 요청하는 방법으로 바꿔보자.

  • CellState에 isFlagged() 생성

  • Cell을 복사하여 Cell2 인터페이스 생성하자.




  • 모두 Cell2 인터페이스로 바꾼 후 Cell 클래스를 지우자.
  • 부모에 있던 isFlag, isOpened와 관련된 메서드들을 조합의 형태로 풀어냈기 때문에 좀 더 유연해졌다.
    -> 기존에는 부모가 변경되면 자식들에게 영향을 미쳤지만 CellState로 분리함으로써 결합도가 낮아졌다.

Value Object

value라는 변수가 final로 선언되어 있다.
-> 불변성 보장

생성자에서 유효성 검사를 하고 있다.

== 은 동일성 검사이고 equals는 동등성 검사이다.

우리 도메인에서 Money는 단위 객체이다.
-> int, long 같은 기본 타입들처러 Money라는 것을 우리 도메인에서 기본 타입으로 쓸거라고 받아드려도 된다.
-> 불변성, 동등성, 유효성 검사를 했다면!

엔티티는 식별자만 같으면 동등하다고 보며 그 외에 값이 다르다면 시간이 지남에 따라 변화했구나 라고 이해하면 된다.


  • rowIndex와 colIndex는 함께 있을때 의미가 있다.
    -> 이를 묶어서 value Object로 리팩토링 해보자.

  • valueObj는 동등성, 유효성, 불변성을 보장 받아야 한다.
    -> 불변성 : final 키워드를 통해 해결
    -> 유효성 : index 기반이기 때문에 0 초과
    -> 동등성 : equals와 hashcode 오버라이드 해서 해결

  • getCellInputFromUser에서 inputHandler가 String을 반환하는게 아니라 CellPosition을 만들어서 반환하는건 어떨까?

  • 이전에 변환하는 과정을 모두 boardIndexConverter로 이관했었다.
    -> inputHandler로 옮기자.

  • InputHandler 인터페이스에 스팩 추가하자.


  • Board라는 것은 Minesweeper의 것이다.
    -> 사용자 입력을 받는 곳에서 Board의 정보를 알 필요가 있을까?
    -> 유효성 검증 로직을 분리하자.

  • CellPostion을 받아서 Gameboard를 가지고 있는 Minesweeper에서 검증을 하자.
    -> 인덱스의 기능(0이상)은 이미 valueObj, inputHandler에서 검증을 했다.
    -> row, col만 넘는지 유효성 검사를 하면 된다.

  • getter를 써서 비교했을 때 자연스러운면 사용해도 되겠지만 일반적으로 무례한 행동이다.
    -> CellPosition에게 물어보자.

  • 메서드 입장에서는 rowSize인지 모른다.
    -> 파라미터를 rowIndex로 바꿔주자.
    -> Col도 마찬가지

  • CellPosition 기반으로 리팩토링 해보자.

  • CellPosition 하나밖에 없으니까 전차사 At을 사용해보자.


  • row < 0, col < 0은 이미 유효성 검사를 하기 때문에 제거해도 된다.

  • CellPosition을 넣어주려고 하는데 유효성 검사가 마음에 걸린다.
    -> CellPosition의 row가 0이었다면 계산한 -1 값이 메서드로 들어가기 때문에 에러가 발생할 것이다.
    -> 이를 해결하기 위해 상대좌표를 의미하는 RelativePosition이라는 VO를 만들어주자.

  • 동등성, 유효성, 불변성을 만족시키자.


  • 물어보자.

  • 메서드 안에 있으면 계속 생성하니까 상수로 만들자.
    -> 외부에서 참조 가능해야 하니 public으로 변경

  • 인라인으로 합친것

  • 두 메서드간 연관관계가 없다.
    -> 외부에서 calcualatePostionBy를 그냥 사용하면 에러가 날 것이다.


  • 스트림으로 바꿔보자.

  • 위 검증로직을 스트림에서 해도 상관 없을 것이다.
  • 검증로직에서는 board size가 넘는지를 검사했지만 스트림에서는 board size를 넘지 않는 것들에 대한 것만 filter처리를 할 것이다.

  • filter 했기 때문에 이부분 제거


  • 재귀랑 일치하는 부분 메서드 추출하자.

  • List로 반환하여 재사용성을 높이자.

  • 재귀로직 가독성을 위해 변수로 추출해서 사용

일급 컬렉션


  • GameBoard에서 스트림을 돌리는 로직이 너무 부담스럽다.
    -> 일급 컬렉션을 이용하자.

  • board로 부터 만들니 from으로 만들어보자.
    -> 스트림을 일급 컬렉션 내부로 옮길 것이다.


  • initializeGame()에 지뢰가 중복 생성되는 버그가 있었다.
    -> 이를 CellPosition에 대한 일급 컬렉션을 만들어 해결해보자.

  • 이중 for문을 돈다는 이야기는 전체 셀 포지션 위치를 다 돌고 싶다는 의미이다.
    -> 어떤 보드가 있을 때 그 보드에 대한 모든 셀 포지션을 먼저 만들고 그 다음에 뭔가 작업을 해주면 어떨까?

  • 외부에서 필드에 있는 컬렉션을 참조 할 수 없도록 새로운 컬렉션을 만들어서 반환해야한다.
    -> 일급 컬렉션 주의점!

  • landMineCount만큼 CellPosition을 뽑으면 된다.


  • 메서드 비슷하니 추출해보자.


  • 중복 제거를 위해 메서드 추출하자.


  • 지뢰가 아닌 셀들을 대상으로 카운트를 센 다음에 카운트가 0이 아니면 업데이트를 하고 있다.

  • cellPosition에게 어떤 positions를 줄테니 그거 빼서 리스트 달라고 요청하자.

  • 헷갈리니 람다로 생각해보자.



  • 버그가 생겼다.

  • 같은 인스턴스를 넣어줬기 때문에 생긴 오류이다.
    -> 성급한 메서드 추출의 잘못된 리팩토링

  • 인라인을 통해 해결

Enum의 특성과 활용

Enum은 코드이기 때문에 변경하려면 배포를 해야한다.
따라서 Enum의 변경이 잦다면 DB에 저장하는 것이 더 좋을 수 있다.


  • inputHandler가 User로부터 1, 2를 받아서 String을 반환하는게 아니라 Enum을 반환하는게 어떨까?


  • Cell을 어떻게 그릴지는 InputHandler에서 결정하는게 자연스럽다.
    -> getSign()에서 Cell이 판단하여 어떻게 그릴지 알려주는 게 이상하다.
  • 따라서 inputHandler에게 Cell에 대한 정보를 넘겨줘야 한다.
    -> SnapshotStatus을 만들어서 넘겨주자.
    -> 어떤 상태인지 알려주는 Enum이다.

  • snapshotStatus을 리턴하려고 했더니 특이하게 NumberCell은 자신의 숫자도 표시해야 한다.
    -> nearbyLandMineCount와 snapshotStatus를 포함하는 CellSnapshot이라는 클래스를 하나 만들자.

  • Cell이 CellSnapshotStatus의 정보를 알아야 할까?
    -> 정적 팩토리 메서드로 만들어주자.


  • outputHandler에 가서 스냅샷 정보를 받아서 그려주자.
    -> CellPosition에 알맞은 스냅샷좀 줄래?

  • getSign() 사용 안하니 삭제하자.
  • Sign 상수들의 사용이 모두 outputHandler로 갔으니까 옮겨주자.

  • 정리 : 셀을 보니까 셀에 어떻게 그릴지에 대한 책임이 셀에 같이 묶여있었으며 SRP를 위반하고 있는 것 같아서 그리는 책임은 무조건 OutputHandler로 가져가자.
    -> 그래서 분리하려고 봤더니 셀이 지금 어떤 상태인지에 대한 정보가 필요해서 스냅샷이라는 개념을 도입했다.
    -> 스냅샷 상태에 따라 분기로 나눠 어떤 것을 그릴 지 반환하도록 하였다.

  • SnapshotStatus와 nearbyLandMineCount가 모두 같으면 같은 스냅샷으로 봐야할 것이다.
    -> ValueObj 이다.
    -> equals, hash 오버라이드 하자.

다형성 활용하기


  • CellSignProvidable 인터페이스를 만들자.
    -> 스냅샷을 넘겨줬을 때 그에 맞는 사인을 리턴해주는 역할을 한다.

  • 필드로 가지고 있던 사인들 제거
  • 아직 if 문을 제거하지 못했다.
    -> CellSignProvidable에 스팩을 추가해보자.

  • 반복문 안에 있어서 List를 계속 만들것이다.
    -> 리스트를 포함하고 있는 클래스를 하나 만들자.

  • CellSignFinder도 계속 생성할 필요 없이 한번만 생성하면 된다.
    -> 필드로 가지고 있자.

  • 필드로 뺏으니 인라인으로 그냥 합치자.

  • CellSign 클래스가 더 생성되면 클래스를 추가해서 CellSignProvidable을 구현해야하고 CellSignFrinder에 구현체를 넣어줘야 한다.
    -> 사람이 실수 할 만하다.
    -> Enum으로 이 문제 해결


클래스를 추가할 때 리스트에 넣어줘야 하며 인터페이스도 구현해야하는 상황이다.
-> 하나라도 놓치면 에러가 발생하는 상황!

Enum을 사용하면 추가와 구현을 같이 해버릴 수 있기 때문에 이 방법으로 리팩토링을 하였다.

숨겨져 있는 도메인 개념 도출하기


  • 마인 스위퍼가 점점 발전한다면 게임 레벨, 인풋 핸들러, 아웃풋 핸들러 말고도 뭐가 더 추가 될 수 있다.
    -> 모든 설정 정보들을 묶어서 한 번에 넣어주는 객체를 하나 만들자.

profile
멈추지 않기

0개의 댓글