전략 패턴
- 요구 사항 오리 시물레이션 게임에서 다음과 같은 클래스 설계 요청
- 이 게임에서는 오리는 꽥꽥 소리와 수영을 할 수 있다.
Duck.class
public abstract class Duck {
protected void quack(){
System.out.println("꽥꽥");
}
protected void swim() {
System.out.println("수영 할 수 있습니다.");
}
abstract void display();
}
- 추상 클래스를 사용하여, Duck 슈퍼 클래스 생성
MallardDuck.class
public class MallardDuck extends Duck {
@Override
public void display() {
quack();
swim();
System.out.println("MallardDuck 입니다.");
}
}
RedheadDuck.class
public class RedheadDuck extends Duck {
@Override
public void display() {
quack();
swim();
System.out.println("레드 해드 덕입니다.");
}
}
- 슈퍼 클래스를 상속받는 MallardDuck과 RedheadDuck 생성
- 임원진의 기획 변경 오리를 날 수 있게 변경해달라!
public abstract class Duck {
protected void quack(){
System.out.println("꽥꽥");
}
protected void swim() {
System.out.println("수영 할 수 있습니다.");
}
protected void fly() {
System.out.println("날 수 있습니다.");
}
abstract void display();
}
- 슈퍼 클래스에 fly() 메소드를 추가하여 모든 오리가 날 수 있게했다.
- 그러나 여기서 문제는 날지 말아야 할 오리들이 날라다니는게 문제였다.
- 또한 나무로 된 가짜 오리는 소리도 내지 말아야 하는데 소리를 내는 문제 발생
- 인터페이스 분리 원칙에 따라서 flyable과 Quackable 인터페이스 분리
public interface Flyable {
public void fly();
}
public interface Quackable {
public void quack();
}
- 하지만 이렇게 하면 코드를 재사용하지 않고 모두 Override를 해줘야 하는 문제로 인해 기존 코드에 많은 영향을 끼치게 된다.
- 인터페이스 분리 원칙으로 인해 무조건 이렇게 해야 한다고 생각했는데, 좋은 방법이 아니었다.
✏️ 디자인 원칙 첫번째 애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리한다.
- 바뀌는 부분은 따로 뽑아서 캡슐화하여 바뀌지 않는 부분에는 영향을 미치지 않고 그 부분만 확장 가능
✏️ 디자인 원칙 두번째 구현보다는 인터페이스에 맞춰서 프로그래밍 한다.
위 처럼 인터페이스에 너무 종속되는 클래스를 만드는 것이 아닌 인터페이스에 어떤 행위에 대해서 정의를 하여 실제 행동을 구현한다. 즉, 상위 형식에 맞춰서 프로그래밍 할 수 있도록 해야한다.
public interface FlyBehavior {
void fly();
}
public class FlyWithWings implements FlyBehavior {
@Override
void fly() {
System.out.println("날 수 있어요!");
}
}
public class FlyNoWay implements FlyBehavior {
@Override
void fly() {
System.out.println("날 수 없어요!");
}
}
- 다음과 같이 행동으로 구분하였다.
public abstract class Duck {
FlyBehavior flyBehavior;
public Duck() {}
public void performFly() {
flyBehavior.fly();
}
}
- 추상 클래스가 인터페이스에 맞춰서 주입
public class MallardDuck extends Duck {
public MallardDuck() {
flyBehavior = new FlyWithWings();
}
public void display() {
System.out.println("저는 물오리 입니다.");
}
}
- 이제 인터페이스에 맞는 구현체가 각각의 클래스에 맞게 구현이 될 것이고, 따로 추가하지 않더라도 구현이 가능하게 되었음.
- 지금까지 과정으로 Duck 클래스에서 정의한 메소드를 써서 구현하지 않고 다른 클래스에 위임하여 구현이 가능하게 되었음.
- 오리 행동을 통합하는 코드
Duck.class
public abstract class Duck {
QuackBehavior quackBehavior;
FlyBehavior flyBehavior;
public Duck(){}
public abstract void display();
public void performQuack(){
quackBehavior.quack();
}
public void performFly(){
flyBehavior.fly();
}
}
오리의 나는 행위
public interface FlyBehavior {
void fly();
}
---
public class FlyNoWay implements FlyBehavior{
@Override
public void fly() {
System.out.println("날 수 없습니다.");
}
}
---
public class FlyWithWings implements FlyBehavior {
@Override
public void fly() {
System.out.println("날 수 있습니다.");
}
}
---
오리의 울음 소리
public interface QuackBehavior {
void quack();
}
---
public class Quack implements QuackBehavior{
@Override
public void quack() {
System.out.println("꽥 소리를 냅니다.");
}
}
---
public class Squeak implements QuackBehavior{
@Override
public void quack() {
System.out.println("뀍 소리를 냅니다.");
}
}
---
public class MuteQuack implements QuackBehavior{
@Override
public void quack() {
System.out.println("소리 낼 수 없습니다.");
}
}
테스트 코드와 결과
@Test
void performDuck(){
Duck duck = new MallardDuck();
duck.display();
duck.performFly();
duck.performQuack();
}
저는 물오리입니다. 날 수 있습니다. 꽥 소리를 냅니다.
Duck 인터페이스에 quackBehavior와 flyBehavior를 performFly(), performQuack() 메소드에 담았다. 이와 같은 캡슐화를 사용하여 깔끔한 객체지향적인 코드, 그리고 구현체는 MallardDuck이지만 Duck 인터페이스를 사용함으로써 리스코프 치환 원칙을 지킬 수 있는 조은 객체지향적인 코드를 만들 수 있게 되었다.
- 동적으로 행동 지정하기
기존의 Duck.class 에 setter를 추가하여 동적으로 변경할 수 있도록 바꿔주자.
public abstract class Duck {
QuackBehavior quackBehavior;
FlyBehavior flyBehavior;
public Duck(){}
public abstract void display();
public void performQuack(){
quackBehavior.quack();
}
public void performFly(){
flyBehavior.fly();
}
public void setQuackBehavior(QuackBehavior qb){
quackBehavior = qb;
}
public void setflyBehavior(FlyBehavior fb){
flyBehavior = fb;
}
}
테스트 코드와 결과
@Test
void dynamicDuck(){
Duck duck = new MockDuck();
duck.display();
duck.performFly();
duck.performQuack();
// 여기서 날수있도록 동적으로 변경
duck.setFlyBehavior(new FlyRocketPowered());
duck.performFly();
}
모형의 오리입니다. 날 수 없습니다. 소리 낼 수 없습니다. 로켓을 사용하여 날 수 있습니다.
원래 모형의 오리여서 날 수 없었지만 중간에 동적으로 FlyBehavior에 주입되어있는 구현 객체를 변경해줌으로써 바꿀 수 있게 되었다.
✏️ 디자인 원칙 세번째 상속보다는 구성을 활용한다.
위 말이 잘 이해가 안 될 수 있는데, 여기서 말하는 구성은 다음과 같이 받아들여야 한다. A에는 B가 있다. 위에 예시를 보고 설명하면 오리에게는 나는 행동과, 꽥꽥 거리는 행동이 있다. 즉, Duck 클래스에 FlyBehavior 클래스, QuackBehavior 클래스가 있고, 이 인터페이스 들이 각각의 행동을 하기 위해 위임을 받는 것을 볼 수 있었다. 이 처럼 두 클래스를 합치는 것을 구성(composition)이라고 한다.
그러나 이렇게 중간에 구현체를 바꿀 수 있게 하는건 안티패턴이다. 처음에 생성할 때만 확실하게 구현 객체를 설정하고 중간에 바뀌지 않도록 하는 것이 중요하다.
이로써 전략패턴을 학습하게 되었고, 전략패턴의 정의는 다음과 같다.
✏️ 전략패턴(Strategy Pattern)은 행동에 따른 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 해준다. 전략 패턴을 이용해 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경이 가능하다.
cf) 알고리즘군 : 각 인터페이스에 구현된 행동 집합들을 말한다.
지금까지의 과정을 간단하게 정리해보자.
- 기존에 오리가 수영하고, 꽥꽥 소리가 나는 오리 시뮬레이션 게임이 있었다.
- 다른 회사와의 차별성을 두기 위해 기획자들이 오리가 날기까지 했으면 좋겠다고 요청했다.
- → 이 과정에서, 슈퍼 클래스에 fly() 라는 메소드를 추가하면서 Duck을 상속하는 모든 오리들이 나는 상황이 발생했다.
- 이후 개발자는 이러한 상황을 막기 위해서 인터페이스 분리 원칙에 의거하여 Flyable 이라는 인터페이스를 만들고 날 수 있는 오리들만 Flyable 인터페이스를 구현하도록 하였다.
- 그런데 이런 상황에서는 재사용을 하지 못하고 각각의 모든 날 수 있는 오리 구현체들에게 그에 맞게 Flyable을 구현해야 하는 상황이 오게 되었다. 물론 자바의 default 예약어를 사용하여 이제 인터페이스에도 구현을 할 수 있긴 하다.
- 하지만 그것보다는 더 좋은 방법을 떠올리기로 했다. 오리가 날고, 우는 행동을 인터페이스에 위임함으로써 그 인터페이스를 구현하는 구현체를 만들고, 날고 우는 행동에 대해서는 인터페이스에 추가하는 방식이다.
- FlyBehavior, QuackBehavior 라는 오리의 행동을 위임하는 인터페이스들을 만들고, Duck 추상 클래스에서 행동을 위임받은 이 인터페이스들에게 구현체를 통해 주입을 받고 행동 자체는 캡슐화하여 다형성을 만족시키는 추상 클래스를 만들게 됐다.
이렇게 클래스에 행동을 위임하는 클래스를 합치는 방식인 구성을 활용하여 유연성을 확장하였다.
전략 패턴
- 요구 사항 오리 시물레이션 게임에서 다음과 같은 클래스 설계 요청
- 이 게임에서는 오리는 꽥꽥 소리와 수영을 할 수 있다.
Duck.class
public abstract class Duck {
protected void quack(){
System.out.println("꽥꽥");
}
protected void swim() {
System.out.println("수영 할 수 있습니다.");
}
abstract void display();
}
- 추상 클래스를 사용하여, Duck 슈퍼 클래스 생성
MallardDuck.class
public class MallardDuck extends Duck {
@Override
public void display() {
quack();
swim();
System.out.println("MallardDuck 입니다.");
}
}
RedheadDuck.class
public class RedheadDuck extends Duck {
@Override
public void display() {
quack();
swim();
System.out.println("레드 해드 덕입니다.");
}
}
- 슈퍼 클래스를 상속받는 MallardDuck과 RedheadDuck 생성
- 임원진의 기획 변경 오리를 날 수 있게 변경해달라!
public abstract class Duck {
protected void quack(){
System.out.println("꽥꽥");
}
protected void swim() {
System.out.println("수영 할 수 있습니다.");
}
protected void fly() {
System.out.println("날 수 있습니다.");
}
abstract void display();
}
- 슈퍼 클래스에 fly() 메소드를 추가하여 모든 오리가 날 수 있게했다.
- 그러나 여기서 문제는 날지 말아야 할 오리들이 날라다니는게 문제였다.
- 또한 나무로 된 가짜 오리는 소리도 내지 말아야 하는데 소리를 내는 문제 발생
- 인터페이스 분리 원칙에 따라서 flyable과 Quackable 인터페이스 분리
public interface Flyable {
public void fly();
}
public interface Quackable {
public void quack();
}
- 하지만 이렇게 하면 코드를 재사용하지 않고 모두 Override를 해줘야 하는 문제로 인해 기존 코드에 많은 영향을 끼치게 된다.
- 인터페이스 분리 원칙으로 인해 무조건 이렇게 해야 한다고 생각했는데, 좋은 방법이 아니었다.
✏️ 디자인 원칙 첫번째 애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리한다.
- 바뀌는 부분은 따로 뽑아서 캡슐화하여 바뀌지 않는 부분에는 영향을 미치지 않고 그 부분만 확장 가능
✏️ 디자인 원칙 두번째 구현보다는 인터페이스에 맞춰서 프로그래밍 한다.
위 처럼 인터페이스에 너무 종속되는 클래스를 만드는 것이 아닌 인터페이스에 어떤 행위에 대해서 정의를 하여 실제 행동을 구현한다. 즉, 상위 형식에 맞춰서 프로그래밍 할 수 있도록 해야한다.
public interface FlyBehavior {
void fly();
}
public class FlyWithWings implements FlyBehavior {
@Override
void fly() {
System.out.println("날 수 있어요!");
}
}
public class FlyNoWay implements FlyBehavior {
@Override
void fly() {
System.out.println("날 수 없어요!");
}
}
- 다음과 같이 행동으로 구분하였다.
public abstract class Duck {
FlyBehavior flyBehavior;
public Duck() {}
public void performFly() {
flyBehavior.fly();
}
}
- 추상 클래스가 인터페이스에 맞춰서 주입
public class MallardDuck extends Duck {
public MallardDuck() {
flyBehavior = new FlyWithWings();
}
public void display() {
System.out.println("저는 물오리 입니다.");
}
}
- 이제 인터페이스에 맞는 구현체가 각각의 클래스에 맞게 구현이 될 것이고, 따로 추가하지 않더라도 구현이 가능하게 되었음.
- 지금까지 과정으로 Duck 클래스에서 정의한 메소드를 써서 구현하지 않고 다른 클래스에 위임하여 구현이 가능하게 되었음.
- 오리 행동을 통합하는 코드
Duck.class
public abstract class Duck {
QuackBehavior quackBehavior;
FlyBehavior flyBehavior;
public Duck(){}
public abstract void display();
public void performQuack(){
quackBehavior.quack();
}
public void performFly(){
flyBehavior.fly();
}
}
오리의 나는 행위
public interface FlyBehavior {
void fly();
}
---
public class FlyNoWay implements FlyBehavior{
@Override
public void fly() {
System.out.println("날 수 없습니다.");
}
}
---
public class FlyWithWings implements FlyBehavior {
@Override
public void fly() {
System.out.println("날 수 있습니다.");
}
}
---
오리의 울음 소리
public interface QuackBehavior {
void quack();
}
---
public class Quack implements QuackBehavior{
@Override
public void quack() {
System.out.println("꽥 소리를 냅니다.");
}
}
---
public class Squeak implements QuackBehavior{
@Override
public void quack() {
System.out.println("뀍 소리를 냅니다.");
}
}
---
public class MuteQuack implements QuackBehavior{
@Override
public void quack() {
System.out.println("소리 낼 수 없습니다.");
}
}
테스트 코드와 결과
@Test
void performDuck(){
Duck duck = new MallardDuck();
duck.display();
duck.performFly();
duck.performQuack();
}
저는 물오리입니다. 날 수 있습니다. 꽥 소리를 냅니다.
Duck 인터페이스에 quackBehavior와 flyBehavior를 performFly(), performQuack() 메소드에 담았다. 이와 같은 캡슐화를 사용하여 깔끔한 객체지향적인 코드, 그리고 구현체는 MallardDuck이지만 Duck 인터페이스를 사용함으로써 리스코프 치환 원칙을 지킬 수 있는 조은 객체지향적인 코드를 만들 수 있게 되었다.
- 동적으로 행동 지정하기
기존의 Duck.class 에 setter를 추가하여 동적으로 변경할 수 있도록 바꿔주자.
public abstract class Duck {
QuackBehavior quackBehavior;
FlyBehavior flyBehavior;
public Duck(){}
public abstract void display();
public void performQuack(){
quackBehavior.quack();
}
public void performFly(){
flyBehavior.fly();
}
public void setQuackBehavior(QuackBehavior qb){
quackBehavior = qb;
}
public void setflyBehavior(FlyBehavior fb){
flyBehavior = fb;
}
}
테스트 코드와 결과
@Test
void dynamicDuck(){
Duck duck = new MockDuck();
duck.display();
duck.performFly();
duck.performQuack();
// 여기서 날수있도록 동적으로 변경
duck.setFlyBehavior(new FlyRocketPowered());
duck.performFly();
}
모형의 오리입니다. 날 수 없습니다. 소리 낼 수 없습니다. 로켓을 사용하여 날 수 있습니다.
원래 모형의 오리여서 날 수 없었지만 중간에 동적으로 FlyBehavior에 주입되어있는 구현 객체를 변경해줌으로써 바꿀 수 있게 되었다.
✏️ 디자인 원칙 세번째 상속보다는 구성을 활용한다.
위 말이 잘 이해가 안 될 수 있는데, 여기서 말하는 구성은 다음과 같이 받아들여야 한다. A에는 B가 있다. 위에 예시를 보고 설명하면 오리에게는 나는 행동과, 꽥꽥 거리는 행동이 있다. 즉, Duck 클래스에 FlyBehavior 클래스, QuackBehavior 클래스가 있고, 이 인터페이스 들이 각각의 행동을 하기 위해 위임을 받는 것을 볼 수 있었다. 이 처럼 두 클래스를 합치는 것을 구성(composition)이라고 한다.
그러나 이렇게 중간에 구현체를 바꿀 수 있게 하는건 안티패턴이다. 처음에 생성할 때만 확실하게 구현 객체를 설정하고 중간에 바뀌지 않도록 하는 것이 중요하다.
이로써 전략패턴을 학습하게 되었고, 전략패턴의 정의는 다음과 같다.
✏️ 전략패턴(Strategy Pattern)은 행동에 따른 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 해준다. 전략 패턴을 이용해 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경이 가능하다.
cf) 알고리즘군 : 각 인터페이스에 구현된 행동 집합들을 말한다.
지금까지의 과정을 간단하게 정리해보자.
- 기존에 오리가 수영하고, 꽥꽥 소리가 나는 오리 시뮬레이션 게임이 있었다.
- 다른 회사와의 차별성을 두기 위해 기획자들이 오리가 날기까지 했으면 좋겠다고 요청했다.
- → 이 과정에서, 슈퍼 클래스에 fly() 라는 메소드를 추가하면서 Duck을 상속하는 모든 오리들이 나는 상황이 발생했다.
- 이후 개발자는 이러한 상황을 막기 위해서 인터페이스 분리 원칙에 의거하여 Flyable 이라는 인터페이스를 만들고 날 수 있는 오리들만 Flyable 인터페이스를 구현하도록 하였다.
- 그런데 이런 상황에서는 재사용을 하지 못하고 각각의 모든 날 수 있는 오리 구현체들에게 그에 맞게 Flyable을 구현해야 하는 상황이 오게 되었다. 물론 자바의 default 예약어를 사용하여 이제 인터페이스에도 구현을 할 수 있긴 하다.
- 하지만 그것보다는 더 좋은 방법을 떠올리기로 했다. 오리가 날고, 우는 행동을 인터페이스에 위임함으로써 그 인터페이스를 구현하는 구현체를 만들고, 날고 우는 행동에 대해서는 인터페이스에 추가하는 방식이다.
- FlyBehavior, QuackBehavior 라는 오리의 행동을 위임하는 인터페이스들을 만들고, Duck 추상 클래스에서 행동을 위임받은 이 인터페이스들에게 구현체를 통해 주입을 받고 행동 자체는 캡슐화하여 다형성을 만족시키는 추상 클래스를 만들게 됐다.
이렇게 클래스에 행동을 위임하는 클래스를 합치는 방식인 구성을 활용하여 유연성을 확장하였다.