헤드 퍼스트 디자인 패턴 1장의 내용을 정리한 글입니다.
오리 시뮬레이션 게임 SumUduck 예제
이 게임에는 헤엄치고, 꽥꽥 소리를 내는 다양한 오리가 등장합니다. 이를 구현하기 위해 일반적인 객체지향 기법을 사용해 봅니다.
1. 슈퍼클래스 Duck 클래스를 만든 다음, Duck을 상속하는 다른 오리들을 만들어 봅니다.
abstract class Duck {
// 모든 오리가 꽥꽥 소리를 낼 수 있습니다.
quack();
// 모든 오리가 헤엄을 칠 수 있습니다.
swim();
// 모든 오리의 모양이 다르므로 display() 메소드는 추상 메소드입니다.
display();
}
class MallardDuck extends Duck {
display() // 나는 말라드덕
}
class RedheadDuck extends Duck {
display() // 나는 레드헤드덕
}
class XXXDuck extends Duck {
display() // 나는 XXX덕
}
...
2. 오리는 날 수 있어야 한다는 요구사항이 생겼습니다. 그것을 구현하기 위해 Duck 클래스 내에 fly() 메소드를 정의했습니다.
abstract class Duck {
// 모든 오리가 꽥꽥 소리를 낼 수 있습니다.
quack();
// 모든 오리가 헤엄을 칠 수 있습니다.
swim();
// 모든 오리의 모양이 다르므로 display() 메소드는 추상 메소드입니다.
display();
// 오리를 날게 하는 메소드를 추가합니다.
fly();
}
3. Duck의 몇몇 서브클래스만 날 수 있을 경우에는 2번의 방법이 올바르지 못합니다. 왜냐하면 2번의 코드는 모든 서브클래스가 fly() 메소드를 상속받기 때문입니다. 이를 해결하기 위해 fly() 메소드를 추상 메소드로 변경 후 각 서브클래스에서 fly() 메소드를 오버라이드하도록 변경할 수 있습니다. 또한 RubberDuck은 꽥꽥 소리가 아닌 삑삑 소리를 내도록 해야 한다면, quack() 메소드를 또 추상클래스로 만든 후 오버라이드하도록 변경해야 합니다.
abstract class Duck {
// 모든 오리가 꽥꽥 소리를 낼 수 있습니다.
quack();
// 모든 오리가 헤엄을 칠 수 있습니다.
swim();
// 모든 오리의 모양이 다르므로 display() 메소드는 추상 메소드입니다.
display();
// 날 수 있는 추상 메소드
fly();
}
class MallardDuck extends Duck {
display() // 나는 말라드덕
}
class RedheadDuck extends Duck {
display() // 나는 레드헤드덕
}
class RubberDuck extends Duck {
display() // 나는 러버덕
quack() // 삑삑
fly() // 아무것도 하지 않도록 오버라이드
}
class DecoyDuck extends Duck {
quack() // 아무것도 하지 않도록 오버라이드
fly() // 아무것도 하지 않도록 오버라이드
display() // 나는 디코이덕
}
...
위처럼 Duck의 행동을 상속할 때 단점이 될 수 있는 요소는 다음과 같습니다.
- 서브클래스에서 코드가 중복된다.
- 실행 시에 특징을 바꾸기 힘들다.
- 모든 오리의 행동을 알기 힘들다.
- 코드를 변경했을 때 다른 오리들에게 원치 않은 영향을 끼칠 수 있다.
요구사항이 계속 변화된다면, 위 방법을 유지할 때 규격이 바뀔 때마다 메소드들을 살펴보고 상황에 따라 오버라이드해야 합니다. 인터페이스를 사용하여 분리한다는 아이디어를 낼 수 있습니다.
abstract class Duck {
swim();
// 모든 오리의 모양이 다르므로 display() 메소드는 추상 메소드입니다.
display();
}
interface Flyable {
fly();
}
interface Quackable {
quack();
}
class MallardDuck extends Duck implements Flyable, Quackable {
fly();
quack();
display() // 나는 말라드덕
}
class RedheadDuck extends Duck implements Flyable, Quackable {
fly();
quack();
display() // 나는 레드헤드덕
}
class RedheadDuck extends Duck implements Quackable {
quack(); // 삑삑
display() // 나는 레드헤드덕
}
class DecoyDuck extends Duck {
display() // 나는 디코이덕
}
...
이 방법은 코드 중복 측면에서 좋지 않은 코드입니다. 서브클래스에서 구현을 모두 하기 때문입니다. 만약 fly() 메소드 안이 살짝 변경되어야 한다면 Flyable 인터페이스를 구현하는 모든 서브클래스 안의 코드를 고쳐야 합니다.
여기서 디자인패턴을 사용한다면 기존 코드에 미치는 영향을 줄이며 작업할 수 있습니다.
소프트웨어 개발에서 불변의 진리는 어떤 애플리케이션이라도 시간이 지남에 따라 변화해야 한다는 점입니다. 디자인 원칙 중 하나는 "바뀌는 부분은 따로 뽑아서 캡슐화한다. 그러면 나중에 바뀌지 않는 부분에는 영향을 미치지 않고 그 부분만 고치거나 확장할 수 있다."라고 합니다.
바뀌는 부분과 그렇지 않은 부분 분리하기
fly(), quack()을 제외한 나머지 부분은 자주 달라지거나 바뀌지 않는다고 가정하겠습니다. 나는 행동과 꽥꽥거리는 행동을 구현하는 클래스 집합을 만드는 것을 생각할 수 있습니다. 여기서 행동에 대한 인터페이스와 구현 클래스를 만들어 보겠습니다.
interface FlyBehavior {
fly();
}
class FlyWithWings implements FlyBehavior {
fly(); // 나는 방법을 구현
}
class FlyNoWay implements FlyBehavior {
fly(); // 아무것도 하지 않음(날 수 없어요)
}
interface QuackBehavior {
quack();
}
class Quack implements QuackBehavior {
quack(); // 꽥꽥 소리를 냄
}
class Squeak implements QuackBehavior {
quack(); // 삑삑 소리를 냄
}
class MuteQuack implements QuackBehavior {
quack(); // 아무것도 하지 않음(소리를 낼 수 없는 경우)
}
이런 식으로 디자인하면 다른 형식의 객체에서도 나는 행동과 꽥꽥거리는 행동을 재사용할 수 있습니다. 기존의 행동 클래스를 수정하거나 새로운 행동을 추가하기 용이합니다.
오리 행동 통합하기
Duck 클래스에 flyBehavior, quackBehavior 인터페이스 형식의 인스턴스 변수를 추가합니다. fly(), quack() 메소드를 제거한 후 flyBehavior, quackBehavior에게 나는 행동, 꽥꽥 행동을 위임하도록 합니다.
public abstract class Duck {
QuackBehavior quackBehavior;
FlyBehavior flyBehavior;
public Duck() {
}
public abstract void display();
public void swim() {
// 헤엄 헤엄
}
public void performQuack() {
quackBehavior.quack();
}
public void performFly() {
flyBehavior.fly();
}
}
이제 각 오리들을 생성할 때 적절한 구현체를 끼워주면 됩니다.
public class MallardDuck extends Duck {
public MallardDuck() {
quackBehavior = new Quack();
flyBehavior = new FlyWithWings();
}
public void display(){
// 나는 말라드덕
}
}
이제 말라드덕을 메인 메소드에서 사용할 수 있습니다.
public class MiniDuckSimulator {
public static void main(String[] args) {
Duck mallard = new MallardDuck();
mallard.performQuack();
mallard.performFly();
}
}
위와 같이 행동을 인터페이스로 분리하여 재사용성이 높은 코드를 만들 수 있게 되었습니다. 나는 행동이 변화할 때, 꽥꽥거리기 행동이 변화할 때 구현체 안의 코드만 변경하면 해당 구현체를 사용하는 오리 서브클래스에 적용이 바로 됩니다. 예를 들어 나는 방법으로 로켓 추진을 사용하는 행동을 추가하고 싶다면 다음과 같은 구현체를 정의하면 됩니다.
public class FlyRocketPowered implements FlyBehavoir {
public void fly() {
//로켓 추진으로 날아갑니다
}
}
public class RocketDuck extends Duck {
...
public RocketDuck() {
flyBehavior = new FlyRocketPowered();
...
}
}
동적으로 행동 지정하기
나는 행동, 꽥꽥 행동이 동적으로 변경할 수 있게 하기 위해서 세터 메소드를 추가할 수 있습니다.
// Duck 클래스 안에 세터메소드 추가
public void setFlyBehavior(FlyBehavior fb) {
flyBehavior = fb;
}
public void setQuackBehavior(QuackBehavior qb) {
quackBehavior = qb;
}
메인 메소드에 세터 메소드를 추가하면 동적으로 로켓 추진 날기 행동을 하는 오리를 만들 수 있습니다.
public class MiniDuckSimulator {
public static void main(String[] args) {
Duck model = new MallardDuck();
model.performFly(); // 날고 있어요
model.setFlyBehavior(new FlyRocketPowered());
model.performFly(); // 로켓 추진으로 날고 있어요
}
}
위와 같은 방법을 적용한 클래스 구조, 디자인 패턴을 정리해 보겠습니다.
1. Duck에는 FlyBehavior, QuackBehavior가 있다.(구성 - Composition)
2. FlyWithWings, FlyNoWay는 FlyBehavior를 구현한다.
3. Quack, Squeak, MuteQuack은 QuackBehavior를 구현한다.
4. MallardDuck, RedheadDuck, RubberDuck 등은 Duck을 상속한다.
전략 패턴(Strategy Pattern)이란 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 해줍니다.
전략 패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있습니다.
전략 패턴을 코드를 통해 알아보았습니다. 앞으로의 포스트에서 다양한 디자인 패턴을 코드로 소개하고자 합니다.
'디자인패턴' 카테고리의 다른 글
[디자인 패턴] 옵저버 패턴 (0) | 2023.10.06 |
---|