🔗 이 포스트에 대해 더 궁금하신가요? 다음 주소를 참고해주세요!
🗒️ 이 글의 수정 내역 (마지막 수정 일자: 없음)
draw
에도 전략 패턴을 적용하자.지난 글에서는 move
에 관한 여러 알고리즘들을 쉽게 교체할 수 있도록 전략 패턴을 이용했어요.
그런데 또 고민이 발생했답니다.
음... 그리는 로직도 결국 전략 패턴을 적용하면 어떨까?
이유는 다음과 같습니다.
현재는 아직 그리는 shape에 대해 딱시 생각하지 않아 기본적인 색상으로 완료한 상태입니다. 이런 상황에서 이미 그리는 색상 등을 배정시켜놓는 것은 유연성이 매우 떨어집니다.
수정 비용이 비쌉니다. 현재는 StaticMetaball
, DynamicMetaball
로 나눠져 있는데, 이러한 Shape
을 적용하기 위해서는 데코레이터 패턴을 사용해야 합니다. 이는 또 확장 및 상속을 진행해야 한다는 것인데, 확장에 대한 코드의 복잡성 대비 얻는 효과가 그렇게 큰 지 의문이었습니다. 또한, 다시 수정할 때의 로직 역시 전부 건드려야 하므로 비용이 비쌉니다.
전략 패턴은 이러한 단점 대비 상대적으로 유연하고, 전략만 추가하면 되는 형태이므로 비용이 싸죠. 또한, 결과물이 그렇게 큰 상태가 아닙니다. 그렇기에 전략패턴으로 리팩토링을 진행해보았습니다.
일단 기존 로직을 지워주죠!
export class DynamicMetaball implements Metaball {
// 기존 코드 생략 ...
// 기존 메서드 내 로직을 모두 복사 후 지운다.
draw() {}
}
이후에는 기존의 전략 추상 클래스 인터페이스에 맞춰 구현해줍시다.
간단하죠?
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
등도 인자로 받는 것이 더 좋습니다.
이는 추후 리팩토링을 하셔도 좋을 것 같아요!
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);
});
}
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();
}
});
}
}
export class DynamicMetaballsObserver implements MetaballsAnimationObserver {
// 기존 코드 생략...
updateDrawStrategy(drawStrategy: DrawStrategy) {
this.metaballs.setDrawStrategy(drawStrategy);
}
}
이렇게 옵저버까지 전달하기 위해서는 상단의 객체들 역시 메서드를 추가해줘야 해요.
그렇지만 기존 코드를 수정할 일은 전혀 발생하지 않죠.
즉, 확장에는 열려 있고, 기존 코드의 수정에는 닫혀 있으니 OOP로 잘 설계했다고 생각할 수 있어요 🥰
setDynamicMetaballDrawStrategy({
drawStrategy,
key,
}: IDynamicMetaballDrawStrategy) {
this.metaballAnimationSubject.notifyUpdateDrawStrategy({
drawStrategy,
key,
});
}
그러면 App에도 달아줄까요?
export class MetaballAnimation implements CanvasAnimation {
// 기존 코드 생략...
setDynamicMetaballDraw({drawStrategy, key}: IDynamicMetaballDrawStrategy) {
this.canvas.setDynamicMetaballDrawStrategy({drawStrategy, key});
}
}
생각보다 많이 복잡한 작업임에도 불구하고, 다음과 같은 결과가 나왔어요.
기존의 로직을 전혀 건드리지 않고도 충분히 리팩토링했고, 정상적으로 작동함을 확인했습니다!
꽤나 성공적인 리팩토링 경험이군요 유후!😉
전략 패턴은 사실 유연성을 가져다주기에 제 글에서는 무슨 만병통치약처럼 작성되었지만, 단점 역시 많습니다.
저와 같이 따라오면서 느끼셨을텐데, 초기 설정을 해줄 게 굉장히 많아지구요! 또 클래스를 생성하는 것 및 적용하는 데 콜백을 호출하는 데 있어 오버헤드 역시 증가합니다.
따라서 항상 모든 디자인 패턴을 적용할 때는 득과 실을 잘 적용하면서 개발하는 게 좋은 자세인 것 같아요. 🙇🏻♂️
벌써 우리, 이제 최적화 이전의 움직이는 로직까지 모두 구현을 완료했어요.
마지막으로 대망의 융합하는 로직을 구현할 때가 왔군요.
그렇다면, 다시 힘차게 달려보자구요. 이상! 🙆🏻♀️🙆🏻