(Swift를 사용한 헤드 퍼스트 디자인 패턴) – 1. 전략 패턴


머리부터 발끝까지 디자인 패턴 시작하기

“Head First Design Patterns”는 Java 기반의 책이지만 여기에서는 Swift로 구현하여 내용을 정리하겠습니다!
언제나 그렇듯 이 글은 개인 정리를 위한 글이니 직접 책을 읽어보시길 추천드립니다 🙂

오리 시뮬레이션 게임 만들기

SimUDuck이라는 오리 게임을 만들어 볼게요 🙂

방법 1. 공통 상속 구조 사용

표준 객체 지향 기술을 사용하여 Duck이라는 슈퍼클래스를 만들고 상속을 통해 여러 Duck을 만듭니다.

(프라이빗, 파이널 등 자세한 설정은 생략하겠습니다)


각 오리는 부모 클래스(Duck) 메서드를 재정의하여 사용합니다.

추가 요구 사항이 있습니다.

오리는 날 수 있어야 합니다.

여기에서는 Duck 클래스에 fly 메서드를 추가합니다.


이를 통해 모든 오리가 하늘을 날 수 있습니다.

문제는 일부 오리는 장난감 오리처럼 날 수 없다는 것입니다


이 문제를 조금씩 해결하기 위해 날 수 없는 오리를 타고 날아가는 것을 재정의하여 아무것도 하지 않도록 할 수 있습니다.


그런데 여기서 또 다른 문제가 발생합니다.

추가 요구 사항이 있습니다.

앞으로 6개월마다 제품 업데이트 하기로 결정

상속이 사용되는 상황에서 제품 사양이 지속적으로 업데이트되면 유지 관리에 많은 리소스가 투입됩니다.

Duck의 모든 방법을 살펴보고 그에 따라 변경해야 합니다.

방법 2. 프로토콜(인터페이스) 사용

위의 방법 1에서 구현한 fly() 메서드를 Duck 클래스에서 제거하고 Flyable이라는 프로토콜을 만들어 구조를 상속합니다.

그렇게 하면 같은 오리라도 “날 수 있는 오리”만이 이 프로토콜을 사용하여 문제를 해결할 수 있습니다.


책에 설명된 이 방법의 단점

(Java 기반) 이 구현으로 인해 많은 코드 중복이 발생합니다.
대부분의 플라이 기능이 Ducks에서 동일하다고 가정하면 Flyable을 사용하여 Ducks에서 동일한 코드를 불필요하게 반복해야 합니다.

단점은 비행 동작을 약간 변경하기 위해 Duck의 하위 클래스에서 모든 비행 방법을 변경해야 할 수도 있다는 것입니다.

Swift에서는 실제로 func를 프로토콜 확장에 정의하는 함수가 있으므로 이 문제를 해결할 수 있습니다.
Java와 달리 POP 측면에서 코드를 구현할 수 있기 때문에 책에서 언급한 단점이 Swift에 적용되지 않는다고 생각합니다.

설계 원칙: 응용 프로그램에서 달라지는 부분을 찾아 분리합니다.

다른 부분을 찾아 “캡슐화”하여 나머지 코드에 영향을 주지 않도록 합니다.

이것은 코드를 변경하는 과정에서 의도하지 않은 발생을 줄이면서 시스템의 유연성을 증가시킵니다.

이 원칙을 적용하는 데는 많은 시간이 걸립니다.

변경되는 부분은 별도로 추출되어 캡슐화됩니다. 그런 다음 나중에 변경할 수 없는 부분에 영향을 주지 않고 해당 부분을 수정하거나 확장할 수 있습니다.

이 개념은 다른 모든 디자인 패턴의 기반이 되는 원칙입니다.

이를 바탕으로 위의 오리 문제를 다시 살펴보자.

Duck 클래스를 “변경 가능한 부분과 정적 부분”으로 분리하려면 두 그룹의 클래스를 만들어야 합니다.

하나는 비행에 관한 것이고 다른 하나는 비명에 관한 것입니다.
각 클래스 집합에는 각 동작의 모든 구현이 포함됩니다.
예를 들어 끽끽 소리 동작을 구현하는 클래스, 신호음 동작을 구현하는 클래스 및 무음 동작을 구현하는 클래스를 만듭니다.

fly() 및 quack()은 오리 유형에 따라 다릅니다.

따라서 Duck 클래스에서 이 두 메서드를 분리하려면 Duck 클래스에서 두 메서드를 모두 가져와 각 동작을 나타내는 새로운 클래스 세트를 만들어야 합니다.

그렇다면 이러한 동작을 구현하는 일련의 클래스를 어떻게 디자인합니까?

최대한 유연하게 만드는게 좋아요!
애당초 이 문제가 나타난 이유는 이 부분에 변화가 있을 확률이 높았기 때문이다.

설계 원칙: 구현 대신 인터페이스로 프로그래밍.

각 동작은 인터페이스로 표시되며 이러한 인터페이스는 동작을 구현하는 데 사용됩니다.

이제 나는 오리 클래스에서 어떤 행동도, 꽥꽥대는 행동도 구현하지 않습니다.

Behavior 인터페이스는 Duck 클래스가 아닌 Behavior 클래스에서 구현됩니다.


“인터페이스로 프로그래밍”이라고 말하면 실제로는 “더 높은 형식으로 프로그래밍”을 의미합니다.

여기서 말하는 인터페이스는 모호하게 사용되며 자바에서 인터페이스를 의미하거나 인터페이스의 개념을 가리킨다. 반드시 Java의 인터페이스(Swift의 프로토콜)를 사용해야 한다는 의미는 아닙니다.

핵심은 위의 형태에 따라 프로그래밍을 하여 다형성을 사용해야 하므로 실제 실행 시 사용되는 객체가 코드에 고정되어 있지 않다는 것입니다.

여기도 “위 형식에 따른 프로그램”~이다 “일반적으로 변수를 선언할 때 추상 클래스나 인터페이스와 같은 상위 유형으로 선언해야 합니다.” 제 생각에는. 개체를 변수에 할당할 때 부모 유형의 구체적인 구현인 한 어떤 개체도 그 안에 넣을 수 있는 유연성이 있기 때문입니다. “그러면 변수를 선언하는 클래스는 실제 객체의 유형을 알 필요가 없습니다.”로 해석될 수도 있습니다.

이 구현의 결과는 다음과 같습니다.

이 디자인을 통해 비행 및 꽥꽥 거동을 다른 유형의 개체(오리 제외)와 함께 재사용할 수 있습니다.

액션 자체가 덕에 숨어있지 않으니까요!

그리고 Duck 클래스를 건드리지 않고 새로운 동작을 추가할 수 있습니다.

상속을 사용하는 부담에서 벗어나 재사용의 이점을 누릴 수 있습니다 🙂

Q. 액션만 보여주는 수업은 번거롭다. 클래스는 원래 무언가를 나타내지 않습니까? 상태와 행동을 모두 포함해야 하지 않습니까?

객체 지향 시스템에서는 귀하의 질문이 정확합니다. 그러나 “액션”은 상태와 메서드도 가질 수 있습니다! (피크 높이, 속도 등)

객체의 개념은 반드시 모든 객체에 제한되지 않습니다.

위의 프로토콜을 사용하여 Duck을 다시 구현해 봅시다!


오리 동작을 동적으로 지정

앞에서 특정 구현을 위해 코딩하면 안 된다고 말했습니다.

이제 앞의 생성자를 보면 Quack이라는 구체적인 클래스의 인스턴스로 선언되었으므로 Duck은 Quack의 구현과 일치합니다.

속성 생성 시 동작은 위에서 확인했지만 이 부분은 유연하게 만들 수 있습니다.

아래와 같이 메소드를 추가하면 코드가 실행되는 동안 동작을 변경할 수 있습니다.


두 클래스 간의 관계

“A에는 B가 있다”의 관계를 생각하면 Duck에는 FlyBehavior와 QuackBehavior가 있습니다.

각각 나는 행동과 돌팔이를 위임합니다.

이와 같이 “두 클래스를 결합하는 것은 구성의 사용입니다” 하다.

설계 원칙: 상속 대신 구성을 사용합니다.

구성은 시스템의 유연성을 향상시킬 수 있습니다.

구성

전략 패턴은 알고리즘 계열을 정의하고 캡슐화하여 모든 알고리즘 계열을 수정하고 사용할 수 있도록 합니다.
전략 패턴은 클라이언트에서 알고리즘을 분리하고 독립적인 변경(관리)을 허용합니다!