💡 애플리케이션에서 새로운 요구사항이 있을 때 마다 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리한다.
즉, 달라지는 부분을 찾아서 나머지 코드에 영향을 주지 않도록 ‘캡슐화’한다.
이렇게 하면 나중에 바뀌지 않는 부분에는 영향을 미치지 않고 그 부분만 고치거나 확장할 수 있다.
위 개념은 매우 간단하지만 다른 모든 디자인 패턴의 기반을 이루는 원칙이다.
결국, 모든 패턴은 ‘시스템의 일부분을 다른 부분과 독릭접으로 변화시킬 수 있는’ 방법들을 제공한다.
💡 구현보다는 인터페이스에 맞춰서 프로그래밍 한다. = 상위 형식에 맞춰서 프로그래밍한다
실제 실행 시에 쓰이는 객체가 코드에 고정되지 않도록, 상위 형식(supertype)에 맞춰 프로그래밍해서 다형성을 활용해야 한다.
- 변수를 선언할 때 보통 추상 클래스나 인터페이스 같은 상위 형식으로 선언해야 한다.
- 객체를 변수에 대입할 때 상위 형식을 구체적으로 구현한 형식이라면 어떤 객체든 넣을 수 있기 때문이다.
- 그러면 변수를 선언하는 클래스에서 실제 객체의 형식을 몰라도 된다.
💡 구성(composition)을 이용한다 = 행동을 상속받는 대신, 올바른 행동 객체로 구성되어 행동을 부여받는다.
서브클래스를 만드는 방식으로 행동을 상속받으면 그 행동은 컴파일할 때 완전히 결정되며,
모든 서브클래스에서 똑같은 행동을 상속받아야 한다. (즉, 유연성이 떨어진다)
하지만 구성으로 객체의 행동을 확장하면 실행 중에 동적으로 행동을 설정할 수 있게 된다.
- 객체를 동적으로 구성하면 기존 코드를 고치는 대신 새로운 코드를 만들어서 기능을 추가할 수 있다.
- 즉, 기존 코드를 건드리지 않으므로 코드 수정에 따른 버그나 의도하지 않은 부작용을 원천봉쇄할 수 있다.
e.g,.
각 오리들에는 나는 행동과 꽥꽥거리는 행동이 존재한다. (A에는 B가 있다)
= 오리 각각은 나는 행동과 꽥꽥거리는 행동을 위임 받는다.
따라서 아래의 클래스 다이어그램처럼, 오리 객체(client)는 구성을 이용하여 행동을 부여받을 수 있다.
즉, 오리 객체의 나는 행동과 꽥꽥거리는 행동을 인터페이스를 통해 동적으로 구현하여 설정할 수 있는 것이다.
💡 전략 패턴(strategy pattern) 또는 정책 패턴(policy pattern)은 실행 중에 알고리즘을 선택할 수 있게 하는 행위 소프트웨어 디자인 패턴이다.
전략 패턴은 특정한 계열의 알고리즘들을 정의하고 각 알고리즘을 캡슐화하며 이 알고리즘들을 해당 계열 안에서 상호 교체가 가능하게 만든다.
간단하게 정리하면, 변경 가능성이 큰 부분을 분리하고, 인터페이스를 이용하여 구현체로 캡슐화하여 실행 중 해당 구현체만 setter 등의 메서드를 통해 상호 교체 가능하게 만드는 것이다.
여러 오리(러버덕(장난감),청둥 오리 등등)를 구현한다고 가정해보자.
별생각 없이 설계한다면, 그저 Duck abstract class를 생성하고, 아래 러버덕과 청둥 오리가 이를 상속받게 구현할 것이다.
그러나 구현해야할 오리가 더 늘어난다면 어떻게 될까?
fly() 등의 행위가 같은 Duck들을 구현 해야한다거나 fly()의 요구사항이 바뀌게 된다면 어떻게 될까?
필히 코드의 반복이 늘어날 것이고, 변경사항이 있을 때마다 모든 하위의 Duck 클래스들을 살펴보아야만 할 것이다.
즉, 이는 적절한 설계가 아니다.
위 첫번째 디자인 원칙에 따라 변경 가능한 부분인 fly()를 분리하고, 이를 캡슐화해보자. 또 두번째 디자인 원칙인 인터페이스를 이용하여 이들을 캡슐화 해보자.
이렇게 fly()가 구현된다면, 이후 Duck abstract class에서 다음과 같은 코드로 fly()를 사용할 수 있게 된다. 당연히 fly() override()는 지워야할 것이다.
public abstract class Duck {
FlyBehavior flyBehavior;
public void performFly() {
flyBehavior.fly();
}
public void swim() {
}
public void display() {
}
//전략 패턴을 위한 추가.
//setter를 통해 구현체만 바꿔줌으로써 다른 알고리즘을 수행할 수 있게 된다.
public void setFlyBehavior(FlyBehavior fb) {
this.flyBehavior = fb;
}
// public void fly(){
// // RubberDuck은 날지 못한다. 상속 구현체에서 fly를 override하는 것이 맞는걸까?
// // 또 다른 구현체가 생겼을 때 일일히 따져 주어야만 하기 때문에 아님.
// // -> 인터페이스를 만들자.
// }
}
public class RubberDuck extends Duck {
public RubberDuck() {
this.flyBehavior = new FlyNoWay();
}
}
Duck에서 flyBehavior를 인스턴스 변수로 사용하여, 3번째 디자인 원칙인 ‘상속보다는 구성을 사용한다.’ 가 지켜진 것도 볼 수 있다.
또 FlyBehavior의 알고리즘군을 교체할 수 있게끔 하기위하여 setter를 추가한 것도 확인할 수 있다.
이렇게 3가지 디자인 원칙을 지켰고,
전략패턴을 사용함으로써 Duck을 새로 만들때, 각 구현체만 지정해주면 구현체는 Duck 서브 클래스에 국한되지 않으므로 코드가 엄청나게 유연해진 것을 볼 수 있다.
(이렇게 하면 RubberDuck이 FlyNoWay 클래스에 의존하게 되어 완벽히 좋은 구현이라고 할 수는 없다.
그러나, setter를 통해 의존을 따로 변경할 수 있으므로 우선 상당히 유연하다고 할 수 있고 후에 해당 문제를 해결할 만한 패턴을 소개할 예정이다.)
새로운 전략이 추가, 수정되어도 컨텍스트의 코드 변경 없이도 새로운 전략을 추가, 수정할 수 있으므로 코드의 변경에 매우 유연하다.
즉, OCP 원칙을 지킬 수 있다.
코드의 복잡성이 증가한다.
즉, 전략의 변경 여지가 없고, 전략의 개수가 하나 혹은 두개 정도일 때는 전략 패턴의 사용이 오히려 코드만 복잡하게 만들 수도 있다.
전략 패턴은 추상화 기법이다. 변경의 여지가 없다고 해서 무조건 사용하지 않는 것이 아니다.
예를 들어, 랜덤 로직에 대한 테스트를 실행할 때 테스트에서는 당연히 랜덤한 수에 대한 테스트가 아니라 정해진 수에 대한 테스트가 필요하고,
랜덤 전략을 정해진 수를 반환하는 전략으로 수정할 수 있어야한다. 전략을 수정하는 방법은 위에서 보았듯이 전략 패턴을 사용하는 것이다.
즉, 변경 여지가 없다고 해서 맹목적으로 구현체만을 사용해서도 안된다. 추상화를 통한 이점을 충분히 파악하고 사용을 고려하는 자세를 갖는 것이 중요하다.