[리팩터링] 5장 유사한 코드 융합하기

공효은·2023년 5월 8일
0

refactoring

목록 보기
5/12
post-thumbnail

이번 장에서 다룰 내용

  • 유사 클래스 통합하기
  • 조건부 산술로 구조 노출하기
  • 간단한 UML 클래스 다이어그램 이해하기
  • 전략 패턴의 도입으로 유사 코드 통합하기
  • 구현체가 하나뿐인 인터페이스를 만들지 말 것으로 어지러움 정리하기

5.1 유사한 클래스 통합하기

이전의 경우와 마찬가지로 그대로 보존하면서 강조하고 싶은 관계를 나타내는 괄호로 묶은 표현식(map[y][x].isStone() || map[y][x].isFallingStone())) 이 있다. 따라서 첫 번째 단계는 괄호로 묶인 두 개의 || 에 대해 각기 하나의 함수를 도입하는것이다.
stony와 boxy를 각기 '돌의 성질을 가진다'와 '박스의 성질을 가진다' 로 이해하자.


// 변경전

updateTile(x:number, y:number){
  if ((map[y][x].isStone() || map[y][x].isFallingStone())
        && map[y + 1][x].isAair()) {
        map[y + 1][x] = new FallingStone();
        map[y][x] = new Air();
      } else if ((map[y][x].isBox()|| map[y][x].isFallingBox())
        && map[y + 1][x].isAir()) {
        map[y + 1][x] = new FallingBox()
        map[y][x] = new Air();
      } else if (map[y][x].isFallingStone()) {
        map[y][x] = new Stone();
      } else if (map[y][x].isFallingBox()) {
        map[y][x] = new Box();
      }
}

// 변경 후
updateTile(x:number, y:number){
  if ((map[y][x].isStony())
        && map[y + 1][x].isAair()) {
        map[y + 1][x] = new FallingStone();
        map[y][x] = new Air();
      } else if (map[y][x].isBoxy())
        && map[y + 1][x].isAir()) {
        map[y + 1][x] = new FallingBox()
        map[y][x] = new Air();
      } else if (map[y][x].isFallingStone()) {
        map[y][x] = new Stone();
      } else if (map[y][x].isFallingBox()) {
        map[y][x] = new Box();
      }
}

interface Tile {
  isStony(): boolean;
  isBoxy(): boolean;
}

class Air implements Tile {
  //...
  isStony() {return false;}
  isBoxy() {return false;}
}

Stone과 FallingStone의 유일한 차이점은 isFallingStone 및 moveHorizontal 메서드의 결과 뿐이다.

메서드가 상수를 반환 할 때 상수 메서드 라고 부른다.
경우에 따라 다른 값을 반환하는 상수 메서드를 공유하기 때문에 이 두 클래스는 합칠 수 있다.

클래스를 결합하는 첫 번째 단계는 상수 메서드를 제외한 클래스의 모든 것을 동일하게 만드는 것이다. 두번 째 단 계는 실제로 합치는 것이다.

  1. 첫 번째 단계에서는 두 개의 moveHorizontals를 동일하게 만든다.
    a. 각 moveHorixontal 본문의 기존 코드 주위에 if(true){}를 추가한다.
    b. true를 각기 isFallingStone() === true 와 isFallingStone() === false로 바꾼다.
//변경전
class Stone implements Tile {
  //...
  moveHorizontal(dx:number){
    if(true){
      if(map[playery][playerx+dx+dx].isAir() && !map[playery+1][playerx+dx].isAir()){
        map[playery][playerx+dx + dx] = this;
        moveToTile(playerx+dx, playery);
      }
    }
  }
}
class FaillingStone implements Tile {
  //...
  moveHorizontal(dx:number){
    if(true) { }
  }
}

// 변경 후 
class Stone implements Tile {
  //...
  moveHorizontal(dx:number){
    if(this.isFallingStone() === false){
      if(map[playery][playerx+dx+dx].isAir() && !map[playery+1][playerx+dx].isAir()){
        map[playery][playerx+dx + dx] = this;
        moveToTile(playerx+dx, playery);
      }
    }
  }
}
class FaillingStone implements Tile {
  //...
  moveHorizontal(dx:number){
    if(this.isFallingStone() === true) { }
  }
}

c 각 moveHorizontal 본문을 복사하여 else와 함께 다른 moveHorizontal에 붙여 넣는다.

// 변경 전
class Stone implements Tile {
  //...
  moveHorizontal(dx:number){
    if(this.isFallingStone() === false){
      if(map[playery][playerx+dx+dx].isAir() && !map[playery+1][playerx+dx].isAir()){
        map[playery][playerx+dx + dx] = this;
        moveToTile(playerx+dx, playery);
      }
    }
  }
}
class FaillingStone implements Tile {
  //...
  moveHorizontal(dx:number){
    if(this.isFallingStone() === true) { }
  }
}

// 변경 후
class Stone implements Tile {
  //...
  moveHorizontal(dx:number){
    if(this.isFallingStone() === false){
      if(map[playery][playerx+dx+dx].isAir() && !map[playery+1][playerx+dx].isAir()){
        map[playery][playerx+dx + dx] = this;
        moveToTile(playerx+dx, playery);
      }
    }
    else if(this.isFllingStone() === true){
    }
  }
}
class FaillingStone implements Tile {
  //...
  moveHorizontal(dx:number){
    if(this.isFallingStone() === false){
      if(map[playery][playerx+dx+dx].isAir() && !map[playery+1][playerx+dx].isAir()){
        map[playery][playerx+dx + dx] = this;
        moveToTile(playerx+dx, playery);
      }
    }
    else if(this.isFallingStone() === true) { 
    }
  }
}
  1. 상수 메서드 isFallingStone만 다르므로 두 번째 단계는 falling 필드를 새로 도입하고 생성자에서 그 값을 할당하는것으로 시작한다.
//변경 전
class Stone implements Tile {
  //...
  isFallingStone() {return false;}
}
class FallingStone implements Tile {
  //...
  isFallingStone() {return true;}
}

// 변경 후
class Stone implements Tile {
  private falling:boolean;
  constructor(){
    this.falling = false;
  }
  //...
  isFallingStone() {return false;}
}

class FallingStone implements Tile {
  private falling:boolean;
  constructor(){
    this.falling = true;
  }
  //...
  isFallingStone() {return true;}
}
  1. isFallingStone을 변경해서 새로운 falling 필드를 반환한다.
//변경 전
class Stone implements Tile {
  //...
  isFallingStone() {return false;}
}
class FallingStone implements Tile {
  //...
  isFallingStone() {return true;}
}

//변경 후
class Stone implements Tile {
  //...
  isFallingStone() {return this.failling;}
}
class FallingStone implements Tile {
  //...
  isFallingStone() {return this.failling;}
}
  1. 문제가 없는지 확인하기 위해 컴파일한다.
  2. 각 클래스에 대해
    a. falling의 기본값을 매개변수로 받도록 만든다.
//변경 전
class Stone implements Tile {
  private failling:boolean;
  constructor(){
    this.failling = false;
  }
//...
}

//변경 후
class Stone implements Tile {
  private failling:boolean;
  constructor(failling:boolean){
    this.failling = failling;
  }

b. 컴파일러 오류를 살펴보고 기본값을 인자로 전달한다.

// 변경 전
new Stone();


//변경 후 
new Stone(false)
  1. 통합 중인 클래스 중 하나를 제외한 모든 클래스를 삭제한 후. 컴파일 오류를 남아있는 클래스로 바꿔서 수정
// 변경 전
new FallingStone(true);

// 변경 후 
new stone(true);

이 통합의 전체 코드는 다음과 같다.

// 변경 전
updateTile(x:number, y:number){
  if ((map[y][x].isStony())
        && map[y + 1][x].isAair()) {
        map[y + 1][x] = new FallingStone();
        map[y][x] = new Air();
      } else if (map[y][x].isBoxy())
        && map[y + 1][x].isAir()) {
        map[y + 1][x] = new FallingBox()
        map[y][x] = new Air();
      } else if (map[y][x].isFallingStone()) {
        map[y][x] = new Stone();
      } else if (map[y][x].isFallingBox()) {
        map[y][x] = new Box();
      }
}

class Stone implements Tile {
  //...
  isFallingStone() {return false;}
  moveHorizontal(dx:number){
    if(this.isFallingStone() === false){
      if(map[playery][playerx+dx+dx].isAir() && !map[playery+1][playerx+dx].isAir()){
        map[playery][playerx+dx + dx] = this;
        moveToTile(playerx+dx, playery);
      }
    }
  }
}

class FallingStone implements Tile {
  //...
  isFallingStone() {return true;}
  moveHorizonetal(dx:number){}
}

// 변경 후
updateTile(x:number, y:number){
  if ((map[y][x].isStony())
        && map[y + 1][x].isAair()) {
        map[y + 1][x] = **new Stone(true);**
        map[y][x] = new Air();
      } else if (map[y][x].isBoxy())
        && map[y + 1][x].isAir()) {
        map[y + 1][x] = new FallingBox()
        map[y][x] = new Air();
      } else if (map[y][x].isFallingStone()) {
        map[y][x] = **new Stone(false);**
      } else if (map[y][x].isFallingBox()) {
        map[y][x] = new Box();
      }
}

class Stone implements Tile {
  //...
  constructor(private falling:boolean){}
  //...
  isFallingStone() {return return this.falling;}
  moveHorizontal(dx:number){
    if(this.isFallingStone() === false){
      if(map[playery][playerx+dx+dx].isAir() && !map[playery+1][playerx+dx].isAir()){
        map[playery][playerx+dx + dx] = this;
        moveToTile(playerx+dx, playery);
      }
    }
    else if(this.isFallingStone() === true){
    }
  }
}

moveHorizontal는 if-else 구분이 포함되어있다. falling 타입을 열거형으로 만들어 노출 시킬 수 있다.

//변경 전

// ...
new Stone(true);

// ...
new Stone(false);

// ...
class Stone implements Tile {
  constructor(private falling:boolean)
  {}
  // ...
  isFallingStone() {
    return this.falling;
  }
}

// 변경 후
enum FallingState {
  FALLING, RESTING
}
// ...
new Stone(FallingState.FALLING);
// ...
new Stone(FallingState.RESTING);
// ...
class Stone implements Tile {
  constructor(private falling: FallingState)
  {}
  // ...
  isFallingStone() {
    return this.falling === FallingState.FALLING;
  }
}

우리는 열거형을 다루는 방법을 알고있다. 바로 클래스로 타입 코드 대체 리팩터링!

// 변경 전 
// ...
new Stone(true);

// ...
new Stone(false);

// ...
class Stone implements Tile {
  constructor(private falling:boolean)
  {}
  // ...
  isFallingStone() {
    return this.falling;
  }
}

// 변경 후

interface FallingState {
  isFalling():boolean;
  isResting():boolean;
}

class Falling implements FallingState{
  isFalling() {return true};
  isResting() {return false};
}

class Resting implements FallingState{
  isFalling() {return false};
  isResting() {return true};
}

new Stone(new Falling());

// ...
new Stone(new Resting());

// ...
class Stone implements Tile {
  constructor(private falling:FallingState)
  {}
  // ...
  isFallingStone() {
    return this.falling.isFalling();
  }
}

isFallingStone을 moveHorizontal 메서드에 인라인화 하려면?

// 변경 전

interface FallingState {
  //...
}
class Falling implements FallingState {
  //...
}

class Resting implements FallingState {
}

class Stone implements Tile {
  //...
  moveHorizontal(dx:number){
    if(!this.falling.isFalling()){
      if(map[playery][playerx+dx+dx].isAir() && !map[playery+1][playerx+dx].isAir()){
        map[playery][playerx+dx + dx] = this;
        moveToTile(playerx+dx, playery);
      }
    }
    else if(this.falling.isFalling()){
    }
  }
}

// 변경 후
interface FallingState {
  //...
  moveHorizontal(tile: Tile, dx:number):void;
}
class Falling implements FallingState {
  //...
  moveHorizontal(tile: Tile, dx:number){}
}

class Resting implements FallingState {
  //...
  moveHorizontal(tile: Tile, dx:number){
      if(map[playery][playerx+dx+dx].isAir() && !map[playery+1][playerx+dx].isAir()) {
        map[playery][playerx+dx + dx] = this;
        moveToTile(playerx+dx, playery);
      }
}

class Stone implements Tile {
  //...
  moveHorizontal(dx:number){
   this.falling.moveHorizontal(this, dx);
}

💁‍♀️ 정리
유사한 클래스 통합할때!
1. 다른 메소드가 있는 지 찾는다.
2. 상수 메소드가 아닌 다른것 부터 합친다. if-else 사용
3. 상수 메소드는 필드로 만들고 생성자에서 ㅇ대입해준다.
4. 상수 메소드를 enum => class로 변경한다.
5. 2에서 합쳐진 메소드를 4에서 생성한 클래스에 인라인한다.
5. 코드 깔끔!

5.1.1 리팩터링 패턴: 유사 클래스 통합

설명

  • 일련의 상수 메서드를 공통으로 가진 두 개 이상의 클래스에서 일련의 상수 메서드가 클래스에 따라 다른 값을 반환할 때마다 이 리팩터링 패턴을 사용해서 클래스를 통합 할 수 있다.
  • 클래스를 통합하는 것은 좋은것이다.

절차

  1. 첫 번째 단계는 모든 비기준 메서드를 동일하게 만드는 것이다. 이런 메서드에 각각 다음을 수행한다.
    a. 각 메서드 버전 본문의 기존 코드 주위에 if(true){}를 추가한다.
    b. true를 모든 기본 메서드를 호출하여 그 결과를 상수 값과 비교하는 표현식으로 바꾼다.
    c. 각 버전 본문을 복사하고 else와 함께 다른 모든 버전에 붙여넣음

  2. 이제 기준 메서드만 다르므로 두 번째 단계는 기준 메서드에 각 메서드에 대한 필드를 도입하고 생성자에서 상수를 할당하는 것으로 시작한다.

  3. 상수 대신 도입한 필드를 반환하도록 메서드를 변경한다.

  4. 문제가 없는지 컴파일한다.

  5. 각 클래스에 대해 한 번에 하나의 필드씩 다음을 수행한다.
    a. 필드의 기본값을 매개변수로 지정한다.
    b. 컴파일러 오류를 살펴보고 기본값을 인자로 전달

  6. 모든 클래스가 동일하면 통합한 클래스 중 하나를 제외한 모두를 삭제하고, 삭제하지 않은 클래스로 바꾸어 모든 컴파일러 오류를 수정한다.

5.2 단순한 조건 통합하기

updateTile을 진행하기 위해 몇몇 if 문의 내용을 더 비슷하게 만들고자 한다.

updateTile(x:number, y:number){
  if ((map[y][x].isStony())
        && map[y + 1][x].isAair()) {
        map[y + 1][x] =new Stone(new Falling());
        map[y][x] = new Air();
      } else if (map[y][x].isBoxy())
        && map[y + 1][x].isAir()) {
        map[y + 1][x] = new Box(new Falling());
        map[y][x] = new Air();
      } else if (map[y][x].isFallingStone()) {
        map[y][x] = new Stone(new Resting());
      } else if (map[y][x].isFallingBox()) {
        map[y][x] = new Box(new Resting());
      }
}

새로운 falling 필드를 설정하거나 설정을 해지하기 위한 메서드를 도입한다.

interface Tile {
  // ...
  drop(): void;
  rest(): void;
}

class Stone implements Tile {
  // ...
  drop() {this.falling = new Falling();}
  rest() {this.falling = new Resting();} 
}

class Flux implements Tile {
  // ...
  drop() { }
  rest() { }
}

updateTile에서 바로 rest 메서드를 사용할 수 있다.

//변경 전
updateTile(x:number, y:number){
  if ((map[y][x].isStony())
        && map[y + 1][x].isAair()) {
        map[y + 1][x] =new Stone(new Falling());
        map[y][x] = new Air();
      } else if (map[y][x].isBoxy())
        && map[y + 1][x].isAir()) {
        map[y + 1][x] = new Box(new Falling());
        map[y][x] = new Air();
      } else if (map[y][x].isFallingStone()) {
        map[y][x] = new Stone(new Resting());
      } else if (map[y][x].isFallingBox()) {
        map[y][x] = new Box(new Resting());
      }
}

//변경 후
updateTile(x:number, y:number){
  if ((map[y][x].isStony())
        && map[y + 1][x].isAair()) {
        map[y + 1][x] =new Stone(new Falling());
        map[y][x] = new Air();
      } else if (map[y][x].isBoxy())
        && map[y + 1][x].isAir()) {
        map[y + 1][x] = new Box(new Falling());
        map[y][x] = new Air();
      } else if (map[y][x].isFallingStone()) {
        **map[y][x].rest();**
      } else if (map[y][x].isFallingBox()) {
       ** map[y][x].rest();**
      }
}

마지막 두 if 문의 내용이 동일함을 알 수 있다. 서로 옆에 있는 두 개의 if 문이 같은 본문을 가질때 간단히 두 조건 사이에 || 넣어 합칠 수 있다.

// 변경 전
updateTile(x:number, y:number){
  if ((map[y][x].isStony())
        && map[y + 1][x].isAair()) {
        map[y + 1][x] =new Stone(new Falling());
        map[y][x] = new Air();
      } else if (map[y][x].isBoxy())
        && map[y + 1][x].isAir()) {
        map[y + 1][x] = new Box(new Falling());
        map[y][x] = new Air();
      } else if (map[y][x].isFallingStone()) {
        **map[y][x].rest();**
      } else if (map[y][x].isFallingBox()) {
       ** map[y][x].rest();**
      }
}
        
// 변경 후
updateTile(x:number, y:number){
  if ((map[y][x].isStony())
        && map[y + 1][x].isAair()) {
        map[y + 1][x] =new Stone(new Falling());
        map[y][x] = new Air();
      } 
  else if (map[y][x].isBoxy())
        && map[y + 1][x].isAir()) {
        map[y + 1][x] = new Box(new Falling());
        map[y][x] = new Air();
      } 
  else if (map[y][x].isFallingStone() 
           || map[y][x].isFallingBox()) {
        **map[y][x].rest();**
  	  }
}

|| 표현식을 클래스로 이관하고 두 가지 메서드 이름의 공통점을 이용해서 isFalling이라고 이름 짓는 일은 자연스럽다.

updateTile(x:number, y:number){
  if ((map[y][x].isStony())
        && map[y + 1][x].isAair()) {
        map[y + 1][x] =new Stone(new Falling());
        map[y][x] = new Air();
      } 
  else if (map[y][x].isBoxy())
        && map[y + 1][x].isAir()) {
        map[y + 1][x] = new Box(new Falling());
        map[y][x] = new Air();
      } 
  else if (map[y][x].isFalling()) {
        **map[y][x].rest();**
  	  }
}

5.2.1 리팩터링 패턴: if 문 결합

설명

내용이 동일한 연속적인 if 문을 결합해서 중복을 제거한다.

절차

  1. 본문이 실제로 동일한지 확인한다.
  2. 첫 번째 if 문의 닫는 괄호와 else if문의 여는 괄호 사이의 코드를 선택하고 삭제한 후 || 을 삽입한다. if 뒤에 여는 괄호를 삽입하고 { 앞에 닫는 괄호를 삽입하고 { 앞에 닫는 괄호를 삽입한다. 동작을 변경하지 않도록 항상 표현식을 괄호로 묶는다.
// 변경 전
if (expression1) {
  // 본문
}else if (expression2) {
  // 동일한 본문
}

//변경 후
if((expression1) || (expression2)) {
  // 본문
}
  1. 표현식이 단순하면 불필요한 괄호를 제거하거나 편집기에서 이를 수행하도록 설정할 수 있다.

5.3 복잡한 조건 통합하기

updateTitle의 첫 번째 if를 보면 단순히 하나의 돌(stone)을 공기(air)로, 하나의 공기(air)를 하나의 돌(stone)로 대체한다는 것을 알 수 있다.
이는 drop 함수를 사용해서 돌(stone) 타일을 이동시키고 떨어지는 상태(failling) 로 설정하는 것과 같다. 박스(box)의 경우도 마찬가지다.

// 변경 전
updateTile(x:number, y:number){
  if ((map[y][x].isStony())
        && map[y + 1][x].isAair()) {
        map[y + 1][x] =new Stone(new Falling());
        map[y][x] = new Air();
      } 
  else if (map[y][x].isBoxy())
        && map[y + 1][x].isAir()) {
        map[y + 1][x] = new Box(new Falling());
        map[y][x] = new Air();
      } 
  else if (map[y][x].isFalling()) {
        **map[y][x].rest();**
  	  }
}
  
// 변경 후
//돌이나 상자를 떨어뜨리고 타일을 교체 한 후 새로 공기를 주입.
updateTile(x:number, y:number){
  if ((map[y][x].isStony())
        && map[y + 1][x].isAair()) {
      map[y][x].drop(); // 떨어뜨림
      map[y + 1][x] = map[y][x]; //타일교체
      map[y][x] = new Air(); //새로 공기 주입
      } 
  else if (map[y][x].isBoxy())
        && map[y + 1][x].isAir()) {
      map[y][x].drop();
      map[y + 1][x] = map[y][x];
      map[y][x] = new Air();
      } 
  else if (map[y][x].isFalling()) {
        **map[y][x].rest();**
  	  }
}

처음 두 개의 if문의 본문이 동일하다. || 로 두 if문을 결합하기위해 if문 결합을 사용

// 변경 전
updateTile(x:number, y:number){
  if ((map[y][x].isStony())
        && map[y + 1][x].isAair()) {
      map[y][x].drop(); // 떨어뜨림
      map[y + 1][x] = map[y][x]; //타일교체
      map[y][x] = new Air(); //새로 공기 주입
      } 
  else if (map[y][x].isBoxy())
        && map[y + 1][x].isAir()) {
      map[y][x].drop();
      map[y + 1][x] = map[y][x];
      map[y][x] = new Air();
      } 
  else if (map[y][x].isFalling()) {
        **map[y][x].rest();**
  	  }
}
  
// 변경 후
updateTile(x:number, y:number){
  if (map[y][x].isStony()
        && map[y + 1][x].isAair() 
      	|| map[y][x].isBoxy()
        && map[y + 1][x].isAir()) {
      map[y][x].drop(); // 떨어뜨림
      map[y + 1][x] = map[y][x]; //타일교체
      map[y][x] = new Air(); //새로 공기 주입
      } 

  else if (map[y][x].isFalling()) {
        **map[y][x].rest();**
  	  }
}

결과 조건이 이전보다 약간 더 복잡하다. 이런 조건을 어떻게 다룰까?

5.3.1 조건을 위한 산술 규칙 하용

  • || 및 | 은 더하기(+)처럼 동작하고, && 및 & 은 곱하기(x) 처럼 동작한다.

a+b+c = (a+b)+c = a+(b+c) +의 결합 법칙
abc = (ab)c = a(bc) 의 결합 법칙
a+b = b+a +의 교환 법칙
a
b = ba 의 교환 법칙
a(b+c) = ab = ac 의 분배 법칙(왼쪽 + 연산에 대해)
(a+b)c = ac+bc 의 분배 법칙(오른쪽 + 연산에 대해)

5.3.2 규칙: 순수 조건 사용

정의

조건은 항상 순수 조건이어야 한다.

설명

  • 조건은 if 또는 while 뒤에 오는 것과 for 에 가운데에 있다.
  • 순수(Pure)라는 말은 조건에 부수적인 동작이 없음을 의미한다.
  • 부수적인 동작 이란 조건이 변수에 값을 할당하거나 예외를 발생시키거나 출력, 파일쓰기 등과 같이 I/O 와 상호작용하는 것을 의미한다.

순수한 조건을 갖는 것은 여러가지 이유로 중요하다.
첫째: 부수적인 동작이 존재하는 조건으로 인해 앞서 언급한 규칙들을 사용할 수 없다.
둘째: 부수적인 동작은 조건문에서 흔하게 사용하지 않기 때문에 조건에 부수적인 동작이 있을 것으로 생각하지 않는다. 이말은 즉 어떤 조건이 부수적인 동작을 가지고 있는지를 추적 하는 데 더 많은 시간과 노력을 투자해야한다는 것을 암시한다.

다음 코드는 일반적이며 readLine은 다음 줄을 반환하고 포인터를 전진시킨다. 포인터를 전진 시키는 것은 부수적인 동작이므로 이 조건은 순수하지 않다.

개선하는 방법은 줄을 가져오고 포인터를 이동하는 책임을 분리한다

// 변경 전
class Reader {
  private data: string[];
  private current: number;

  readLine(){
    this.current++;
    return this.data[this.current] || null;
  }
  //...
  let br = new Reader();
  let line: string | null;
  while((line = br.readLine()) !== null){
    console.log(line);
  }
}

// 변경 후
class Reader {
  private data: string[];
  private current: number;

  nextLine() {//부수적인 동작을 가진 새로운 메서드
    this.current++;
  }
  readLine() {// 기존 메서드에서 제거된 부수적인 동작
    return this.data[this.current] || null;
  }
  //...
  let br = new Reader();
  for(;br.readLine() !== null; br.nextLine()){
      let line = br.reaLine();
      console.log(line)
  }
}

의도

데이터를 가져오는 것과 변경하는 것을 분리하는것이다. 이는 코드를 더 깔끔하고 예측가능하게 만든다.

5.3.3 조건 산술 적용

map[y][x].isStony() && map[y+1][x].isAir()
|| map[y][x].isBoxy() && map[y+1][x].isAir()

= ab + cb
= (a+c)*b

(map[y][x].isStony() || map[y][x].isBoxy()) && map[y+1][x].isAir()

이 단순화를 코드에 적용하면 다음과 같다.

// 변경 전
updateTile(x:number, y:number){
  if (map[y][x].isStony()
        && map[y + 1][x].isAair() 
      	|| map[y][x].isBoxy()
        && map[y + 1][x].isAir()) {
      map[y][x].drop(); // 떨어뜨림
      map[y + 1][x] = map[y][x]; //타일교체
      map[y][x] = new Air(); //새로 공기 주입
      } 

  else if (map[y][x].isFalling()) {
        **map[y][x].rest();**
  	  }
}

// 변경 후
updateTile(x:number, y:number){
  if ((map[y][x].isStony() || map[y][x].isBoxy())
        && map[y + 1][x].isAair())
      {
      map[y][x].drop(); // 떨어뜨림
      map[y + 1][x] = map[y][x]; //타일교체
      map[y][x] = new Air(); //새로 공기 주입
      } 
  else if (map[y][x].isFalling()) {
        **map[y][x].rest();**
  	  }
}
 

4장에서는 돌과 상자 사이에 비슷한 성질이 있는데, 이를 pushable이라는 함수로 다뤘다. 그러나 이 상황에서는 의미가 없다. 동일한 관계를 다루고 있다고 해서 이름을 맹목적으로 재사용하지 않는 것이 중요하다. 이름은 문맥도 포함해야한다. 이 경우 canFall이라는 새로운 메서드를 사용한다!

// 변경 전
updateTile(x:number, y:number){
  if ((map[y][x].isStony() || map[y][x].isBoxy())
        && map[y + 1][x].isAair())
      {
      map[y][x].drop(); // 떨어뜨림
      map[y + 1][x] = map[y][x]; //타일교체
      map[y][x] = new Air(); //새로 공기 주입
      } 
  else if (map[y][x].isFalling()) {
        map[y][x].rest();
  	  }
}
  
// 변경 후
updateTile(x:number, y:number){
  if (map[y][x].canFall()
        && map[y + 1][x].isAair())
      {
      map[y][x].drop(); // 떨어뜨림
      map[y + 1][x] = map[y][x]; //타일교체
      map[y][x] = new Air(); //새로 공기 주입
      } 
  else if (map[y][x].isFalling()) {
        map[y][x].rest();
  	  }
}

5.4 클래스 간의 코드 통합

updateTile에 클래스로의 코드 이관 패턴을 적용할 수 있다.

// 변경 전
updateTile(x:number, y:number){
  if (map[y][x].canFall()
        && map[y + 1][x].isAir())
      {
      map[y][x].drop(); // 떨어뜨림
      map[y + 1][x] = map[y][x]; //타일교체
      map[y][x] = new Air(); //새로 공기 주입
      } 
  else if (map[y][x].isFalling()) {
        map[y][x].rest();
  	  }
}

// 변경 후
function updateTile(x:number, y:number){
  map[y][x].update(x,y);
}
interface Tile {
  // ...
  update(x:number, y:number):void;
}
class Air implements Tile {
  // ...
  update(x:number, y:number){}
}
class Stone implements Tile {
  // ...
  update(x:number ,y:number){
   if (map[y + 1][x].isAair())
      {
      this.falling = new Falling(); // 떨어뜨림
      map[y + 1][x] = this; //타일교체
      map[y][x] = new Air(); //새로 공기 주입
      } 
  else if (this.falling.isFalling()) {
        map[y][x].rest();
  	  }
  }
}

정리를 위해 updateTile을 인라인 한다. 삭제 후 컴파일하기를 이용한 중간 정리를 수행하기 좋은 시점이다. 이 과정에서 대부분의 isX 메서드가 제거된다. 남는 메서드는 isLockX와 isAir같이 다른 타일의 동작에 영향을 미치는 특별한 의미를 가지는 것이다.

Stone과 Box가 정확히 그런 코드를 갖고 있다. 이것은 분기를 필요로 하는 곳이 아니다. 떨어지는 동작은 동기화 상태를 유지해야하며, 나중에 다른 타일을 더 도입해도 다시 사용할 수 있도록 해야한다.

  1. 먼저 새로운 FallStrategy 클래스를 만든다.
// 새로운 클래스 
class FallStrategy {
}
  1. Stone과 Box의 생성자에서 FallStrategy를 인스턴스화 한다.
//변경 전
class Stone implements Tile {
  constructor(
  	private falling:FallingState)
  {
  }
  //...
}

//변경 후
class Stone implements Tile {
  private fallStrategy: FallStrategy; // 새로운 필드
   constructor(
  	private falling:FallingState)
  {
    this.fallStrategy = new FallStrategy(); //새로운 필드 초기화
  }
}
  1. 클래스로의 코드 이관과 같은 방식으로 update를 FallStrategy로 옮긴다.
//변경 전
class Stone implements Tile {
   update(x:number ,y:number){
   if (map[y + 1][x].isAair())
      {
      this.falling = new Falling(); // 떨어뜨림
      map[y + 1][x] = this; //타일교체
      map[y][x] = new Air(); //새로 공기 주입
      } 
  else if (this.falling.isFalling()) {
       this.falling = new Resting();
  	  }
  }
}

class FallStrategy {
}

//변경 후
class FallStrategy {
  update(x:number ,y:number){
   if (map[y + 1][x].isAair())
      {
      this.falling = new Falling(); // 떨어뜨림
      map[y + 1][x] = this; //타일교체
      map[y][x] = new Air(); //새로 공기 주입
      } 
  else if (this.falling.isFalling()) {
       this.falling = new Resting();
  	  }
  }
}
  1. fallling 필드에 의존하므로 다음을 수행한다.
    a falling 필드를 FallStrategy로 옮기고 이에 대한 접근자를 만든다.
// 변경 전
class Stone implements Tile {
  private fallStrategy: FallStrategy; // 새로운 필드
   constructor(
  	private falling:FallingState)
  {
    this.fallStrategy = new FallStrategy(); //새로운 필드 초기화
  }
}
class FallStrategy {
  // ...
}

// 변경 후
class Stone implements Tile {
  private fallStrategy: FallStrategy; // 
   constructor(
  	 falling:FallingState) //private을 제거
  {
    this.fallStrategy = new FallStrategy(falling); //인자 추가
  }
}
class FallStrategy {
  constructor(
  	private falling: FallingState) //매개변수를 가진 생성자 추가
  ){ }
  getFalling(){return this.falling;} // 새로운 필드에 대한 접근자
}

b 새로운 접근자를 사용해서 원래 클래스의 오류를 수정한다.

// 변경 전
class Stone implements Tile {
  //...
  moveHorizontal(dx:number){ 
    this.falling.moveHorizontal(this, dx);
  }
  
}
// 변경 후
class Stone implements Tile {
  //...
  moveHorizontal(dx:number){ 
    this.fallStrategy.getFalling().moveHorizontal(this, dx);
  } 
}
  1. FallStrategy의 나머지 오류에 대해 this를 대체할 tile 매개변수를 추가한다.
//변경 전
class Stone implements Tile {
  // ...
  update(x:number, y:number){
    this.fallStrategy.update(x,y);
  }
}
class FallStrategy {
  update(x:number ,y:number){
   if (map[y + 1][x].isAir())
      {
      this.falling = new Falling(); // 떨어뜨림
      map[y + 1][x] = this; //타일교체
      map[y][x] = new Air(); //새로 공기 주입
      } 
  else if (this.falling.isFalling()) {
       this.falling = new Resting();
  	  }
  }
}

//변경 후
class Stone implements Tile {
  // ...
  update(x:number, y:number){
    this.fallStrategy.update(this,x,y);
  }
}
class FallStrategy {
  update(tile:Tile, x:number ,y:number){
   if (map[y + 1][x].isAir())
      {
      this.falling = new Falling();
      map[y + 1][x] = tile; //this를 대체할 매개변수 추가
      map[y][x] = new Air(); 
      } 
  else if (this.falling.isFalling()) {
       this.falling = new Resting();
  	  }
  }
}

결과적으로 다음과 같은 변화가 발생한다.

//변경 전
class Stone implements Tile {
  constructor(private falling:FallingState){
  }
   update(x:number ,y:number){
   if (map[y + 1][x].isAir())
      {
      this.falling = new Falling(); // 떨어뜨림
      map[y + 1][x] = this; //타일교체
      map[y][x] = new Air(); //새로 공기 주입
      } 
  else if (this.falling.isFalling()) {
       this.falling = new Resting();
  	  }
  }
}

//변경 후

class Stone implements Tile {
  // ...
  private fallStrategy: FallStrategy;
  constructor(falling: FallingState){
    this.fallStrategy = new FallStrategy(falling);
  }
  update(x:number, y:number){
    this.fallStrategy.update(this,x,y);
  }
}
class FallStrategy {
  constructor(private falling: FallingState){ }
  isFalling() {return this.falling;}
  update(tile:Tile, x:number ,y:number){
   if (map[y + 1][x].isAir())
      {
      this.falling = new Falling();
      map[y + 1][x] = tile; //this를 대체할 매개변수 추가
      map[y][x] = new Air(); 
      } 
  else if (this.falling.isFalling()) {
       this.falling = new Resting();
  	  }
  }
}

FallStrategy.update에서 else if를 자세히 살펴보면 falling이 true이면 false로 설정되고, 그렇지 않으면 이미 false인 상태이다. 따라서 이 상태를 제거할 수 있다.

// 변경 전
class FallStrategy {
  // ...
  update(tile:Tile, x:number ,y:number){
   if (map[y + 1][x].isAir())
      {
      this.falling = new Falling();
      map[y + 1][x] = tile; //this를 대체할 매개변수 추가
      map[y][x] = new Air(); 
      } 
  else if (this.falling.isFalling()) {
       this.falling = new Resting();
  	 }
  }
}

// 변경 후
class FallStrategy {
  // ...
  update(tile:Tile, x:number ,y:number){
   if (map[y + 1][x].isAir())
      {
      this.falling = new Falling();
      map[y + 1][x] = tile;
      map[y][x] = new Air(); 
      } 
   else {
       this.falling = new Resting();
  	 }
  }
}

이제 코드는 모든 경우에서 falling을 할당하기 때문에 이를 밖으로 꺼내 처리할 수 있다. 그리고 빈 else를 제거한다. 그러면 변수와 동일한 값인지를 확인하는 if가 존재한다. 이 경우에는 대신 변수를 직접 사용하는 것이 좋다.

// 변경 전
class FallStrategy {
  // ...
  update(tile:Tile, x:number ,y:number){
   if (map[y + 1][x].isAir())
      {
      this.falling = new Falling();
      map[y + 1][x] = tile;
      map[y][x] = new Air(); 
      } 
   else {
       this.falling = new Resting();
  	 }
  }
}

// 변경 후
class FallStrategy {
  // ...
  update(tile:Tile, x:number ,y:number){
   this.falling = map[y + 1][x].isAir()
    ? new Falling()
    : new Resting(); // this.falling을 if문에서 분리
    if(this.falling.isFalling()){
      map[y + 1][x] = tile;
      map[y][x] = new Air();
    }
}

update메소드가 다섯줄을 넘기지 않지만. if 문은 함수의 시작에만 배치 라는 규칙을 상기하자. 이 규칙을 따르기 위해 간단히 메서드 추출을 수행한다.

// 변경 전
class FallStrategy {
  // ...
  update(tile:Tile, x:number ,y:number){
   this.falling = map[y + 1][x].isAir()
    ? new Falling()
    : new Resting(); // this.falling을 if문에서 분리
    if(this.falling.isFalling()){
      map[y + 1][x] = tile;
      map[y][x] = new Air();
    }
}
  
// 변경 후
class FallStrategy {
  // ...
  update(tile:Tile, x:number ,y:number){
   this.falling = map[y + 1][x].isAir()
    ? new Falling()
    : new Resting(); // this.falling을 if문에서 분리
   this.drop(tile, x, y);
    
  private drop(tile:Tile, x:number, y:number){
    if(this.falling.isFalling()){
      map[y + 1][x] = tile;
      map[y][x] = new Air();
    }
  }
}

5.5 유사 함수 통합하기

유사한 코드를 가진 또 다른 곳은 removeLock1과 removeRock2 함수가 있는 곳이다.

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

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

변형을 도입해야한다!

  1. 분리하려는 코드에서 메서드 추출을 수행하는 것으로 시작한다.
// 변경 전
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();
      }
    }
  }
}

// 변경 후
//removeRock1
function removeLock1() {
   for (let y = 0; y < map.length; y++) {
    for (let x = 0; x < map[y].length; x++) {
      if (check(map[y][x])) {
        map[y][x] = new Air();
      }
    }
  }
}

function check(tile:Tile){
  return tile.isLock1();
}
  1. 새로운 클래스를 만든다.
class RemoveStrategy {
}
  1. 이 경우 새로운 클래스를 인스턴스화 할 수 있는 생성자가 없다. 대신 함수에서 직접 인스턴스화 한다.
// 변경 전
function removeLock1() {
   for (let y = 0; y < map.length; y++) {
    for (let x = 0; x < map[y].length; x++) {
      if (check(map[y][x])) {
        map[y][x] = new Air();
      }
    }
  }
}

funtion check(tile: Tile){
  return tile.isLock1();
}

// 변경 후
function removeLock1() {
  let shouldRemove = new RemoveStrategy();
   for (let y = 0; y < map.length; y++) {
    for (let x = 0; x < map[y].length; x++) {
       if (check(map[y][x])) {
        map[y][x] = new Air();
      }
    }
  }
}
  1. 메서드를 옮긴다.
// 변경 전
function removeLock1() {
  let shouldRemove = new RemoveStrategy();
   for (let y = 0; y < map.length; y++) {
    for (let x = 0; x < map[y].length; x++) {
       if (check(map[y][x])) {
        map[y][x] = new Air();
      }
    }
  }
}
funtion check(tile: Tile){
  return tile.isLock1();
}

// 변경 후
function removeLock1() {
  let shouldRemove = new RemoveStrategy();
   for (let y = 0; y < map.length; y++) {
    for (let x = 0; x < map[y].length; x++) {
       if (shouldRemove.check(map[y][x])) {
        map[y][x] = new Air();
      }
    }
  }
}

class RemoveStrategy {
   check(tile: Tile){
    return tile.isLock1();
  }
}
  1. 필드에 대한 종속성이 없고 새로운 클래스에 오류가 없다.

전략을 도입했기 때문에 변형을 추가 할 수 있도록 구현에서 인터페이스 추출을 사용한다.

  1. 추출할 클래스와 동일한 이름으로 새로운 인터페이스를 만든다.
interface RemoveStrategy {
}
  1. 인터페이스를 추출하려는 클래스의 이름을 바꾸고 새로운 인터페이스를 구현하게 한다.
//변경 전
class RemoveStrategy {
}

//변경 후
class RemoveLock1 implements RemoveStrategy {
  //...
}
  1. 컴파일한 오류를 검토한다.
    a. new로 인한 오류일 경우 새로운 클래스 이름으로 변경한다.
//변경 전
function removeLock1() {
  let shouldRemove = new RemoveStrategy();
   for (let y = 0; y < map.length; y++) {
    for (let x = 0; x < map[y].length; x++) {
       if (shouldRemove.check(map[y][x])) {
        map[y][x] = new Air();
      }
    }
  }
}

//변경 후
function removeLock1() {
  let shouldRemove = new RemoveLock1();
   for (let y = 0; y < map.length; y++) {
    for (let x = 0; x < map[y].length; x++) {
       if (shouldRemove.check(map[y][x])) {
        map[y][x] = new Air();
      }
    }
  }
}

b 그렇지 않으면 오류를 일으키는 메서드를 인터페이스에 추가한다.

//변경 전
interface RemoveStrategy {
}

//변경 후
interface RemoveStrategy {
  check(tile:Tile):boolean;
}

이제 RemoveLock1의 복사본에서 RemoveLock2를 만드는 것은 간단하다. 만든 다음 shouldRemove만 매개 변수로 옮기면 된다.

  1. removeLock1에서 첫 번째 줄을 제외한 모든것을 추출해서 remove를 얻는다.
  2. shouldRemove 지역변수는 한 번만 사용되므로 인라인화 한다.
  3. removeLock1에 메서드의 인라인화를 수행해 제거한다.

리팩터링의 결과로 단 하나의 remove만 남는다.

//변경 전
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();
      }
    }
  }
}

class Key1 implements Tile {
  //...
  moveHorizontal(dx:number){
    removeLock1();
    moveToTile(playerx + dx, playery);
  }
}

//변경 후
function remove(
shouldRemove:RemoveStrategy
){
  for (let y = 0; y < map.length; y++) {
    for (let x = 0; x < map[y].length; x++) {
      if (shouldRemove.check(map[y][x])) {
        map[y][x] = new Air();
      }
    }
  }
}
class Key1 implements Tile {
   //...
  moveHorizontal(dx:number){
    remove(new RemoveLock1());
    moveToTile(playerx + dx, playery);
  }
}

interface RemoveStrategy {
  check(tile: Tile):boolean;
}
class RemoveLock1 implements RemoveStrategy {
  check(tile:Tile){
    return tile.isLock1();
  }
}

다른 유형의 타일을 제거하려면 수정 없이 RemoveStrategy를 구현하는 다른 클래스를 간단히 만들면 된다.

어떤 애플리케이션에서는 루프 내에서 new를 호출하는 것을 꺼린다. 그렇게 하면 애플리케이션의 속도가 느려질 수 있기 떄문이다. 이 경우 RemoveLock 전략을 인스턴스 변수에 간단히 저장하고 생성자에서 초기화할 수 있다.

5.6 유사한 코드 통합하기

Key1과 Key2, Lock1과 Lock2에도 중복된 부분이 존재한다. 각 경우에서 짝지어진 클래스가 거의 동일하다.

class Key1 implements Tile {
  // ...
  draw(g: CanvasRenderingContext2D,x:number,y:number){
    g.fillStyle = "#ffcc00";
    g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
  }
  
  moveHorizontal(dx:number){
    remove(new RemoveLock1())'
    moveToTile(playerx + dx, playery);
  }
}

class Lock1 implements Tile {
  // ...
  isLock1() {return true;}
  isLock2() {return false;}
  
   draw(g: CanvasRenderingContext2D,x:number,y:number){
    g.fillStyle = "#ffcc00";
    g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
  }
  
}

class Key2 implements Tile {
  // ...
  draw(g: CanvasRenderingContext2D,x:number,y:number){
    g.fillStyle = "#00ccff";
    g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
  }
  
  moveHorizontal(dx:number){
    remove(new RemoveLock2())'
    moveToTile(playerx + dx, playery);
  }
}

class Lock2 implements Tile {
  // ...
  isLock1() {return false;}
  isLock2() {return true;}
  
   draw(g: CanvasRenderingContext2D,x:number,y:number){
    g.fillStyle = "#00ffcc";
    g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
  }
  
}

먼저 두 Lock과 Key 모두에 유사 클래스 통합을 수행한다.

// 변경 전
class Key1 implements Tile {
  // ...
  draw(g: CanvasRenderingContext2D,x:number,y:number){
    g.fillStyle = "#ffcc00";
    g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
  }
  
  moveHorizontal(dx:number){
    remove(new RemoveLock1())'
    moveToTile(playerx + dx, playery);
  }
}

class Lock1 implements Tile {
  // ...
  draw(g: CanvasRenderingContext2D,x:number,y:number){
    g.fillStyle = "#ffcc00";
    g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
  }
}

function transformTile(tile:RawTile){
  switch(tile){
      //...
    case RawTile.KEY1:
      return new Key1();
    case RawTIle.LOCK1:
      return new Lock1();
  }
}

// 변경 후

class Key implements Tile {
 constructor(
  private color:string,
  private removeStrategy: RemoveStrategy)
 }{ }
//...
  draw(g: CanvasRenderingContext2D,x:number,y:number){
    g.fillStyle =this.color;
    g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
  }
  
  moveHorizontal(dx:number){
    remove(this.removeStrategy);
    moveToTile(playerx + dx, playery);
  }
}

class Lock implements Tile {
 constructor(
  private color: string,
  private lock1: boolean,
  private lock2: boolean
  ){ }
  //...
  isLock1(){return this.lock1;}
  isLock2(){return this.lock2;}
  
}

function transformTile(tile:RawTile){
  switch(tile){
      //...
    case RawTile.KEY1:
      return new Key("#ffcc00", new RemoveLock1());
    case RawTIle.LOCK1:
      return new Lock("#ffcc00", true, false);
  }
}

이 코드가 동작하는 데는 문제가 없지만 이미 알고 있느 일부 구조를 활용해 개선할 수 있다. isLock1과 isLock2 메서드를 추가했다. 이들 메서드는 열거형의두 값에서 가져왔으므로 이러한 메서드 중 하나만 주어진 클래스에 대해 true를 반환할 수 있다는 것은 이미 알고 있다. 따라서 메서드를 모두 나타내는 매개변수는 하나만 필요하다.

//변경 전
class Lock implements Tile {
 constructor(
  private color: string,
  private lock1: boolean,
  private lock2: boolean
  ){ }
  //...
  isLock1(){return this.lock1;}
  isLock2(){return this.lock2;}
  
}

//변경 후
class Lock implements Tile {
 constructor(
  private color: string,
  private lock1: boolean,
  ){ }
  //...
  isLock1(){return this.lock1;}
  isLock2(){return !this.lock1;}
  
}

또한 Key 및 Lock에 있는 생성자의 매개변수인 color, lock2, removestrategy 사이에 연관성이 있어 보인다. 두 클래스를 걸쳐 무언가를 통합하고 싶을 때 새로운 트릭인 전략 패턴의 도입을 사용한다.

class Key implements Tile {
 constructor(
  private color:string,
  private removeStrategy: RemoveStrategy)
 }{ }
//...
  draw(g: CanvasRenderingContext2D,x:number,y:number){
    g.fillStyle =this.color;
    g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
  }
  
  moveHorizontal(dx:number){
    remove(this.removeStrategy);
    moveToTile(playerx + dx, playery);
  }
}

class Lock implements Tile {
 constructor(
  private color: string,
  private lock1: boolean,
  ){ }
  //...
  isLock1(){return this.lock1;}
  isLock2(){return !this.lock1;}
  
}

function transformTile(tile:RawTile){
  switch(tile){
      //...
    case RawTile.KEY1:
      return new Key("#ffcc00", new RemoveLock1());
    case RawTIle.LOCK1:
      return new Lock("#ffcc00", true);
  }
}

//변경 후
class Key implements Tile {
 constructor(
  private keyConf: KeyConfiguration
 )
 }{ }
//...
  draw(g: CanvasRenderingContext2D,x:number,y:number){
    g.fillStyle =this.keyConf.getColor();
    g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
  }
  
  moveHorizontal(dx:number){
    remove(this.keyConf.getRemoveStrategy());
    moveToTile(playerx + dx, playery);
  }
  moveVertical(dy: number){
    remove(this.keyConf.getRemoveStrategy());
    moveToTile(playerx, playery + dy);
  }
}

class Lock implements Tile {
 constructor(
   private keyConf: KeyConfiguration
  ){ }
  //...
  isLock1(){return this.keyConf.is1();}
  isLock2(){return !this.keyConf.is1();}
  draw(g: CanvasRenderingContext2D,x:number,y:number){
    g.fillStyle =this.keyConf.getColor();
    g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
  }
  
}
class KeyConfiguration {
  constructor(
    private color: string,
    private _1:boolean,
    privage removeStrategy:RemoveStrategy
  ){ }
  getColor() {return this.color;}
  _1() {return this._1};
  getRemoveStrategy(){
    return this.removeStrategy;
  }

}
const YELLOW_KEY = new KeyConfiguration("#ffcc00", true, new RemoveLock1());

function transformTile(tile:RawTile){
  switch(tile){
      //...
    case RawTile.KEY1:
      return new Key(YELLO_KEY);
    case RawTile.LOCK1:
      return new Lock(YELLO_KEY);
  }
}

요약

  • 모아야 할 코드가 있을 때 우리는 그것을 통합해야한다. 유사 클래스 통합, if 문 결합을 사용해서 클래스를 통합하고 전략 패턴의 도입을 사용해서 메서드를 통합할 수 있다.
  • 순수 조건 사용 규칙은 조건에 부수적인 동작이 없어야 한다고 명시하고 있다. 부수적인 동작이 없는 경우 조건부 산술을 사용할 수 있기 때문이다. 또한 부수적인 동작을 조건과 분리하기 위해 캐시를 사용하는 방법을 살펴 보았다.
  • UML 클래스 다이어그램은 일반적으로 코드베이스에 대한 특정 아키텍처의 변경을 설명하기 위해 사용된다.
  • 구현 클래스가 하나뿐인 인터페이스는 불필요한 일반화의 한 형태이다. 구현체가 하나뿐인 인터페이스를 만들지 말것 규칙에는 이런 항목이 없어야한다. 대신 구현에서 인터페이스 추출 리팩터링 패턴을 사용해 인터페이스를 나중에 도입해야한다.
profile
잼나게 코딩하면서 살고 싶어요 ^O^/

0개의 댓글