독서

책01. 파이브라인즈 오브 코드 04. 타입 코드 처리하기

제비랑 2024. 2. 7. 19:44

파이브 라인스 오브 코드 - 크리스찬 클라우젠

4. 타입 코드 처리하기

4.1. 간단한 if 문 리팩터링

4.1.1. if문에서 else 문 사용하지말기

  • if문은 검사(check)이고, if-else문은 의사결정(decision)으로 간주합니다. (캬.. else문을 쓰면 빠르게 "결정"되니까, 하드코딩느낌이고, 코드의 유연성이 떨어진다)
  • if-else같은 동작은 컴파일때 빠르게 결정되는 이른바인딩 스멜. 별루다. if문을 수정해야 변경할 수 있기 때문에 전에 배운 추가에 의한 변경을 방해한다. 반면 늦은 바인딩은 추가에 의한 변경을 가능케 한다.
  • 여기서 또 멋있는 말이 등장하는데, if는 조건연산자로 흐름을 제어하지만, 객체지향 프로그래밍에서는 객체라는 훨씬 더 강력한 제어 흐름 연산자가 있다!. 즉 if를 쓰지말고 객체를 쓰자는 말인데, 이게 무슨 말인고 하니...
    인터페이스를 사용하는 두가지 다른 구현(클래스라던지)이 있는 경우, 인스턴스화하는 클래스에 따라 실행할 코드를 결정할 수 있따는 말.
  • 늦은 바인딩은 클래스로 타입코드대체(4.1.3)이나 전략패턴도입(5.4.2)이라는 리팩터링 패턴을 볼때 볼 수 있다.

4.1.2. if문의 열거형을 인터페이스로 바꾸기(살짝 이해안되는데? 왜바꾸지. if문도 그대로 있네)

  • 열거형이 있다?(if문 조건들이겠찌?) 그럼 일단 인터페이스로 바꿔!. 그담에 구현체로 열거형 애들 하나씩 있다고 치고, 구현체에 따라 다르게 실행되게 하라는 뜻.
  • enum Input { UP, DOWN, LEFT, RIGHT }
  • 이런 열거형 친구들 하나하나는 결국 if의 조건이라고 할수 있는데, 얘네를 인터페이스화 시켜보자.
    아무튼 이런식으로 바꿔서, 열거형이 들어가는 모든곳에 input.isRight() 식으로 집어넣던지, new Right() 이런식으로 세팅하던지가 가능하다.
  • 코드가 너무 길어지는데...?
  • interface input2{ isRight() : boolean; isLeft() : boolean; isUp() : boolean; isDown() : boolean; } class left implements Input2{ isRight(){ return false;} isLeft(){ return true;} isUp(){ return false;} isDown(){ return false;} } class right implements Input2{ isRight(){ return true;} isLeft(){ return false;} isUp(){ return false;} isDown(){ return false;} } class up implements Input2{ isRight(){ return true;} isLeft(){ return false;} isUp(){ return true;} isDown(){ return false;} } class down implements Input2{ isRight(){ return true;} isLeft(){ return false;} isUp(){ return false;} isDown(){ return true;} }
  • 혹시 코드가 너무 길어진다고 생각했다면? 정상이라고 한다.. 5장에서 저 is메서드들을 잔뜩 혼내줄 예정이라고 한다. (혹시 그 유명한 전략 패턴의 등장?!)

4.1.3을 보면서 이 과정을 한번더 곱씹자.

4.1.3. 클래스로 타입 코드 대체(이것도 어렵네)

  • 열거형에 값을 추가할때는 수많은 파일에 거쳐서 해당 열거형과 연결된 로직들을 확인해야 한다.
  • 헌데 인터페이스를! 구현한 새로운 클래스를 추가하는 것은 해당클래스에 메서드 구현이 필요할 뿐이다.
  • 타입 코드를 볼 때는 바로 열거형으로 바꾼뒤, 인터페이스로 바꾸는 리팩터링을 생각하자.

4.1.4. 클래스로 코드 이관하기

  • 이제 마법이 일어납니다. function이 붙어있떤 함수에서, 메소드가 되어버립니다.
  • 순서를 기재하자면.
    1. 함수를 메서드로 만든다.
    2. 메서드 선언을 인터페이스에 넣는다. 기존 메서드와 약간 다른 이름을 짓고, 매개변수는 의미가없어질경우 제거한다.
    3. 인터페이스를 구현한 구현체에서 알맞게 메서드를 변경한다.
    4. 메서드가 된 함수원형에, 메서드를 호출하도록 코드를 변경한다.
    • 필자가 가장 좋아하는 리팩터링 패턴이라고 한다. 깐깐한 내가 봐도 이건 깔끔하다는 생각이 든다.4.1.5. 리팩터링패턴 : 클래스로 코드 이관.
    • 위의 방식의 연장이다.4.1.6. 불필요한 메서드 인라인화.( <-> 메서드 추출과 정확히 반대)
    • 리팩터링을하다보면, 가독성을 해치고 공간만 차지하는 메서드는 과감하게 인라인화 하자. 흔히 한줄짜리 메서드를 인라인화한다. 물론, 한줄 초과의 메서드도 가능하지만, 인라인화하기에 복잡하다던지, 동일한 추상화수준인지 생각해보자.4.1.7. 리팩터링패턴 : 메서드의 인라인화
    • 다음 코드는 절댓값을 구하는 한줄짜리 메서드인데, 지금도 이해를 못했다. 이런건 인라인화 하지말자
    • const NUMBER_BITS = 32; function absolute(x: number){ reutrn( x ^ x >> NUMBER_BITS-1) - (x >> NUMBER_BITS-1); }
    • 또, 입/출금시 동시에 값을변경해야된다면, 값의 update를 하나의 메소드로 만들지말고, 입출금을 하나의 메서드로 인라인화해서 만드는게 좋겠지?

4.2. 긴 if문의 리팩터링

- 리마인드 : if문은 함수의 시작에만 배치, else if 쓰지말기, 열거형 인터페이스/클래스로 바꾸기,

와근데 여기서

  enum Tile{
  AIR,
  FLUX,
  UNBREAKABLE,
  PLAYER,
  STONE, FALLING_STONE,
  BOX, FALLING_BOX,
  KEY1, LOCK1,
  KEY2, LOCK2
}

이거 하나를 리팩터링을 위해가지고 엄청 쓰려니까 좀 힘들다...☆

4.2.1. 일반성 제거(말이 뭐이리 어려워 ㅋㅋ) -> 메서드 전문화

- 실제로 하는일에비해, 메서드 기능 정의는 범용적으로 되어있을때 일반적인 메서드라 함. 예를들어 특정 구현체에 대해서만 제거하는

remove(Tile) 함수가 있다고 생각해보자. 매개변수로 Tile 인터페이스 구현체들이 올 수 있지만, 실제로는 특정 구현체Lock만 오도록 쓰인다고 생각해보자.
이럴때 메서드 전문화를 한다.

4.2.2. 리팩터링 패턴 : 메서드의 전문화

function remove(tile: Tile){
      for(let y = 0; y < map.length; y++){
      for(let x = 0; x< map[y].length; x++){
        if(map[y][x] === tile){
          map[y][x] = new Air();
        }
      }
    }
}

위 코드를 보자. 어차피 실제로 쓸때는 Tile에 Lock1,Lock2 구현(클래스)만 들어간다. 그래서 이걸 메서드 전문화를 통해 다음과 같이 바꿀 수 있다.

function removeLock1() {
  for (let y = 0; y < map.length; y++) {
    for (let x = 0; x < map[y].length; x++) {
      if (map[y][x].isLock1()) {
        map[y][x] = new Air();
      }
    }
  }
}
////
function removeLock2() {
  for (let y = 0; y < map.length; y++) {
    for (let x = 0; x < map[y].length; x++) {
      if (map[y][x].isLock2()) {
        map[y][x] = new Air();
      }
    }
  }
}

사실 메서드 하나를 메서드 2개로까지 바꿔가면서 전문화를 해야하나.. 싶긴하다.
암튼 너무 일반화하면 책임이 흐려지고 다양한 위치에서 코드를 호출할 수 있어 문제가 될 수 있다고한다.
메서드 전문화 과정을 순서대로 나열하고 넘어가자.

    1. 전문화하려는 메서드를 복제
    1. 메서드중 하나의 이름을 새로 사용할 메서드의 이름으로 변경, 전문화하려는 매개변수를 제거(또는 교체)합니다.
    1. 매개변수 제거에 따라 메서드를 수정해서 오류가 없도록 합니다.
    1. 이전의 호출을 새로운 것을 사용하도록 변경합니다.

4.2.3. 스위치가 허용되는 유일한 경우는??(궁금하네)

인덱스(배열)은 객체보다 직렬화하기 쉽다. 즉 논리적일수 있다. 전체 map을 변경 하느 대신 열거형 인덱스에서 새로운 클래스를 사용하도록 새로운 함수를 만드는것이 좋다. -> 이때 스위치가 허용된다는건가..?
deafault케이스가 없고, 모든 case에 반환 값이 있는 경우가 아니라면 switch를 사용하지말자.

4.2.4. 스위치를 사용하지말것(이랬다 저랬다좀 하지말지.)

4.2.5. if를 제거해보자.


4.3. 코드 중복 처리

4.3.1. 인터페이스말고 추상클래스는 왜 잘 안쓸까?

4.3.2. 인터페이스에서만 상속받을것.

4.3.3. 클래스에 있는 코드의 중복은 다 무엇일까

분기조장

4.4. 복잡한 if 체인 구문 리팩터링


4.5. 필요없는 코드 제거하기(너무 뻔한데?)

4.5.1. 리팩터링 패턴: 삭제 후 컴파일하기.


4장 요약

  • 타입 코드 처리하기라고 쓰고 if문 혼내주기
    • else 사용말자(분기가빨리결정되잖아)
    • switch 사용말자(왜)
  • 지나친 메서드 일반화 no. 메서드 전문화
  • 인터페이스로만 상속받기(불필요한 긴밀한 커플링 방지)
  • 리팩터링 후 리팩터링 : 메서드 인라인화 + 삭제후 컴파일하기.

P.S. 메소드 어디에쓰이는지 보려고 일부러 이름 슥 바꿔보는거 되게 꿀팁인듯.