[2D 메타볼 애니메이션 구현] 7. 메타볼 그리는 로직 전략 패턴으로 리팩토링하기

young_pallete·2023년 3월 27일
0
post-thumbnail

🔗 이 포스트에 대해 더 궁금하신가요? 다음 주소를 참고해주세요!

🗒️ 이 글의 수정 내역 (마지막 수정 일자: 없음)

draw에도 전략 패턴을 적용하자.

지난 글에서는 move에 관한 여러 알고리즘들을 쉽게 교체할 수 있도록 전략 패턴을 이용했어요.

그런데 또 고민이 발생했답니다.

음... 그리는 로직도 결국 전략 패턴을 적용하면 어떨까?

이유는 다음과 같습니다.

  • 현재는 아직 그리는 shape에 대해 딱시 생각하지 않아 기본적인 색상으로 완료한 상태입니다. 이런 상황에서 이미 그리는 색상 등을 배정시켜놓는 것은 유연성이 매우 떨어집니다.

  • 수정 비용이 비쌉니다. 현재는 StaticMetaball, DynamicMetaball로 나눠져 있는데, 이러한 Shape을 적용하기 위해서는 데코레이터 패턴을 사용해야 합니다. 이는 또 확장 및 상속을 진행해야 한다는 것인데, 확장에 대한 코드의 복잡성 대비 얻는 효과가 그렇게 큰 지 의문이었습니다. 또한, 다시 수정할 때의 로직 역시 전부 건드려야 하므로 비용이 비쌉니다.

전략 패턴은 이러한 단점 대비 상대적으로 유연하고, 전략만 추가하면 되는 형태이므로 비용이 싸죠. 또한, 결과물이 그렇게 큰 상태가 아닙니다. 그렇기에 전략패턴으로 리팩토링을 진행해보았습니다.

Metaball.ts

일단 기존 로직을 지워주죠!

export class DynamicMetaball implements Metaball {
  
	// 기존 코드 생략 ...
  
    // 기존 메서드 내 로직을 모두 복사 후 지운다.
    draw() {}
}

Strategies.ts

이후에는 기존의 전략 추상 클래스 인터페이스에 맞춰 구현해줍시다.
간단하죠?


export class DrawStrategy implements Strategy {
  before?: (...args: unknown[]) => void;

  after?: (...args: unknown[]) => void;

  constructor() {}

  setBefore(callback: (...args: unknown[]) => void) {
    this.before = callback.bind(this);
  }

  setAfter(callback: (...args: unknown[]) => void) {
    this.after = callback.bind(this);
  }

  exec(metaball: DynamicMetaball) {
    this.before?.();

    const ctx = metaball.getCtx();

    const {x, y, r} = metaball;

    ctx.save();

    ctx.beginPath();

    ctx.fillStyle = '#f7f711';
    ctx.arc(x, y, r, 0, Math.PI * 2);
    ctx.fill();

    ctx.closePath();

    ctx.restore();

    this.after?.();
  }
}

사실 이 역시 fillStyle에 들어갈 color 등도 인자로 받는 것이 더 좋습니다.
이는 추후 리팩토링을 하셔도 좋을 것 같아요!

Metaballs.ts

draw에 관한 전략을 설정해줄 수 있도록 코드를 넣어줍시다!

export class DynamicMetaballs implements Metaballs<DynamicMetaball> {
  balls: DynamicMetaball[];

  constructor(
    public moveStrategy?: MoveStrategy,
    public drawStrategy?: DrawStrategy,
  ) {
    this.balls = [];
  }

  setDrawStrategy(drawStrategy: DrawStrategy) {
    this.drawStrategy = drawStrategy;
  }

  moveAll() {
    if (!this.moveStrategy || !this.drawStrategy) return;

    const {moveStrategy, drawStrategy} = this;

    /* eslint-disable-next-line no-console */
    this.balls.forEach(ball => {
      const move = moveStrategy.exec.bind(moveStrategy);
      const draw = drawStrategy.exec.bind(drawStrategy);

      move(ball);
      draw(ball);
    });
  }

AnimationSubject.ts

export class AnimationSubject implements Subject {
  // 기존 코드 생략...
  
  public notifyUpdateDrawStrategy({
    drawStrategy,
    key,
  }: IDynamicMetaballDrawStrategy): void {
    this.observers.forEach(observer => {
      if (observer.key === key) {
        (observer as DynamicMetaballsObserver).updateDrawStrategy(drawStrategy);
        observer.update();
      }
    });
  }
}

AnimationObserver.ts


export class DynamicMetaballsObserver implements MetaballsAnimationObserver {
  // 기존 코드 생략...
  
  updateDrawStrategy(drawStrategy: DrawStrategy) {
    this.metaballs.setDrawStrategy(drawStrategy);
  }
}

Canvas.ts

이렇게 옵저버까지 전달하기 위해서는 상단의 객체들 역시 메서드를 추가해줘야 해요.
그렇지만 기존 코드를 수정할 일은 전혀 발생하지 않죠.

즉, 확장에는 열려 있고, 기존 코드의 수정에는 닫혀 있으니 OOP로 잘 설계했다고 생각할 수 있어요 🥰

  setDynamicMetaballDrawStrategy({
    drawStrategy,
    key,
  }: IDynamicMetaballDrawStrategy) {
    this.metaballAnimationSubject.notifyUpdateDrawStrategy({
      drawStrategy,
      key,
    });
  }

App.ts

그러면 App에도 달아줄까요?


export class MetaballAnimation implements CanvasAnimation {
 	// 기존 코드 생략...
  
    setDynamicMetaballDraw({drawStrategy, key}: IDynamicMetaballDrawStrategy) {
    this.canvas.setDynamicMetaballDrawStrategy({drawStrategy, key});
  }
}

결과

생각보다 많이 복잡한 작업임에도 불구하고, 다음과 같은 결과가 나왔어요.
기존의 로직을 전혀 건드리지 않고도 충분히 리팩토링했고, 정상적으로 작동함을 확인했습니다!
꽤나 성공적인 리팩토링 경험이군요 유후!😉

🎉 마치며

전략 패턴은 사실 유연성을 가져다주기에 제 글에서는 무슨 만병통치약처럼 작성되었지만, 단점 역시 많습니다.

저와 같이 따라오면서 느끼셨을텐데, 초기 설정을 해줄 게 굉장히 많아지구요! 또 클래스를 생성하는 것 및 적용하는 데 콜백을 호출하는 데 있어 오버헤드 역시 증가합니다.

따라서 항상 모든 디자인 패턴을 적용할 때는 득과 실을 잘 적용하면서 개발하는 게 좋은 자세인 것 같아요. 🙇🏻‍♂️

벌써 우리, 이제 최적화 이전의 움직이는 로직까지 모두 구현을 완료했어요.
마지막으로 대망의 융합하는 로직을 구현할 때가 왔군요.
그렇다면, 다시 힘차게 달려보자구요. 이상! 🙆🏻‍♀️🙆🏻

profile
People are scared of falling to the bottom but born from there. What they've lost is nth. 😉

0개의 댓글