에릭 프리먼의 <헤드 퍼스트 디자인 패턴>을 읽고 정리한 후,
Swift 로 적용해보는 스터디입니다.
SimUDuck
이라는 오리 연못 시뮬레이션 게임이 있다.
이 시스템을 처음 디자인한 사람들은 표준적인 객체지향 방법을 이용하여
Duck
이라는 수퍼 클래스를 만들고,
그 클래스를 확장하여 다른 종류의 오리(청둥 오리, 러버덕 등)를 만들었다.
그리고 오리들이 날아다닐 수 있도록 수퍼 클래스 Duck
에 fly()
메소드를 추가했다.
그렇게 모든 서브 클래스에서 fly()
를 상속받게 되고
날 수 없는 오리 인형도 날아다니는 불상사가 발생한다....
만약 fly()
메소드가 들어있는 Flyable
인터페이스를 만들어서 날 수 있는 오리들에 대해서만 인터페이스를 구현한다면??
Flyable
인터페이스를 사용하면 자바 인터페이스에는 구현된 코드가 들어가지 않기 때문에 코드를 재사용 할 수 없다는 문제점이 생긴다.
한 행동을 바꿀 때마다 그 행동이 정의되어 있는 서로 다른 서브클래스를 전부 찾아서 코드를 일일이 고쳐야한다.
💡 디자인 원칙 1
애플리케이션에서 달라지는 부분을 찾아내고,
달라지지 않는 부분으로부터 분리시킨다.
바뀌는 부분을 따로 뽑아서 캡슐화시켜야 한다.
그러면 코드를 변경하는 과정에서 의도하지 않은 일이 일어나는 것을 줄이고 시스템의 유연성을 향상시킬 수 있다.
"변화하는 부분과 그대로 있는 부분" 을 Duck
하고 완전히 분리하려면
두 개의 클래스 집합을 만들어야한다.
fly()
와 quack()
은 Duck
클래스에서 오리마다 구현이 달라지는 부분이다.
이러한 행동을
Duck
클래스로부터 갈라내기 위해서
두 메소드를 모두 Duck 클래스로부터 끄집어내서
각 행동을 나타낼 클래스 집합을 새로 만들도록 하자!
나는 행동과 꽥꽥거리는 행동을 구현하는 클래스 집합은 어떻게 디자인 해야할까?
Duck
클래스에 행동과 관련된 세터 메소드를 포함시켜 프로그램 실행 중에도 Mallard Duck
의 나는 행동을 바꿀 수 있도록 하면 좋을 것이다.
💡 디자인 원칙 2
구현이 아닌 인터페이스에 맞춰서 프로그래밍한다.
이제부터 Duck의 행동을 (특정 행동 인터페이스를 구현한) 별도의 클래스 안에 들어있게 해보자.
그렇게 하면 Duck 클래스에서는 그 행동을 구체적으로 구현하는 방법에 대해서 더이상 알고 있을 필요가 없게 된다.
행동 인터페이스는 Duck의 클래스가 아닌 행동 클래스에서 구현한다.
이 방법은 특정 구현에 의존했던 아래의 방법과는 다르다.
새로운 디자인을 사용하면 Duck의 서브클래스에서는 인터페이스로 표현되는 행동을 사용하게 된다.
따라서 행동을 실제로 구현한 것은 Duck 서브 클래스에 국한되지 않는다.
이제 Duck
에서 나는 행동과 꽥꽥거리는 행동은 다른 클래스에 위임하게 된다.
기존의 Duck
클래스에 FlyBehavior
와 quackBehavior
라는 두개의 인터페이스 형식의 인스턴스 변수를 추가하고
기존 fly()
와 quack()
메소드 대신 performFly()
와 performQuack()
이라는 메소드를 만든다.
public class Duck {
QuackBehavior quackBehavior;
...
public void performQuack() {
quackBehavior.quack();
}
}
public class MallardDuck extends Duck {
public MallardDuck() {
quackBehavior = new Qauck();
flyBehavior = new FlyWithWings();
}
public void display() {
system.our.printIn("저는 물오리입니다 🦆");
}
}
오리의 행동 형식을 생성자에서 인스턴스를 만드는 방법이 아닌
Duck의 서브클래스
에서 세터 메소드를 호출하는 방법
으로 설정하면 된다.
public void setFlyBehavior(FlyBehevior fb) {
flyBehavior = fb;
}
public void setQuackBehavior(QuackBehevior qb) {
quackBehavior = qb;
}
public class FlyRocketPowered implements FlyBehavior {
public void fly() {
system.our.printIn("로켓 발사 슝 🚀");
}
}
이렇게 하면 청둥오리를 세터 메소드를 호출해 로켓으로 날게 할 수 있다!
Duck mallard = new MallardDuck();
model.display(); // 저는 물오리입니다 🦆
model.setFlyBehavior(new FlyRocketPowered());
model.performFly(); // 로켓 발사 슝 🚀
💡 디자인 원칙 3
상속보다는 구성을 활용한다.
각 오리에는 FlyBehavior
와 QuackBehavior
가 있으며,
각각 행동과 꽥꽥거리는 행동을 위임받는다.
두 클래스를 이런 식으로 합치는 것을 구성
을 이용한다고 한다.
앞서 만든 오리 클래스에서는 행동을 상속받는 대신, 올바른 행동 객체로 구성됨으로써 행동을 부여받게 된다.
이와 같은 방법을 Strategy Pattearn, 전략 패턴이라고 한다.
전략 패턴에서는 알고리즘군을 정의하고 각각을 캡슐화
하여 교환하여 사용할 수 있도록 만든다. 전략 패턴을 활용하면 알고리즘을 활용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.
앞서 봤던 오리의 행동들을 알고리즘 군으로 생각해보면
나는 행동과 꽥꽥거리는 행동을 인터페이스로 정의하고 캡슐화된 알고리즘군을 활용했다.
장점
단점
Object using a Strategy (전략을 사용하는 객체)
iOS 에서는 주로 뷰 컨트롤러가 이에 해당한다.
Strategy Protocol (전략 프로토콜)
모든 전략들이 반드시 구현해야 하는 메소드를 정의하고 있다. (인터페이스)
Concrete Strategies (구체적인 전략)
Strategy protocol 을 구현합니다. (알고리즘)
참고 PinguiOS
interchangeable
인 2개 이상의 서로 다른 동작들을 갖고 있을 때
네비게이션 애플리케이션이 있다고 생각해보자.
처음에는 자동차의 경로만 알려주는 내비게이션이었지만 점차 사용자가 늘어 도보, 대중교통 경로와 같은 다양한 알고리즘이 필요하게 되었다.
이 때 전략 패턴을 사용해 각 자동차, 도보, 대중교통에 대한 알고리즘을 따로 구현하고 캡슐화하면 된다.
delegation 패턴은 런타임 동안 자주 delegate 들이 고정된다.
하지만, strategy 들은 런타임 동안 쉽게 interchangeable 하다.
책 예제와 동일하게 Duck
클래스를 만들어주고 이를 상속받는 여러 종류의 오리 클래스를 만들어줍니다.
예제와 같이 fly()
메소드를 추가하자
의도와 달리 날아가버린 러버덕...
앞서 배운 전략 패턴을 이용해서 스위프트에서는 인터페이스 대신 프로토콜을 사용해서 FlyBehavior, QuackBehavior
와 같은 Strategy Protocol
를 만들어줍니다.
그럼 이렇게 전략패턴을 사용해 상속에서의 문제점을 해결하면서도
런타임에서 알고리즘을 쉽게 변경할 수 있다는 걸 알 수 있습니다 !!
요번에는 디자인 패턴의 필요성과 전략 패턴에 대해 정리해봤는데요.
다음에는 옵저버 패턴으로 찾아오겠습니다....
아됴스
당훈뷘은 다르다