오리 시뮬레이션 게임, SimUduck
모든 오리가 꽥꽥 소리를 낼 수 있고(quack()), 헤엄을 칠 수 있으므로(swim()) 오리를 슈퍼 클래스로 작성을 했어요.
오리마다 모양이 다르기 때문에 모양을 나타내는 메소드는 추상 메소드로 만들고 상속받는 클래스에서 오버라이드 하기로 했습니다.
오리 시뮬레이션 게임 차별화하기.
다른 회사보다 차별화를 위해 오리가 날 수 있도록 해야 한다.
가장 간단한 방법으로 Duck에 날 수 있는 메소드만 추가하면 모든 오리가 그걸 상속받기 때문에 Duck에 fly()메소드 추가를 해요.
문제 발생
Duck의 몇몇 서브클래스만 날아야 하는데 날아다니면 안 되는 오리에게도 날아다니는 기능이 추가됐습니다.
슈퍼클래스의 일부만 고쳤는데, 프로그램 전체에 고무오리가 날아다니는 오류가 발생했다.
상속을 생각하기
이 오류를 해결하기 위해 가장 간단한 상속으로 해결을 해보려고 해서 quack() 메소드를 오리에 따라 오버라이드 했듯이 RubberDuck에 fly()를 오버라이드를 했습니다.
위 방식처럼 상속했을 때 문제점.
- 서브 클래스에서 코드가 중복된다.
- 모든 오리의 행동을 알기 힘들다
- 실행 시에 특징을 바꾸기 힘들다
- 코드를 변경했을 때 다른 오리들에게 원치 않은 영향을 끼칠 수 있다.
인터페이스 설계하기
상속은 올바른 해결책이 아니다.
상속을 계속 활용하면 앞으로 규격이 바뀔 때마다 Duck의 서브 클래스에 있는 fly()와 quack() 메소드를 일일이 살펴보고 상황에 따라 오버라이드해야 합니다.
특정 형식의 오리만 날거나 꽥괙거릴 수 있도록 하는 더 깔끔한 방법을 찾아야 합니다.
해결 방법 고민하기
Flyable과 Quackable 인터페이스를 만들어서 해당 기능이 필요한 오리만 넣어서 사용하기로 합니다.
하지만 이렇게 하면 일부 문제점은 해결할 수 있지만, 코드를 재사용하지 않으므로 코드 관리에 문제가 생깁니다.
날 수 있는 오리 중에서도 날아다니는 방식이 서로 다를 수 있다는 문제도 있기 때문입니다.
문제를 명확하게 파악하기
상속이 그리 성곡적인 해결책이 아니라는 사실은 이제 확실히 알았고, 서브클래스마다 오리의 행동이 바뀔 수 있는데도 모든 서브클래스에서 한 가지 행동만 하도록 하는 것은 올바르지 못하다. 인터페이스를 사용하는 방법은 괜찮아 보였지만, 자바 인터페이스에는 구현된 코드가 없으므로 코드를 재사용할 수 없다는 문제점이 있습니다.
즉 한 가지 행동을 바꿀 때마다 그 행동이 정의되어 있는 서로 다른 서브클래스를 전부 찾아서 코드를 일일이 고쳐야 하고, 그 과정에서 새로운 버그가 생길 가능성도 있다.
디자인 원칙
애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리한다.
달라지는 부분을 찾아서 나머지 코드에 영향을 주지 않도록 '캡술화' 합니다.
그러면 코드를 변경하는 과정에서 의도치 않게 발생하는 일을 줄이면서 시스템의 유연성을 향상시킬 수 있습니다.
코드에 새로운 요구사항이 있을 때마다 바뀌는 부분이 있다면 분리해야 합니다.
바뀌는 부분은 따로 뽑아서 캡슐화한다. 그러면 나중에 바뀌지 않는 부분에는 영향을 미치지 않고 그 부분만 고치거나 확장할 수 있다.
바뀌는 부분과 그렇지 않은 부분 분리하기
fly()와 quack()은 Duck 클래스에 있는 오리 종류에 따라 달라지는 부분입니다.
fly()와 quack()을 Duck 클래스로부터 분리하려면 2개의 메소드를 모두 Duck 클래스에서 끄집어내서
각 행동을 나타낼 클래스 집합을 새로 만들어야 한다.
오리의 행동을 디자인하는 방법
디자인 원칙
구현보다는 인터페이스에 맞춰서 프로그래밍한다.
각 행동은 인터페이스로 표현하고 이런 인터페이스를 사용해서 행동을 구현하겠습니다.
나는 행동과 꽉꽉거리는 행동은 특정 행동만을 목적으로 하는 클래스의 집합을 만들겠습니다.
행동(behavior) 인터페이스는 Duck 클래스가 아니라 방금 설명한 행동 클래스에서 구현합니다.
Duck 클래스에서 구체적으로 구현하거나 서브클래스 자체에서 별도로 구현하는 방법과는 상반된 방법으로 항상 특정 구현에 의존했기에 코드를 변경해야만 행동을 변경할 수 있었습니다.새로운 디자인을 사용하면 Duck 서브클래스는 인터페이스로 표현되는 행동을 사용합니다.따라서 실제 행동 구현은 Duck 서브클래스에 국한되지 않습니다.
"인터페이스에 맞춰서 프로그래밍한다"라는 말은 사실 "상위 형식에 맞춰서 프로그래밍한다"라는 말입니다.
변수를 선언할 때 추상 클래스나 인터페이스 같은 상위 형식으로 선언해야 한다.
객체를 변수에 대입할 때 상위 형식을 구체적으로 구현한 형식이라면 어떤 객체든 넣을 수 있기 때문이다.
그러면 변수를 선언하는 클래스에서 실제 객체의 형식을 몰라도 된다.
오리의 행동을 구현하는 방법
날 수 있는 클래스는 무조건 FlyBehavior 인터페이스를 구현합니다.
날 수 있는 클래스를 새로 만들 때는 무족건 fly 메소드를 구현해야 합니다.꽥꽥 거리는 것과 관련된 행동도 마찬가지입니다. 반드시 구현해야하는 quack() 메소드가 들어있는 QuackBehavior인터페이스가 있습니다.
이런 식으로 디자인하면 다른 형식의 객체에서도 나는 행동과 꽥꽥거리는 행동을 재사용할 수 있습니다.그리고 기존의 행동 클래스를 수정하거나 날아다니는 행동을 사용하는 Duck 클래스를 전혀 건드리지 않고도 새로운 행동을 추가할 수 있습니다.
따라서 상속을 쓸 때 더안게 되는 부담을 전부 떨쳐 버리고 재사용의 장점을 그대로 누릴 수 있습니다.
오리 행동 통합하기
가장 중요한 점은 나는 행동과 꽥꽥거리는 행동을 Duck 클래스에서 정의한 메소드를 써서 구현하지 않고 다른 클래스에 위임한다는 것입니다.
우선 Duck 클래스에 flyBehavior와 quackBehavior라는 인터페이스 형식의 변수를 추가하고 각 오리 객체에선 실행 시 이 변수에 특정 행동 형식의 레퍼런스를 다형적으로 설정합니다.
나는 행동과 꽥꽥거리는 행동은 flyBehavior 와 quackBehavior 인터페이스로 옮겨놨으므로 Duck 클래스와 모든 서브 클래스에서 fly()와 quack()메소드를 제거합니다.
Duck 클래스에 fly()와 quack() 대신 performfFly()와 performQuack()라는 메소드를 넣습니다.
오리(Duck) 코드
public abstract class Duck {
public FlyBehavior flyBehavior;
public QuackBehavior quackBehavior;
public Duck() {}
public abstract void display();
public void performFly() {
flyBehavior.fly();
}
public void performQuack() {
quackBehavior.quack();
}
public void swim() {
System.out.printLn("모든 오리는 물에 뜹니다. 가짜오리도 뜨죠.");
}
}
public interface FlyBehavior {
void fly();
}
public class FlyWithWings implements FlyBehavior {
@Override
public void fly() {
System.out.println("날고 있어요");
}
}
public class FlyNoWay implements FlyBehavior {
@Override
public void fly() {
System.out.println("저는 못 날아요");
}
}
public interface QueakBehavior {
void quack();
}
public class Quack implements QueakBehavior {
@Override
public void quack() {
System.out.println("꽥");
}
}
public class MuteQuack implements QueakBehavior {
@Override
public void quack() {
System.out.println("조용");
}
}
public class Squeak implements QueakBehavior {
@Override
public void quack() {
System.out.println("삑");
}
}
public class MallardDuck extends Duck {
public MallardDuck() {
flyBehavior = new FlyWithWings();
quackBehavior = new Quack();
}
@Override
public void display() {
System.out.println("저는 물오리 입니다.");
}
}
public class DuckApplication {
public static void main(String[] args) {
Duck mallard = new MallardDuck();
mallard.performFly();
mallard.performQuack();
}
}
동적으로 행동 지정하기
public abstract class Duck {
public FlyBehavior flyBehavior;
public QuackBehavior quackBehavior;
public Duck() {}
public abstract void display();
public void performFly() {
flyBehavior.fly();
}
public void performQuack() {
quackBehavior.quack();
}
public void swim() {
System.out.printLn("모든 오리는 물에 뜹니다. 가짜오리도 뜨죠.");
}
public void setFlyBehavior(FlyBehavior fb) {
flyBehavior = fly;
}
public void setQuackBehavior(QuackBehavior qb) {
quackBehavior = quack;
}
}
public class DuckApplication {
public static void main(String[] args) {
Duck mallard = new MallardDuck();
mallard.performFly();
mallard.performQuack();
mallard.setFlyBehavior(new FlyNoWay());
mallard.performFly();
}
}
캡슐화된 행동 살펴보기
클라이언트에서는 나는 행동과 꽥꽥거리는 행동을 캡슐화된 알고리즘으로 구현합니다.
각 오리엔 FlyBehavior와 QuackBehavior가 있고, 각각 나는 행동과 꽥꽥거리는 행동을 위임받습니다.
이런 식으로 두 클래스를 합치는 것을 '구성을 이용한다'라고 부릅니다.
여기에 나와 있는 오리 클래스에선 행동을 상속받는 대신, 올바른 행동 객체로 구성되어 행동을 부여받습니다.
디자인 원칙
상속보다는 구성을 활용한다.
전략패턴은 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 해 줍니다. 전략패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있습니다.
나의 의견 : 객체가 할 수 있는 행위 각각 전략으로 만들어 놓고, 동적으로 행위의 수정이 필요한 경우 전략을 바꾸는 것만으로 행위의 수정이 가능하도록 만든 패턴.
'개발 도서' 카테고리의 다른 글
디자인 패턴 - 커맨드 패턴 (0) | 2023.12.09 |
---|---|
디자인 패턴 - 싱글톤 패턴 (0) | 2023.12.04 |
디자인 패턴 - 팩토리 메서드 패턴, 추상 팩토리 패턴 (1) | 2023.12.02 |
디자인 패턴 - 데코레이터 패턴 (0) | 2023.11.28 |
디자인 패턴 - 옵저버 패턴 (1) | 2023.11.23 |