4 minute read

객체의 행동을 자유롭게 바꿔 끼울 수 있는 패턴

상속(Inheritance)보단 조립(Composition)을 활용하라는 객체지향 원칙을 따른다.

오리는 날고 싶다.

quack(), swim() 메서드를 가진 “오리” 클래스가 있다.

duck

오리들이 날아다닐 수 있도록 하고 싶다.

상속 사용하기

오리 클래스에 fly() 메서드를 추가했다.

duck2

그랬더니 날면 안 되는 고무오리까지 날아다니게 됐다.

flying0rubber-duck

상속으로 코드를 재사용할 수 있을 거 같았지만 별 도움이 안 된다.

날면 안 되는 오리에 날지 못하도록 fly() 메서드를 오버라이드 해보려니

오리마다 fly() 메서드가 아무것도 안 하도록 오버라이드를 해야 돼서 코드 칠 게 너무 많다.

인터페이스 사용하기

fly() 메서드를 가진 Flayable 인터페이스와, quack() 메서드를 가진 Quackable 인터페이스를 만들었다.

duck(3)

인터페이스를 사용해서 자식 오리 클래스들이 자신들이 하는 행동만 구현해서 사용할 수 있도록 했다.

오리 종류 100가지나 된다면, 모든 오리 서브 클래스의 메서드를 오버라이드 하기란 쉽지 않다.

디자인 원칙 1

애플리케이션에서 달리지는 부분을 찾아내고, 달라지지 않는 부분으로부터 분리시킨다.

다시 말하면,

바뀌는 부분은 따로 뽑아서 캡슐화시킨다. 그렇게 하면 나중에 바뀌지 않는 부분에는 영향을 미치지 않은 채로 그 부분만 고치거나 확장할 수 있다.

바뀌는 부분과 그렇지 않은 부분 분리하기

fly()와 quack()은 오리 클래스에서 오리마다 달라진다.

두 메서드를 분리해서 각 행동을 나타내는 새로운 집합을 만든다.

디자인 원칙 2

구현이 아닌 인터페이스에 맞춰서 프로그래밍한다.

오리의 행동

FlyBehavior와 QuackBehavior라는 두 인터페이스와 구체적인 행동을 구현하는 클래스들을 만든다.

duck-Page-2

이런 식으로 디자인하면 다른 타입의 객체에서도 나는 행동과 꽥꽥거리는 행동을 재사용할 수 있다.

또한, 기존의 오리 클래스를 건드리지 않고도 새로운 행동을 추가할 수 있다.

오리 행동 통합하기

오리의 행동을 오리 클래스에서 구현하지 않고 다른 클래스에 위임한다.

duck-Page-3(1)

이런 식으로

class 오리 {
  // 모든 오리는 QuackBehavior 인터페이스를
  // 구현하는 것에 대한 레퍼런스를 가진다.
  quackBehavior: QuackBehavior;

  performQuack(): void {
    // 꽥꽥거리는 행동을 직접 처리하는 대신,
    // quackBehavior로 참조되는 객체에 그 행동을 위임한다.
    quackBehavior.quack();
  }
}

행동을 동적으로 지정하기

오리 클래스에 아래 메서드를 추가하고 필요에 따라 호출해서 행동을 바꾸면 된다.

setFlyBehavior(fb: FlyBehavior) {
  this.flyBehavior = fb;
}

setQuackBehavior(qb: QuackBehavior) {
  this.quackBehavior = qb;
}

구현

abstract class 오리 {
  // 행동 인터페이스에 대한 레퍼런스
  flyBehavior: FlyBehavior;
  quackBehavior: QuackBehavior;

  constructor() {}

  abstract display(): void;

  // 클래스에 행동을 위임
  performFly(): void {
    this.flyBehavior.fly();
  }

  performQuack(): void {
    this.quackBehavior.quack();
  }

  setFlyBehavior(fb: FlyBehavior) {
    this.flyBehavior = fb;
  }

  setQuackBehavior(qb: QuackBehavior) {
    this.quackBehavior = qb;
  }

  swim(): void {
    console.log("모든 오리는 물에 뜬다.");
  }
}

// 나는 행동
interface FlyBehavior {
  fly(): void;
}

class FlyWithWings implements FlyBehavior {
  fly(): void {
    console.log("날고 있어요");
  }
}

class FlyNoWay implements FlyBehavior {
  fly(): void {
    console.log("못 날아요");
  }
}

// 꽥꽥거리는 행동
interface QuackBehavior {
  quack(): void;
}

class Quack implements QuackBehavior {
  quack(): void {
    console.log("");
  }
}

class MuteQuack implements QuackBehavior {
  quack(): void {
    console.log("(조용..)");
  }
}

class Squeak implements QuackBehavior {
  quack(): void {
    console.log("");
  }
}

class 청둥오리 extends 오리 {
  constructor() {
    super();
    this.flyBehavior = new FlyWithWings();
    this.quackBehavior = new Quack();
  }

  display(): void {
    console.log("진짜 청둥오리");
  }
}

const 청둥 = new 청둥오리();

청둥.performFly(); // 날고 있어요
청둥.performQuack(); // 꽥

생각거리

  • 행동 메서드를 호출할 때 실행에 필요한 의존성(변수, 다른 메서드)은 어떻게 구하는가?

참고자료

  • Head First Design Patterns