전략 패턴 (Strategy Pattern)
오리의 행동 들을 쉽게 확장하거나 변경할 수 있는 클래스들의 집합으로 캡슐화하는 방법.
이를 통해 실행 중에도 확장과 변경이 가능해진다.
키워드
- 변하는 코드와 변하지 않는 코드의 분리
- 상위 형식 사용 (클래스 상속/인터페이스 구현)
용어
- 부모 클래스 = super class
- 자식 클래스 = sub class
- 베이스 클래스
: 다른 클래스로부터 상속받지 않는 클래스 - 오버라이드 (Override)
: 상속받은 메서드 덮어쓰기
부모 클래스와 자식 클래스 사이에서만 성립할 수 있다.
- 오버로딩 (Overload)
- : 함수명은 같게 하되, 파라미터 또는 리턴값을 다르게 함으로써, 같은 기능을 동일한 메서드 명으로 정의하는 방법
- 상속을 받은 자식 클래스는 오버라이드하지 않은 수퍼 클래스의 메서드일 지라도 사용할 수 있다.
public class Duck {
public void quack() {
System.out.println("꽥꽥");
}
public void swim() {
System.out.println("헤엄");
}
protected void display() {
System.out.println("나는 오리야");
}
public void fly() { // 새로 추가된 기능
System.out.println("하늘 날기");
}
}
public class MallardDuck extends Duck{
@Override
public void display() {
System.out.println("나는 청둥오리야");
}
}
public class Main {
public static void main(String[] args) {
MallardDuck mallardDuck = new MallardDuck();
mallardDuck.display();
mallardDuck.swim();
mallardDuck.quack();
mallardDuck.fly(); // 사용 가능. override하지 않은 수퍼 클래스의 메서드
}
}
나는 청둥오리야
헤엄
꽥꽥
하늘 날기
그렇기 때문에 상속은 바람직하지 않다.
fly()기능이 필요하지 않은 자식 클래스가 있을 수도 있는데, 수퍼 클래스에 fly() 기능을 추가해버렸기 때문에 모든 자식 클래스에는 fly() 기능이 생긴다.
즉 코드를 변경했을 때 다른 객체/클래스에 원치 않은 영향을 끼칠 수 있다.
+ 모든 Duck의 행동을 파악하기 힘들다.
디자인 원칙 1
애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리한다
💡 바뀌는 부분은 따로 뽑아서 캡슐화한다.
그러면 나중에 바뀌지 않는 부분에는 영향을 미치지 않고 그 부분만 고치거나 확장할 수 있다.
즉, 새로운 요구 사항이 있을 때마다 코드에 바뀌는 부분이 있다면 분리해야 한다.
매우 기초가 되는 원칙이다. 모든 디자인 패턴은 시스템의 일부분을 다른 부분과 분리하여, 독립적으로 변경시킬 수 있는 방법을 제공하기 때문이다.
디자인 원칙 2
구현보다는 인터페이스/상위 형식에 맞춰서 프로그래밍한다.
💡 바뀌는 부분(객체의 ‘행동’과 같은 구체적인 것들)은 인터페이스로 표현하고, 그 인터페이스를 이용해서 구현한다. 인터페이스를 구현한 별도의 클래스 안에서 구체적인 내용을 구현한다.
그렇게 하면 부모 클래스든 자식 클래스든 클래스 내에서 구체적인 내용을 직접 구현하지 않아도 된다.
즉 특정 구현에 의존하지 않을 수 있다.
→ 다른 형식의 객체에서도 바뀌는 부분을 가져다가 사용할 수 있게 된다. ( =바뀌는 부분을 다른 객체에서 재사용할 수 있게 된다.)
또한 객체를 변수에 대입할 때 상위 형식을 구체적으로 구현한 형식이라면 어떤 객체든 넣을 수 있게 된다.
public class Main {
public static void main(String[] args) {
Duck mallardDuck = new MallardDuck(); // 상위 형식에 맞춰서 객체 생성
mallardDuck.display();
mallardDuck.swim();
mallardDuck.quack();
mallardDuck.fly(); // 사용 가능. override하지 않은 수퍼 클래스의 메서드
}
}
나는 청둥오리야
헤엄
꽥꽥
하늘 날기
예시: 오리 코드
public abstract class Duck {
FlyBehavior flyBehavior; // 변경이 발생할 수 있는 부분을 의존성 주입
QuackBehavior quackBehavior; // 변경이 발생할 수 있는 부분을 의존성 주입
/**
* 변하는 부분
*/
public void performFly() {
flyBehavior.fly(); // 인터페이스/상위 타입
}
public void performQuack() {
quackBehavior.quack(); // 인터페이스/상위 타입
}
/**
* 변하지 않는 부분
*/
public void swim() { // 모든 오리는 swim이 동일하기 때문에 직접 구현.
System.out.println("헤엄");
}
public abstract void display(); // 자식 클래스에 따라 구현이 달라지기 때문에 추상 메소드.
}
public class MallardDuck extends Duck{
public MallardDuck(QuackBehavior quackBehavior, FlyBehavior flyBehavior) {
super.quackBehavior = quackBehavior;
super.flyBehavior = flyBehavior;
}
@Override
public void display() {
System.out.println("나는 청둥오리야");
}
}
public interface FlyBehavior {
public void fly();
}
public class FlyWithWings implements FlyBehavior {
@Override
public void fly() {
System.out.println("날개로 날기");
}
}
public class Main {
public static void main(String[] args) {
System.out.println("Hello world!");
QuackBehavior quack = new Quack(); // 상위 형식, 인터페이스에 맞춰서 프로그래밍 -> 변경 사항 대응에 용이함.
FlyBehavior fly = new FlyNoWay(); // [1]
// FlyBehavior fly = new FlyWithWings(); // [2] 이런식으로 갈아끼우기만 하면 됨.
Duck mallardDuck = new MallardDuck(quack, fly); // 상위 형식, 인터페이스에 맞춰서 프로그래밍
mallardDuck.display();
mallardDuck.swim();
mallardDuck.performQuack();
mallardDuck.performFly();
}
}
[1]: ‘날 수 없음’ 동작
[2]: ‘날 수 있음’ 동작
위의 경우 malladDuck의 날기 동작을 ‘날 수 없음’ 에서 ‘날 수 있음’으로 변경하고자 한다면 위처럼 fly 변수에 할당된 객체만 바꿔주면 된다.
인터페이스/상위 형식을 이용하면 변경 사항이 발생했을 때 갈아끼우기가 매우 용이하다.
동적으로 변경 사항을 반영할 때에는 setter를 통해 동적으로 반영한다.
public class Main {
public static void main(String[] args) {
System.out.println("Hello world!");
QuackBehavior quack = new Quack(); // 상위 형식, 인터페이스에 맞춰서 프로그래밍 -> 변경 사항 대응에 용이함.
FlyBehavior fly = new FlyNoWay(); // [1]
// FlyBehavior fly = new FlyWithWings(); // [2] 이런식으로 갈아끼우기만 하면 됨.
Duck mallardDuck = new MallardDuck(quack, fly); // 상위 형식, 인터페이스에 맞춰서 프로그래밍
mallardDuck.display();
mallardDuck.swim();
mallardDuck.performQuack();
mallardDuck.performFly();
mallardDuck.setFlyBehavior(new FlyWithWings()); // 동적으로 변화 반영하기
mallardDuck.performFly();
}
}
디자인 원칙 3
💡 상속보다는 구성(composition)을 활용한다.
상위 형식을 통해 상속받는 것보다는 의존성 주입을 활용하라는 것이다.
이렇게 하면 시스템 유연성을 향상시킬 수 있으며, 구성 요소로 사용되는 객체에서 인터페이스를 구현하기만 하면 동적으로 변경 사항을 반영할 수 있기 때문이다.
요약
- 변경이 발생하는 부분과 발생하지 않는 부분으로 구분하고 분리함으로써, 코드가 달라지더라도 나머지 코드에는 영향을 주지 않도록 ‘캡슐화’한다.
⇒ 코드를 변경하는 과정에서 의도치 않게 발생하는 변경(위험)을 줄이면서 시스템의 유연성을 향상시킬 수 있다. - 구현보다는 인터페이스/상위 형식을 활용해서 구현함으로써, 변경 사항이 발생해도 기존 코드를 변경시켜야 하는 상황이 발생하지 않도록(=변화에 유연하게) 만들 수 있다.
즉, 다형성을 활용해야 한다는 것이다. - 기능을 추가해야 할 때에는 직접 구현 또는 상속을 하는 것보다는 구성(의존성 주입)을 이용한다.