이번 장에서 다룰 내용
이전의 경우와 마찬가지로 그대로 보존하면서 강조하고 싶은 관계를 나타내는 괄호로 묶은 표현식(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 메서드의 결과 뿐이다.
메서드가 상수를 반환 할 때 상수 메서드 라고 부른다.
경우에 따라 다른 값을 반환하는 상수 메서드를 공유하기 때문에 이 두 클래스는 합칠 수 있다.
클래스를 결합하는 첫 번째 단계는 상수 메서드를 제외한 클래스의 모든 것을 동일하게 만드는 것이다. 두번 째 단 계는 실제로 합치는 것이다.
//변경전
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) {
}
}
}
//변경 전
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;}
}
//변경 전
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;}
}
//변경 전
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)
// 변경 전
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. 코드 깔끔!
첫 번째 단계는 모든 비기준 메서드를 동일하게 만드는 것이다. 이런 메서드에 각각 다음을 수행한다.
a. 각 메서드 버전 본문의 기존 코드 주위에 if(true){}를 추가한다.
b. true를 모든 기본 메서드를 호출하여 그 결과를 상수 값과 비교하는 표현식으로 바꾼다.
c. 각 버전 본문을 복사하고 else와 함께 다른 모든 버전에 붙여넣음
이제 기준 메서드만 다르므로 두 번째 단계는 기준 메서드에 각 메서드에 대한 필드를 도입하고 생성자에서 상수를 할당하는 것으로 시작한다.
상수 대신 도입한 필드를 반환하도록 메서드를 변경한다.
문제가 없는지 컴파일한다.
각 클래스에 대해 한 번에 하나의 필드씩 다음을 수행한다.
a. 필드의 기본값을 매개변수로 지정한다.
b. 컴파일러 오류를 살펴보고 기본값을 인자로 전달
모든 클래스가 동일하면 통합한 클래스 중 하나를 제외한 모두를 삭제하고, 삭제하지 않은 클래스로 바꾸어 모든 컴파일러 오류를 수정한다.
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();**
}
}
내용이 동일한 연속적인 if 문을 결합해서 중복을 제거한다.
// 변경 전
if (expression1) {
// 본문
}else if (expression2) {
// 동일한 본문
}
//변경 후
if((expression1) || (expression2)) {
// 본문
}
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();**
}
}
결과 조건이 이전보다 약간 더 복잡하다. 이런 조건을 어떻게 다룰까?
a+b+c = (a+b)+c = a+(b+c) +의 결합 법칙
abc = (ab)c = a(bc) 의 결합 법칙
a+b = b+a +의 교환 법칙
ab = ba 의 교환 법칙
a(b+c) = ab = ac 의 분배 법칙(왼쪽 + 연산에 대해)
(a+b)c = ac+bc 의 분배 법칙(오른쪽 + 연산에 대해)
조건은 항상 순수 조건이어야 한다.
순수한 조건을 갖는 것은 여러가지 이유로 중요하다.
첫째: 부수적인 동작이 존재하는 조건으로 인해 앞서 언급한 규칙들을 사용할 수 없다.
둘째: 부수적인 동작은 조건문에서 흔하게 사용하지 않기 때문에 조건에 부수적인 동작이 있을 것으로 생각하지 않는다. 이말은 즉 어떤 조건이 부수적인 동작을 가지고 있는지를 추적 하는 데 더 많은 시간과 노력을 투자해야한다는 것을 암시한다.
다음 코드는 일반적이며 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)
}
}
데이터를 가져오는 것과 변경하는 것을 분리하는것이다. 이는 코드를 더 깔끔하고 예측가능하게 만든다.
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();
}
}
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가 정확히 그런 코드를 갖고 있다. 이것은 분기를 필요로 하는 곳이 아니다. 떨어지는 동작은 동기화 상태를 유지해야하며, 나중에 다른 타일을 더 도입해도 다시 사용할 수 있도록 해야한다.
// 새로운 클래스
class FallStrategy {
}
//변경 전
class Stone implements Tile {
constructor(
private falling:FallingState)
{
}
//...
}
//변경 후
class Stone implements Tile {
private fallStrategy: FallStrategy; // 새로운 필드
constructor(
private falling:FallingState)
{
this.fallStrategy = new 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();
}
}
}
// 변경 전
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);
}
}
//변경 전
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();
}
}
}
유사한 코드를 가진 또 다른 곳은 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();
}
}
}
}
변형을 도입해야한다!
// 변경 전
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();
}
class RemoveStrategy {
}
// 변경 전
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();
}
}
}
}
// 변경 전
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();
}
}
전략을 도입했기 때문에 변형을 추가 할 수 있도록 구현에서 인터페이스 추출을 사용한다.
interface RemoveStrategy {
}
//변경 전
class RemoveStrategy {
}
//변경 후
class RemoveLock1 implements RemoveStrategy {
//...
}
//변경 전
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만 매개 변수로 옮기면 된다.
리팩터링의 결과로 단 하나의 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 전략을 인스턴스 변수에 간단히 저장하고 생성자에서 초기화할 수 있다.
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);
}
}