파이브 라인스 오브 코드 - 5장

이마닷·2024년 8월 24일
0
post-thumbnail

5장. 유사한 코드 융합하기

5.1 유사한 클래스 통합하기

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

설명
일련의 상수 메서드(상수를 반환하는 메서드)를 공통으로 가진 둘 이상의 클래스에서, 이 일련의 상수 메서드가 클래스에 따라 다른 값을 반환할 때 클래스를 통합할 수 있습니다.
여기서 일련의 상수 메서드 집합을 기준(basis)라고 합니다. 일련의 상수 메서드가 두 개일 때 두 개의 접점을 가진 기준이라고 합니다. X개의 클래스를 통합하려면 최대 X-1개의 접점을 가진 기준이 필요합니다.

  • 예제 리팩터링 전
class Stone implements Tile {
  moveHorizontal(dx: number) {
    ...
  }
  ...
}
  
class FallingStone implements Tile {
  moveHorizontal(dx: number) {
    ...
  }
  ...
}  

function updateTile(x: number, y: number) {
  if ((map[y][x].isStone() || map[y][x].isFallingStone()) && map[y+1][x].isAir()) {
    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();
  }
}         
  • 예제 리팩터링 후
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; }
}


class Stone implements Tile {
  private falling: FallingState;

  constructor(falling: FallingState) {
    this.falling = falling;
  }
  
  isFallingStone() { return this.falling.isFalling }

  moveHorizontal(dx: number) {
    if (!this.isFallingStone()) {{
      ...
    } else {
      ...
    }
  ...
}
  

function updateTile(x: number, y: number) {
  if (map[y][x].isStony() && map[y+1][x].isAir()) {
    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());
  }
}         

5.2 단순한 조건 통합하기

리팩터링 패턴: if문 결합

설명
내용이 동일한 연속적인 if문을 결합해서 중복을 제거합니다. 이 패턴은 ||를 추가해서 두 식의 관계를 드러내기 때문에 유용합니다.
표현식이 단순하면 불필요한 괄호를 제거하거나 편집기에서 이를 수행하도록 설정할 수 있습니다.

  • 변경 전
if (expression1) {
  // 본문
} else if (expression2) {
 // 동일한 본문
}
  • 변경 후
if ((expression1) || (expression2)) {
  // 본문
}
  • 예제 리팩터링 전
function updateTile(x: number, y: number) {
  if (map[y][x].isStony() && map[y+1][x].isAir()) {
    map[y+1][x] = new Stone(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());
  }
}
  • 예제 리팩터링 후
interface Tile {
  ...
  drop(): void;
  rest(): void;
}

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

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

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

5.3 복잡한 조건 통합하기

규칙: 순수 조건 사용

설명
조건if 또는 while 뒤에 오는 것과 for 루프의 가운데에 있는 것입니다. 순수(pure)라는 말은 조건에 부수적인 동작이 없음을 의미합니다. 부수적인 동작이란 조건이 변수에 값을 할당하거나 예외를 발생시키거나 출력, 파일 쓰기 등과 같이 I/O와 상호작용하는 것을 의미합니다. 이 규칙은 '메서드는 한 가지 작업을 해야 한다'에 근거한다고 말할 수 있습니다.

  • 결합법칙, 교환법칙, 분배법칙 등 단순 산술 규칙을 사용해 조건문을 보다 단순화 시키기 위해서는, 순수 조건만 사용하도록 코드를 작성해야 한다

  • 예제 조건문 단순화 과정

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

5.4 클래스 간의 코드 통합

리팩터링 패턴: 전략 패턴의 도입

설명
다른 클래스를 인스턴스화해서 변형(variance)을 도입하는 개념을 전략 패턴이라고 합니다.
전략(strategy)이 필드를 가지고 있는 경우, 이를 상태 패턴(state pattern)이라고 합니다. 이러한 구분은 대부분 이론적인 것입니다. 기본적인 아이디어는 동일합니다. 바로 클래스를 추가해서 변경이 가능하게 하는 것입니다.
변형(전략)은 전략 패턴의 목적이기 때문에 항상 상속으로 묘사됩니다. 전략 패턴의 변형은 늦은 바인딩의 궁극적인 형태입니다. 런타임에 전략 패턴을 사용하면 코드에 사용자 정의 클래스를 적재하고, 이를 제어 흐름에 원활하게 통합할 수 있습니다.

  • 동일한 구조를 가진 서로 다른 클래스들에 대하여, 인터페이스를 상속한 형태의 전략을 컴포지션 형태로 의존성 주입함으로써 코드 통합

두 가지 상황에서 전략 패턴을 도입합니다.
첫째, 코드에 변형을 도입하고 싶어서 리팩터링을 수행하는 경우입니다. 이 경우 결국 인터페이스가 있어야 합니다.
둘째, 떨어지는 성질을 코드화했던 상황에서 바로 변형의 추가가 필요하다고 예상하지 않았을 때입니다. 단지 클래스 간의 동작을 통합하려는 경우입니다.

규칙: 구현체가 하나뿐인 인터페이스를 만들지 말 것

설명
이 규칙은 단일 구현체를 가진 인터페이스가 있으면 안된다고 명시하고 있습니다.
한 가지 주장은 구현 클래스가 하나밖에 없는 인터페이스는 가독성에 도움이 되지 않는다는 것입니다. 더 나쁜 것은 인터페이스는 변형을 전제로 하는데, 아무것도 없다면 오히려 가독성을 방해할 수 있다는 점입니다. 또한 구현 클래스를 수정하려는 경우 인터페이스를 수정해야 해서 오버헤드를 발생시킵니다.

의도
의도는 불필요한 코드의 상용구(boilerplate)를 제한하는 것입니다. 인터페이스는 상용구의 일반적인 원인입니다. 많은 사람들이 인터페이스는 항상 바람직하다고 배웠기 때문에 특히 위험합니다.

리팩터링 패턴: 구현에서 인터페이스 추출

설명
인터페이스를 만드는 것을, 필요할 때(변형을 도입하고 싶을 때)까지 연기할 수 있어서 유용합니다.

  • 리팩터링 패턴: 전략 패턴의 도입을 수행 시에 적용할 수 있는 패턴. 전략 패턴을 도입하고자 할 때에 전략 인터페이스를 먼저 만들기 보다, 공통된 구조의 구현체들을 먼저 작성해보고 해당 구현체들로부터 하나의 인터페이스를 도출할 수 있는 패턴

  • 예제 리팩터링 전

    • 배열을 일괄 처리할 수 있는 두 개의 클래스에 대한 소스코드
    • 최솟값을 찾는 일괄 처리 프로세서와 합을 계산하는 일괄 처리 프로세서
class ArrayMinimum {
  constructor(private accumulator: number) { }
  process(arr: number[]) {
    for (let i = 0; i < arr.length; i++)
      if (this.accumulator > arr[i])
        this.accumulator = arr[i];
    return this.accumulator;
  }
}

class ArraySum {
  constructor(private accumulator: number) { }
  process(arr: number[]) {
    for (let i = 0; i < arr.length; i++)
      this.accumulator += arr[i];
    return this.accumulator;
  }
}
  • 예제 리팩터링 후
class BatchProcessor {
  constructor(private processor: ElementProcessor) { }
  process(arr: number[]) {
    for (let i = 0; i < arr.length; i++)
      this.processor.processElement(arr[i]);
    return this.processor.getAccumulator;
  }
}

interface ElementProcessor { // 추상화된 전략
  processElement(e: number): void;
  getAccumulator(): number;
}

class MinimumProcessor implements ElementProcessor {
  constructor(private accumulator: number) { }
  getAccumulator() { return this.accumulator; }
  process(e: number) {
    if (this.accumulator > e)
      this.accumulator = e;
  }
}

class SumProcessor implements ElementProcessor {
  constructor(private accumulator: number) { }
  getAccumulator() { return this.accumulator; }
  process(e: number) {
    this.accumulator += e;
  }
}
profile
적당히 잘하고 싶어요

0개의 댓글