전략패턴 (Strategy Pattern )
headfirst 디자인 패턴책을 공부하고 정리한 것 입니다.
문제점
abstract class IronMan {
public fly():void{
console.log('fly');
}
public abstract showInfo():void;
}
class Mark2 extends IronMan{
public showInfo(){
console.log('i am mark2 suit');
}
}
추상 클래스 아이언맨을 상속받아 여러가지 서브 버전을 생성할 수 있습니다.
mark2 뿐만아니라 더 확장해 나갈 수 있져. 그러던 어느날 레이저 공격 기능을 추가하고 싶었습니다.
public lazerAttack():void{
console.log('lazerattack');
}
다음의 함수를 작성하고 추상클래스에 메소드를 새로 추가하였습니다.
그러나 문제가 생겼습니다. 레이저 공격을 하지 말아야 하는 마크버전에서도 쓸모없는 기능이 추가된 것이져.
레이저 공격을 하지 않는 서브 클래스에서 아무것도 하지 않도록 작성할 수 있습니다만...
- 서브클래스에서 코드가 중복
- 모든 서브클래스들의 행동을 알기 힘들다
- 코드 변경시 다른 서브 클래스에게 사이드 이펙트를 끼칠 수 있다.
인터페이스를 사용해서 해결해 볼까요
interface LazerAttack{
attack():void;
}
class Mark32 extends IronMan implements LazerAttack {
public showInfo(){
console.log('i am mark2 suit');
}
public attack():void{
console.log('lazer attack');
}
}
const mk32:Mark32 = new Mark32();
mk32.attack();
레이저 공격을 추상클래스에서 제거하고 인터페이스를 만들어 구현하고 싶은 서브클래스에 구현할 수 있습니다.
좋아보이긴 하지만 단점도 있습니다. 레이저 공격을 디테일하게 바꿔줘야 할 경우 (어떤 물질을 써야한다던가, 여러 방향으로 공격해야 한다던가) 레이저공격 인터페이스를 구현하는 모든 서브클래스를 전부 수정해햐 합니다.
이 코드에 대한 재사용을 기대할 수 없습니다.
이 문제에 대해서 상속을 사용하는것이 좋은 해답이라고 볼 수 없습니다. 서브 클래스 마다 구현이 달라야 할 경우말이져.
인터페이스 또한 좋아보이지만 인터페이스에는 구현을 할 수 없기 때문에 재사용을 기대하긴 어렵습니다. 인터페이스 사용의 경우 한
행동을 바꿀 때 마다 그 행동이 구현되어있는 모든 서브클래스에 코드를 수정해야 합니다.
이상황에 어울리는 디자인 원칙이 있습니다.
달라지는 부분을 찾아내어 분리시키고 캡슐화를 시킨다.
새로운 요구사항이 올 때마다 변경해야 하는 부분이 있다면 골라내서 분리해야 합니다. 분리하고 캡슐화 하면 나중에 바뀌지 않는 부분에 사이드이펙트를 끼치지 않고 고치거나 확장이 가능합니다.
해결
레이저 공격및 앞으로 추가될 신규 기능들은 여러 아이먼맨 마크버전마다 달라지는 부분입니다. 변경되는 부분을 분리하여 클래스 집합을 만들어야 합니다.
이런 행동 클래스 집합들은 유연하게 작성하고 IronMan 인스턴스에 기능을 set 할 수 있어야 합니다. 이렇게 작성하면 어떤 새로운 기능이던 동적으로 바꿀 수 있습니다. 디자인 원칙 두번째로
구현이 아닌 인터페이스에 맞춰서 프로그래밍 한다.
인터페이스에 맞춰 프로그래밍 한다는 것은 상위 형식에 맞춰서 프로그래밍 한다라는 것입니다.
가장 핵심은 실제 실행시 쓰이는 객체인스턴스가 코드에 의해서 고정되지 않도록 즉 다형성을 이용하는 것입니다.
즉 변수선언시 추상클래스나 인터페이스 같은 상위 타입으로 선언해야 하고 이렇게 작성할 시 상위형식을 구체적으로 구현한 어떤 객체든 변수에 대입하 수 있기 때문입니다.
class Animal{
public sound():void{}
}
class Dog extends Animal{
}
const animal: Animal = new Dog();
animal.sound();
전략패턴 구현
말이 많았습니다. 실제로 구현해봅시다.우선 아이언맨 클래스에 두가지 기능을 추가해 보겠습니다. 방패기능과 레이저공격기능을요 우선 레이저 공격기능입니다 인터페이스를 선언하고 서브클래스로 구현합니다.
interface LazerAttack{
attack():void;
}
class LazerDefaultAttack implements LazerAttack{
public attack():void{
console.log('lazer default attack');
}
}
class LazerMultiAttack implements LazerAttack{
public attack():void{
console.log('lazer multi attack');
}
}
방패기능도 똑같습니다.
interface Shield{
shield():void;
}
class ShieldNano implements Shield{
public shield():void{
console.log('on shield nano')
}
}
class ShieldVibranium implements Shield{
public shield():void{
console.log('on shield vibranium')
}
}
먼저 아이언맨 추상클래스입니다.
abstract class IronMan {
private lazerAttack: LazerAttack;
private sheild: Shield;
constructor(l : LazerAttack, s: Shield){
this.lazerAttack = l;
this.sheild = s;
}
public fly():void{
console.log('fly');
}
// 동적으로 레이저 행동 클래스를 set하기 위한 함수
public setLazerAttackType(l : LazerAttack){
this.lazerAttack = l;
}
// 동적으로 방패 행동 클래스를 set하기 위한 함수
public setShieldType(s: Shield){
this.sheild = s;
}
// 레이저공격 클래스 행동을 위임한다.
public onLazerAttack():void{
this.lazerAttack.attack();
}
// 방패클래스 행동을 위임한다.
public onShield():void{
this.sheild.shield();
}
public abstract showInfo():void;
}
임의의 마크 36 버젼을 구현해봅시다. 마크36은 나노쉴드와 기본 레이저 공격이 가능합니다.
class Mark36 extends IronMan{
constructor(l : LazerAttack, s: Shield){
super(l,s);
}
public showInfo():void{
// code
}
}
const mk36 : IronMan = new Mark36(new LazerDefaultAttack(), new ShieldNano());
mk36.onLazerAttack(); // "lazer default attack"
mk36.onShield(); // "on shield nano"
그러던 중 마크36에 비브라늄 쉴드와 여러명을 공격할 수 있는 레이저 공격을 셋팅하고 싶습니다.
이렇게 동적으로 추가 가능합니다.
const mk36 : IronMan = new Mark36(new LazerDefaultAttack(), new ShieldNano());
mk36.onLazerAttack(); // "lazer default attack"
mk36.onShield(); // "on shield nano"
mk36.setLazerAttackType(new LazerMultiAttack());
mk36.setShieldType(new ShieldVibranium());
mk36.onLazerAttack(); // "lazer multi attack"
mk36.onShield(); // "on shield vibranium"
이를 UML 클래스 다이어그램으로 한눈에 알아보면 정말 쉽습니다.
이상 전략패턴에 대해 알아보았습니다.
틀린부분이나 지적할 부분 있음 말씀해주심 감사하겠습니다.