-
디자인패턴 (1) - 전략패턴언어 2023. 9. 7. 16:38
1. 전략 패턴이란?
-> 객체가 할 수 있는 행위 분리 (인터페이스화)
-> 행위에 대한 구현체를 생성하고, 구체적 행위 정의 (캡슐화)
-> 추후 객체의 행위를 동적으로 바꾸고 싶은 경우 직접 행위를 수정하지 않고
전략(구현체)을 바꿔주기만 함으로써 행위를 유연하게 확장하는 방법을 말합니다.
- 여기서 가장 중요한 두가지 과정은 아래와 같다.
1. 기존 행위 중 변화할 수 있는 특정 행위를 분리 -> 인페이스화
2. 행위에 대한 구현객체와 구상관계 만들기
> 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 해준다.
전략패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있다.
2. 오리 예제
- 오리를 생성해야하는데, 상황에 따라 오리가 가지는 특성이 다르고, 이는 계속 변할 수 있다.
2.1 상속을 통해 해결해 보기
- Duck 클래스에 quack(),swim(),display()가 정의되어있다. (display()는 추상)
- 자식 클래스인 MallerdDuck,RedheadDuck는 display()구현하고 있다.
public abstract class Duck{ public void quack(){ //..꽥소리 } public void swim(){ //..수영중 } public void fly(){ //.. 날다 } public abstract void display() } public class MallerdDuck extends Duck { public void display(){ //mallerd의 생김새 } } public classs RedheadDuck extends Duck { public void display(){ //RedheadDuck의 생김새 } }
-> 이 상황에서 날 수 없는 오리와 소리 낼 수 없는 오리들이 추가된다고 해보자.
-> 문제는 둘 다 오리를 상속받기때문에 fly()나 quack()가 추가된다.
-> 물론! 오버라이딩을 통해 새로 생성된 오리들의 fly()나 quack()을 수정해줘도됨
-> 하지만 이는 매우 번거로운 일 (오리를 추가할 때 마다 바꿔줘야함)
* 상속은 코드 중복을 줄 일 수 있는 좋은 방법이지만,
하위 타입에 일괄 적용되어, 상속할 필요가 없는 부분까지하게 됨
2.2 인터페이스를 통해 해결해보기
public interface Flyable{ public void fly() } public interface Quackable{ public void quack() } public class MallerdDuck extends Duck implement Flyable,Quackable { public void display(){ //mallerd의 생김새 } public void fly(){ //날다 } public void quack(){ //울다 } } public classs RedheadDuck extends Duck implement Flyable,Quackable { public void display(){ //RedheadDuck의 생김새 } public void fly(){ //날다 } public void quack(){ //울다 } } public class RubberDuck extends Duck implement Quackable{ public void display(){ //RedheadDuck의 생김새 } public void quack(){ //울다 } }
- 기존 Duck에 있던, fly와 Quackable을 인터페이스로 분리하고
- 날 수 있는, 혹은 꽥 거릴 수 있는 클래스만 이를 구현하도록 만들었다.
--> 변할 수 있는 행위를 따로 빼고, 이를 각 자식 클래스가 책임을 가지도록 캡슐화 (좋은 시도)
--> 문제점 : 인터페이스를 구현할 때마다 세부내용을 바꿔줘야함
클라이언트가 Duck 변수로만 각 인스턴스를 담아서 쓰고 싶을 때 fly()와 quack()호출 못함
그렇다면, 클라이언트는 Duck이 아닌, 각 자식 클래스를 의존
* 변화하기 쉬운 행위를 인터페이스화 하는 시도는 좋았다! 여기서 조금만 더 나아가면 된다.
3. 오리예제에 디자인 패턴 적용
- fly와 Quack 행위를 인터페이스화 하되, 각 오리는이 이를 직접 구현하는 것이 아니라,
미리 구현된 fly와 quack 콘크리트 클래스를 의존하는 형태로 바꿔보자
(미리 구현된 전략 중 선택!)
public interface Fly{ public void fly() } public class FlyWithWings implements Fly{ public void fly(){ //날개로 날아요 } } public class FlyNoWay implements Fly{ public void fly(){ //못날아요 } } public interface Quackable{ public void quack() } public class Quack implements Quackable{ public void quack(){ //콱 } } public class Squack implements Quackable{ public void quack(){ //고무오리의 콱 } } public class Mute implements Quackable{ public void quack(){ //조용 } }
public class Duck { Flyable flyable; Quakable quackable; public void fly(){ flyable.fly(); } public void quack(){ quackable.quack() } } public class MallarDuck extends Duck{ public mallarDuck(){ quackable = new Quack(); flyable = new FlyWithWings(); } public void display(){ //생김새 } }
- duck은 변화하는 행위 (fly,quack)를 직접 구현하지 않고, 위임함 (책임분리)
4. 캡슐화된 행동 살펴보기
- 오리들의 행동을 일련의 행동으로 생각하는 대신 알고리즘으로 생각
- 클라이언트에서 자주바뀌는 행위인 나는행동과 꽥꽥행동은 캡슐화된(클래스) 알고리즘으로 구현
5. 두 클래스 합치는 방법
- "a에는 b가 있다" 라는 관계 -> 인스턴스 변수로 가지고 있는것
- 이를 구성이라고도 함
- 구성(의존)은 디자인 원칙에서 매우 중요함
*디자인 원칙 1 : 변화하는 부분을 찾아내서 분리한다.
달라지는 부분을 찾아서 나머지 코드에 영향을 주지 않도록 캡슐화한다.
캡슐화하면, 내부구현에 자유가 생김 -> 추후에 바뀌는 부분만 고치거나 확장 가능
*디자인 원칙 2: 상속보단 구성을 사용한다.
* 디자인 원칙 3: 구현보다는 인터페이스에 맞춰서 프로그래밍하라
* 인터페이스에 맞춰서 프로그래밍하라는 말은 변수 상위 타입으로 선언하여 다형성을 최대한 활용하라는 것
메서드를 호출하는 쪽은 구체 클래스에 대해서는 몰라도 됨
- 변화하는 것과 클라이언트의 의존관계는 하위 구현 클래스가 아닌
- 상위 인터페이스나 부모타입과 의존관계를 맺어야함을 의미
* 캡슐화 한다는 것은 내부구현을 감추고, 공용 인터페이스 호출만해서 사용할 수 있도록 바꾼다는 것
이를 통해 오류의 범위 축소, 자율성 확보(내부구현자유), 특정 데이터 감추기 가능
* 전략패턴은 특정 행위(책임)이 변화하기 쉬운 환경에서 적용할 수 있을 듯 하다.
* 인터페이스화 하는 것은 -> 특정 역할 (역할 수행자의 변화 가능성) ex DB
-> 특정 행동 (행동의 변화 가능성)
-> 변화할 수 있는 부분을 인터페이스화
-> 인터페이스에는 당연히 콘크리트 클래스가 따르고
-> 클래스를 만드는 과정에서 캡슐화 이점 가져갈 수 있음!
* 인터페이스로 분리된 행동은 구현될 수도 있고, 구상될 수도 있음
참고자료: 헤드퍼스트 디자인패턴