스트래티지 패턴이란?
애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분으로부터
분리시켜서 캡슐화 하는 것
- 나중에 바뀌지 않는 부분에는 영향을 미치지 않으면서 바뀌는 부분만 고치거나 확장가능
- 바뀌는 부분을 뽑아내서 구현하는 클래스 집합은 유연성있게 디자인 하고 행동들을
동적으로 바꿀 수 있도록 세터 메소드도 포함하는 것이 좋다.
구현이 아닌 인터페이스에 맞춰서 구현
상위 형식에 맞춰서 프로그래밍 한다는 것을 뜻하는 것으로 꼭 자바의 인터페이스를
사용하라는 것이 아니라 인터페이스라는 개념을 지칭한다. 실제 실행시에 쓰이는 객체가
코드에 의해서 고정되지 않도록 어떤 상위형식에 맞춰서 프로그래밍 함으로써 다형성을
활용 해야 한다는 것이다. 상위형식에 맞춰서 프로그래밍 한다는 원칙은 변수를 선언할 때
보통 추상클래스나 인터페이스 같은 상위형식으로 선언해야한다. 그렇게 할 경우 객체를
변수에 대입할 때 어떤 형식이든 집어넣을 수 있다.
다형적인 형식을 사용하는 간단한 예
Animal 이라는 추상클래스가 있고 그 밑에 Dog와 Cat이라는 구상클래스가 있다고 가정
구현에 맞춰서 프로그래밍 할 경우
Dog d = new Dog();
d.bark();
변수 “d"를 Dog형식(Animal을 확장한 구상클래스)으로 선언하면
어떤 구체적인 구현에 맞춰서 코딩해야 함
인터페이스/상위 형식에 맞춰서 프로그래밍 할 경우
Animal animal = new Dog();
animal.makeSound();
Dog라는 걸 알고 있긴 하지만 다형성을 활용하여 Animal에 대한 레퍼런스를 사용
상속보다는 구성을 활용
구성이란 "A에는 B가 있다" 라는 관계로 설명 할 수 있으며 상위 클래스에서 여러 개의
인터페이스 형식의 인스턴스변수를 추가하여 합치는 것을 구성(composition)을 이용하는 것이라고 한다. 구성은 단순히 알고리즘군을 별도의 클래스의 집합으로 캡슐화할 수 있도록 만들어주는 것 뿐 아니라 구성요소로 사용하는 객체에서 올바른 행동 인터페이스를 구현하기만 하면 실행시에 행동을 바꿀 수도 있게 해준다.
예를 들면
Duck 이라는 클래스와
FlyBehavior, QuackBehavior라는 인터페이스가 있고
FlyWithWings, FlyNoWay는 FlyBehavior를 인터페이스로 하는 클래스라고 할 때
Duck 클래스는
FlyBehavior flyBehavior
QuackBehavior quackBehavior
2개의 인터페이스 형식의 변수를 선언 함으로서
FlyBehavior과 QuackBehavior 인터페이스를 캡슐화와 구성으로 사용 하고 있다.
출처 : Head First Design Patterns
디자인 패턴의 두번째! 옵저버 패턴(Observer Pattern)
옵저버 패턴이란?
한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고
자동으로 내용이 갱신되는 방식으로 일대다(one-to-many)의 의존성을 정의
옵저버 패턴의 요소
주제(subject) 객체 - 데이터가 바뀌면 옵저버 객체들에게 전달,
옵저버가 될 객체의 등록 및 제거
옵저버(observer) 객체 - 주제의 데이터가 바뀌면 갱신 내용을 전달 받음
일대다 관계는 주제와 옵저버에 의해 정의되고 옵저버는 주제에 의존됨.
느슨한 결합(Loose Coupling)
느슨하게 결합되어 있다는 것은 그 둘이 상호작용을 하긴 하지만 서로에 대해
잘 모른다는 것을 의미하며 서로 상호작용을 하는 객체 사이에서는 가능하면
느슨하게 결합하는 디자인을 사용해야 함
- 느슨하게 결합하는 디자인을 사용하면 변경사항이 생겨도 무난하게 처리할 수 있는
유연한 객체지향 시스템을 구축할 수 있다(객체 사이의 상호의존성을 최소화 할 수 있기 때문)
옵저버 패턴에서는 주제와 옵저버가 느슨하게 결합되고 있는
객체 디자인을 제공
- 주제가 옵저버에 대해 아는것은 옵저버가 특정 인터페이스를 구현 한다는 것 뿐
- 옵저버는 언제든지 새로 추가 가능
- 새로운 형식의 옵저버를 추가하려고 할 때도 주제를 변경할 필요가 전혀 없음
- 주제와 옵저버는 서로 독립적으로 재사용 할 수 있음
- 주제나 옵저버가 바뀌더라도 서로에게 영향을 미치지 않음
<옵저버 패턴 클래스 다이어그램>
자바 내장 옵저버 패턴
- Observer 인터페이스와 Observable 클래스로 구현되어 있음
- 많은 기능들을 제공하고 푸시(Push)방식과 풀(Pull)방식 사용 가능
- 푸시방식은 데이터를 메소드의 인자로 전달하는 데이터 객체 형태로 전달
- 풀방식은 옵저버에서 전달받은 Observable 객체로부터 원하는 데이터를 가져가는 방식
자바 내장 옵저버 패턴의 단점
- Observable은 클래스
- 서브 클래스를 만들어야 됨(재사용성에 제약 생김)
- Observable 인터페이스라는 것이 없기 때문에 자바에 내장된 Observer API하고
잘 맞는 클래스를 직접 구현한다는 것이 불가능
- Observable 클래스의 핵심 메소드를 외부에서 호출 할 수 없음
- Observable 메소드를 살펴보면 setChanged() 메소드가 protected로 선언되어
있기 때문에 서브 클래스 에서만 호출이 가능
(상속보다는 구성을 사용한다는 디자인 원칙에 위배됨)
출처 : Head First Disign Patterns
디자인 패턴의 세번째! 데코레이터 패턴(Decorator Pattern)
데코레이터 패턴이란?
객체의 추가적인 요건을 동적으로 첨가. 데코레이터는 서브클래스를 만드는 것을 통해서
기능을 유연하게 확장 할 수 있는 방법을 제공.
OCP - 클래스는 확장에 대해서는 열려있어야 하지만
코드변경에 대해서는 닫혀있어야 한다.
기존 코드는 건드리지 않은 채로 확장을 통해서 새로운 행동을 간단하게 추가 할 수 있도록 하는 것
데코레이터 패턴의 특징
- 데코레이터의 수퍼클래스는 자신이 장식하고 있는 수퍼클래스와 같다.
- 한 객체를 여러개의 데코레이터로 감쌀 수 있다.
- 데코레이터는 자신이 감싸고 있는 객체와 같은 수퍼클래스를 가지고 있기 때문에 원래
객체(쌓여져 있는 객체)가 들어갈 자리에 데코레이터 객체를 집어넣어도 상관이 없다.
- 데코레이터는 자신이 장식하고 있는 객체에게 어떤 행동을 위임 하는것 외에
원하는 추가적인 작업을 수행할 수 있다.
- 객체는 언제든지 감쌀 수 있기 때문에 실행중에 필요한 데코레이터를 마음대로
적용할 수 있다.
<클래스 다이어그램>
커피 가격 프로그램
위와 같이 할 경우 우유나 모카 등을 추가할 경우 Milk DarkRoast, Mocha DarkRoast, Milk Espresso, Mocha Espresso 등 클래스가 엄청나게 늘어나게 됨
인터페이스와 수퍼클래스 상수를 사용할 경우
문제점 : 첨가물의 가격이 바뀔 때마다 기본 코드수정을 해야하고 첨가물의 종류가 많아지면 새로운 메소드 추가, 수퍼클래스의 cost()도 수정해야 함. 아이스티 처럼 새로운 음료를 추가 할 때 들어가지 말아야 할 첨가물이 발생해도 상속을 받게 됨
데코레이터를 사용한 커피 가격 프로그램
<클래스 다이어그램>
데코레이터를 사용하는 코드Beverage beverage = new DarkRoast(); //DarkRoast 객체를 만듦
beverage = new Mocha(beverage); // Mocha로 감쌈
beverage = new Milk(beverage); // Milk로 감쌈
beverage = new Milk(new Mocha)); // 위 두줄을 이렇게 한줄로 나타낼 수 도 있음
자바 입출력을 할 때 늘 사용되는 자바 IO 에서도 데코레이터가 사용되고 있음
출처 : Head First Design Patterns
팩토리 메소드 패턴(Factory Method Pattern)
팩토리 메소드 패턴에서는 객체를 생성하기 위한 인터페이스를 정의하는데, 어떤
클래스의 인스턴스를 만들지는 서브클래스에서 결정하게 만듦. 팩토리 메소드 패턴을 이용하면 클래스의 인스턴스를 만드는 일을 서브 클래스에게 맡김
- 서브클래스에서 결정한다는 것은 이 패턴을 사용할 때 서브클래스에서 실행중에 어떤 클래스의 인스턴스를 만들지 결정하기 때문이 아니라 생산자 클래스 자체가 실제 생산될 제품에 대한 사전지식이 전혀 없이 만들어 지기 때문(팩토리 메소드를 사용하는 이유는 이전 블로깅의 new를 사용할 때의 단점을 보완하여 유연성 있도록 하기 위함)
팩토리 메소드 패턴의 사용 예(이전 블로깅에서 만들었던 내용에 이어서)
이전에 만들었던 createPizza() 메소드를 PizzaStore에 다시 넣으면서 추상 메소드로 선언하고 PizzaStore의 서브클래스를 만듦
public abstract class PizzaStore{
public Pizza orderPizza(String type){
Pizza pizza;
pizza = createPizza(type);
// 팩토리 객체가 아닌 PizzaStore에 있는 createPizza를 호출하게 됨
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
protected abstract Pizza createPizza(String type);
//기타 메소드
//팩토리 메소드가 PizzaStore의 추상메소드로 바뀜
//Pizza 인스턴스를 만드는 일은 이제 팩토리 역할을 하는 메소드에서 맡아서 처리
}
팩토리 메소드는 객체 생성을 처리하며, 팩토리 메소드를 이용하면 객체를 생성하는 작업을 서브클래스에 캡슐화 시킬 수 있음. 이렇게 하면 수퍼클래스에 있는 클라이언트 코드와 서브클래스에 있는 객체 생성 코드를 분리시키게 됨.
서브클래스에서 결정되는 것
PizzaStore의 orderPizza() 메소드에 이미 주문 시스템이 갖춰져 있으므로(서브 클래스
에서 변경하지 못하도록 final로 선언 가능) 각 서브클래스에서는 달라지는 피자의 스타일을 createPizza() 메소드로 구현
<서브 클래스의 코드>
public class NYPizzaStore extends PizzaStore{
Pizza createPizza(String item){
if(item.equals("cheese")){
return new NYStyleCheesePizza();
} else if (item.equals("veggie)){
return new NYStyleVeggiePizza();
} else return null;
}
}
수퍼클래스에 있는 orderPizza()메소드에서는 어떤 피자가 만들어지는지 전혀 알 수 없음. 그 메소드에서는 피자를 준비하고 굽고 자르고 포장하는 작업만 처리함.
팩토리 메소드를 이용한 피자 주문(뉴욕풍 피자 주문)
1. PizzaStore 인스턴스를 확보(NYPizzaStore의 인스턴스를 만듦)
2. PizzaStore가 만들어지고 나면 orderPizza()를 호출
(인자를 사용해서 cheese, veggie 등을 알려줌)
3. 피자를 만들 때는 createPizza() 메소드가 호출되는데, 이 메소드는 PizzaStore의 서브
클래스인 NYPizzaStore에 정의되어 있으므로 NYPizzaStore에서는 뉴욕풍의 피자 인스
턴스를 만들고 어떤 서브클래스를 쓰든지 Pizza 객체가 orderPizza() 메소드로 리턴됨.
4. orderPizza() 메소드에서는 어떤 종류의 피자가 만들어졌는지 전혀 알지 못하지만 피자
라는 것을 알고있고 그 피자를 준비하고, 굽고, 자르고, 포장하는 작업을 완료함.
<만들어지는 과정>
1. 뉴욕풍 피자가게 확보
PizzaStore nyPizzaStore = new NYPizzaStore();
// NYPizzaStore 인스턴스 생성
2. 피자 가게가 확보되었으니 주문을 받음
nyPizzaStore.orderPizza(“cheese”);
// nyPizzaStore 인스턴스의 orderPizza() 메소드가 호출(PizzaStore에 정의된 메소드 호출)
3. orderPizza() 메소드에서 createPizza() 메소드를 호출
Pizza pizza = createPizza(“cheese”)
// 팩토리 메소드인 createPizza() 메소드는 서브 클래스에서 구현. 이 경우 뉴욕풍 피자 리턴
4. 피자를 받아서 마무리 작업
pizza.prepare();
pizza.bake()
pizza.cut();
pizza.box();
// 이 aphememf은 모두 createPizza()
<팩토리 메소드 패턴의 다이어그램>
모든 팩토리 패턴에서는 객체 생성을 캡슐화 하고 팩토리 메소드 패턴에서는 서브클래스에서 어떤 클래스를 만들지를 결정하게 함으로써 객체 생성을 캡슐화
<생산자(Creator) 클래스>
<제품(Product) 클래스>
객체 의존성
만약 팩토리 메소드 패턴을 사용하지 않고 객체 인스턴스를 직접 만들면 구상 클래스에 의존해야 함. 모든 피자 객체를 팩토리에 맡겨서 만들지 않고 PizzaStore 클래스 내에서 직접 만들시 모든 피자 객체들에게 직접적으로 의존하게 되고 피자 클래스들의 구현이 변경되면 PizzaStore 클래스까지 고쳐야 됨
의존성 뒤집기 원칙(Dependency Inversion Principle)
구상 클래스처럼 구체적인 것이 아닌 추상 클래스나 인터페이스와 같이 추상적인 것에 의존하는 코드를 만들어야 한다는 것으로 이전에 나왔던 “특정 구현이 아닌 인터페이스에 맞춰서 프로그래밍 한다”는 원칙과 비슷하지만 의존성 뒤집기 원칙에서는 “추상화”를 더 많이 강조함. 이 원칙에는 고수준 구성요소가 저수준 구성요소에 의존하면 안된다는 것이 내포되어 있고 항상 추상화에 의존하도록 만들어야 한다는 것(PizzaSotre를 고수준 구성요소라고 할수 있고 서브 피자 클래스들은 저수준 요소라고 할수 있음)
원칙 적용
팩토리 메소드 패턴을 적용하고 나면 고수준 구성요소인 PizzaStore와 저수준 구성요소인 피자 객체들이 모두 추상 클래스인 Pizza에 의존하게 됨. 팩토리 메소드 패턴이 의존성 뒤집기 원칙을 준수하기 위해 쓸 수 있는 유일한 기법은 아니지만 가장 접합한 방법 가운데 하나
원칙을 지키는 데 도움이 될만한 가이드라인
어떤 변수에도 구상 클래스에 대한 레퍼런스를 저장하지 말 것
- new 연산자를 사용하면 구상 클래스에 대한 레퍼런스를 사용하게 되는 것이고
팩토리를 써서 구상 클래스에 대한 레퍼런스를 변수에 저장하는 일을 미리 방지
구상 클래스에서 유도된 클래스를 만들지 말 것
- 구상 클래스에서 유도된 클래스를 만들면 특정 구상 클래스에 의존하게 됨.
인터페이스나 추상 클래스처럼 추상화된 것으로부터 클래스를 만들어아 함
베이스 클래스에 이미 구현된 메소드를 오버라이드 하지 말 것
- 이미 구현되어 있는 메소드를 오버라이드 한다는 것은 애초부터 베이스 클래스가 제대로
추상화 된 것이 아니라고 볼 수 있음. 베이스 클래스에서 메소드를 정의할 때는 모든
서브클래스에서 공유할 수 있는 것만 정의해야 함
※ 이 가이드라인은 항상 지켜야 하는 규칙이 아니라 우리가 지향해야 할 바를 밝히고 있을 뿐
이고 자바 프로그램 가운데 이 가이드라인을 완벽하게 따르는 것은 하나도 없음. 그러나 이
가이드라인을 완전히 습득한 상태에서 디자인을 할 때 항상 이 가이드라인을 염두해
둔다면, 불가피한 상황에서만 합리적인 이유를 바탕으로 그렇게 하게 될 것이다.
출처 : Head First Design Patterns