flagtags / OOP-study

0 stars 0 forks source link

1장 - 전략패턴 #9

Open j03y14 opened 6 months ago

j03y14 commented 6 months ago

@kkirico

디자인패턴은 새로 파지말고 여기 이어서 하자. 라벨 붙여놨음.

j03y14 commented 6 months ago

오리 시뮬레이션 게임

class Duck {
  quack()
  swim()
  display()
}

class MallarDuck extends Duck {
  display()
}

class RedheadDuck extends Duck {
  display()
}

이렇게 되어있었음.

그런데 새로운 요구사항이 등장.

오리가 날아야 한다.

=> 처리1. Duck 클래스에 fly 메서드만 추가하면 될듯?

class Duck {
  quack()
  swim()
  display()
  fly()
}

class MallarDuck extends Duck {
  display()
}

class RedheadDuck extends Duck {
  display()
}

그런데 고무 오리들도 날아다닌다. (자식 중에 일부에만 추가가 되어야 하는 상황)

class Duck {
  quack()
  swim()
  display()
  fly()
}

class MallarDuck extends Duck {
  display()
}

class RedheadDuck extends Duck {
  display()
}

class RubberDuck extends Duck {
  display()
  fly() {
    return; // 아무것도 하지 않도록 오버라이드
  }
}

이게 맞나..?

=> 처리2. Flyable, Quackable 인터페이스를 만들어서 처리한다.

interface Flyable {
  fly(): void;
}

interface Quackable {
  quack(): void;
}

class Duck {
  quack() {}
  swim() {}
  display() {}
  fly() {}
}

class RedheadDuck extends Duck implements Flyable, Quackable {
  display() {}
  fly() {}
  quack() {}
}

class MallarDuck extends Duck implements Flyable, Quackable {
  display() {}
  fly() {}
  quack() {}
}

class RubberDuck extends Duck implements Quackable {
  quack() {}
}

class DecoyDuck extends Duck {

}

이러면 코드 중복이 발생한다.


❗ 디자인 원칙1: 어플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리한다.

바뀌는 부분과 안 바뀌는 부분 분리하기

fly, quack을 제외하고는 바뀌지 않고 있다.

변화하는 부분을 분리해서 나는 것, 꽥꽥거리는 것을 나타내는 클래스를 만들어야 한다.

❗ 디자인 원칙2: 구현보다는 인터페이스에 맞춰서 프로그래밍한다.

각 행동을 인터페이스로 표현하고, 인터페이스를 구현한 클래스에서 행동을 구현한다. Duck 클래스에서 행동을 구현한 방법은 행동을 변경할 여지가 없었지만 이제 행동을 변경할 여지가 있다.


interface FlyBehavior7 {
  fly(): void;
}

class FlyWithWings implements FlyBehavior7 {
  fly() {}
}

class FlyNoWay implements FlyBehavior7 {
  fly() {}
}

interface QuackBehavior7 {
  quack(): void;
}

class Quack implements QuackBehavior7 {
  quack() {}
}

class Squeak implements QuackBehavior7 {
  quack() {}
}

class MuteQuack implements QuackBehavior7 {
  quack() {}
}

abstract class Duck6 {
  flyBehavior: FlyBehavior7;

  quackBehavior: QuackBehavior7;

  swim() {}
  display() {}

  // ? 책에서는 performQuack으로 되어있는데 왜 네이밍을 바꿔서 이렇게 했을까?
  quack() { 
    this.quackBehavior.quack();
  }
  fly() {
    this.flyBehavior.fly();
  }
}

class RedheadDuck6 extends Duck6 {
  constructor() {
    super();
    this.flyBehavior = new FlyWithWings();
    this.quackBehavior = new Quack();
  }

  display() {}
}

class MallardDuck6 extends Duck6 {
  constructor() {
    super();
    this.flyBehavior = new FlyWithWings();
    this.quackBehavior = new Quack();
  }
  display() {}
}

class RubberDuck6 extends Duck6 {
  constructor() {
    super();
    this.flyBehavior = new FlyNoWay();
    this.quackBehavior = new Squeak();
  }
}

class DecoyDuck6 extends Duck6 {
  constructor() {
    super();
    this.flyBehavior = new FlyNoWay();
    this.quackBehavior = new MuteQuack();
  }
}

동적으로 행동을 변경하고 싶다.

// 문제: 동적으로 행동을 변경하고 싶다.

// 대응: setter를 통해 행동을 변경할 수 있도록 한다.

interface FlyBehavior7 {
  fly(): void;
}

class FlyWithWings7 implements FlyBehavior7 {
  fly() {}
}

class FlyNoWay7 implements FlyBehavior7 {
  fly() {}
}

interface QuackBehavior7 {
  quack(): void;
}

class Quack7 implements QuackBehavior7 {
  quack() {}
}

class Squeak7 implements QuackBehavior7 {
  quack() {}
}

class MuteQuack7 implements QuackBehavior7 {
  quack() {}
}

abstract class Duck7 {
  flyBehavior: FlyBehavior7;

  quackBehavior: QuackBehavior7;

  swim() {}
  display() {}

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

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

  // ? 책에서는 performQuack으로 되어있는데 왜 네이밍을 바꿔서 이렇게 했을까?
  quack() { 
    this.quackBehavior.quack();
  }
  fly() {
    this.flyBehavior.fly();
  }
}

class RedheadDuck7 extends Duck7 {
  constructor() {
    super();
    this.flyBehavior = new FlyWithWings();
    this.quackBehavior = new Quack();
  }

  display() {}
}

class MallardDuck7 extends Duck7 {
  constructor() {
    super();
    this.flyBehavior = new FlyWithWings();
    this.quackBehavior = new Quack();
  }
  display() {}
}

class RubberDuck7 extends Duck7 {
  constructor() {
    super();
    this.flyBehavior = new FlyNoWay();
    this.quackBehavior = new Squeak();
  }
}

class DecoyDuck7 extends Duck7 {
  constructor() {
    super();
    this.flyBehavior = new FlyNoWay();
    this.quackBehavior = new MuteQuack();
  }
}

function main() {
  const mallard = new MallardDuck7();
  mallard.setFlyBehavior(new FlyWithWings());
  mallard.setQuackBehavior(new Quack());
  mallard.fly();
  mallard.quack();
}

이 때 행동을 알고리즘군으로 생각하면 오리가 하는 행동이 아니라 세금 계산 방식을 구현하는 클래스 등 다른 경우에도 사용할 수 있다는걸 알 수 있다.

Duck과 FlyBehaviour, QuackBehavior는 "A에는 B가 있다"의 관계이다. Duck은 각 행동을 FlyBehaviour, QuackBehaviour에 위임한다.

이런식으로 클래스를 합치는 것을 composition을 사용한다고 한다.

❗디자인 원칙3: 상속보다는 구성을 사용한다.

전략패턴

알고리즘군을 정의하고 캡슐화 해서 각각의 알고리즘군을 수정해서 쓸 수 있게 한다. 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있다.


참고

Duck의 행동을 상속할 때 생길 수 있는 단점?

QuackDuck과 FlyDuck이라고 Duck을 상속해서 만들면 문제점이 뭘까?

kkirico commented 5 months ago

오리 문제

class Duck {
  quack()
  swim()
  display()
  fly()
}

class MallarDuck extends Duck {
  display()
}

class RedheadDuck extends Duck {
  display()
}

문제점: 고무 오리를 추가하면, 고무오리도 날게된다

-> fly가 아무것도 하지않도록 오버라이드 해서 해결?

class RubberDuck {
  quack() {}
  display() {}
  fly() {
    // do nothing
  }
}

-> 단점:

  1. 앞으로 생기게 될 모든 종류의 오리의 행동을 알 수 없다.
  2. 서브 클래스에 코드가 중복된다
  3. 실행 중에 행동을 바꿀 수 없다.
  4. 코드를 변경하면 다른 모든 오리에게 영향을 줄 수 있다.

모든 서브클래스에 날거나 꽥꽥 거리는 기능이 있어야하므로, 상속이 올바른 방법이 아니다. 애플리케이션의 달리지는 부분을 찾아내고, 달라지는 부분과 그렇지 않은 부분을 분리해야한다.

이 경우, 행동을 별도의 클래스에서 구현합니다. 행동을 Duck 클래스나 그 서브클래스에서 정의하지 않고, 메소드를 써서 다른 클래스에 위임하자

public abstract class Duck {
  FlyBehavior flyBehavior;
  QuackBehavior quackBehavior;

  public Duck() { }

  public abstract void display();

  public void performFly() {
    flyBehavior.fly();
  }

  public void performQuack() {
    quackBehavior.quack();
  }
}
public class MallardDuck extends Duck {
  public MallardDuck() {
    quackBehavior = new Quack();
    flyBehavior = new FlyWithWings();
  }

  public void display() { ... }
}

동적으로 행동 지정하기

Duck 클래스에 set메소드 추가해서 사용하면 가능하빈다!

결론

오리의 행동을 알고리즘 군으로 생각한다.(날기 -> 행동이 아니라 고도나 속도 등의 정보를 포함하는 객체가 될 수 있음) 두 클래스를 함께 사용하는 것을 구성(composition)이라고 합니다.

전략 패턴은 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 해줍니다. 전략 패턴을 통해 클라이언트로 부터 알고리즘을 분리해서 독립적으로 사용할 수 있습니다.