자바의 상태 디자인 패턴

1. 개요

이 자습서에서는 동작 GoF 디자인 패턴 중 하나 인 상태 패턴을 소개합니다.

처음에는 목적에 대한 개요를 제공하고 해결하려는 문제를 설명합니다. 그런 다음 State의 UML 다이어그램과 실제 예제의 구현을 살펴 보겠습니다.

2. 상태 디자인 패턴

상태 패턴의 주요 아이디어 는 객체가 클래스를 변경하지 않고 동작을 변경할 수 있도록 허용하는 것입니다. 또한이를 구현함으로써 코드는 많은 if / else 문없이 깔끔하게 유지되어야합니다.

우체국으로 보낸 소포가 있다고 상상해보십시오. 소포 자체를 주문한 다음 우체국으로 배달하고 최종적으로 고객이받을 수 있습니다. 이제 실제 상태에 따라 배달 상태를 인쇄하려고합니다.

가장 간단한 방법은 부울 플래그를 추가하고 클래스의 각 메소드 내에 간단한 if / else 문을 적용하는 것입니다. 간단한 시나리오에서는 그다지 복잡하지 않습니다. 그러나 처리 할 상태가 더 많아지면 더 많은 if / else 문이 생성 될 때 코드가 복잡해지고 오염 될 수 있습니다.

게다가 각 상태에 대한 모든 논리는 모든 방법에 분산됩니다. 이제 이것은 State 패턴이 사용되는 것으로 간주 될 수있는 곳입니다. State 디자인 패턴 덕분에 로직을 전용 클래스로 캡슐화하고, 단일 책임 원칙과 개방 / 폐쇄 원칙을 적용하고, 더 깔끔하고 유지 관리가 쉬운 코드를 가질 수 있습니다.

3. UML 다이어그램

UML 다이어그램에서 Context 클래스에는 프로그램 실행 중에 변경 될 관련 State 가 있음을 알 수 있습니다.

우리의 컨텍스트는 동작을 상태 구현위임 할입니다. 즉, 들어오는 모든 요청은 상태의 구체적인 구현에 의해 처리됩니다.

논리가 분리되어 있고 새로운 상태를 추가하는 것이 간단 하다는 것을 알 수 있습니다. 필요한 경우 다른 상태 구현 을 추가 하는 것이 중요합니다.

4. 구현

애플리케이션을 설계 해 보겠습니다. 이미 언급했듯이 패키지는 주문, 배송 및 수신이 가능하므로 세 가지 상태와 컨텍스트 클래스가 있습니다.

먼저 컨텍스트를 정의하겠습니다. 이것이 Package 클래스 가 될 것입니다 .

public class Package { private PackageState state = new OrderedState(); // getter, setter public void previousState() { state.prev(this); } public void nextState() { state.next(this); } public void printStatus() { state.printStatus(); } }

보시다시피 상태 관리를위한 참조가 포함되어 있습니다 . 작업을 상태 객체에 위임하는 previousState (), nextState () 및 printStatus () 메서드를 확인하세요. 상태는 서로 연결되고 모든 상태는 두 메서드에 전달 참조를 기반으로 다른 상태를 설정 합니다.

클라이언트는 Package 클래스 와 상호 작용 하지만 상태 설정을 처리 할 필요가 없으며 클라이언트가해야 할 일은 다음 또는 이전 상태로 이동하는 것입니다.

다음으로 다음 시그니처를 가진 세 가지 메서드 가있는 PackageState 가 있습니다.

public interface PackageState { void next(Package pkg); void prev(Package pkg); void printStatus(); }

이 인터페이스는 각 구체적인 상태 클래스에 의해 구현됩니다.

첫 번째 구체적인 상태는 OrderedState가됩니다 .

public class OrderedState implements PackageState { @Override public void next(Package pkg) { pkg.setState(new DeliveredState()); } @Override public void prev(Package pkg) { System.out.println("The package is in its root state."); } @Override public void printStatus() { System.out.println("Package ordered, not delivered to the office yet."); } }

여기서는 패키지 주문 후 발생할 다음 상태를 가리 킵니다. 정렬 된 상태는 루트 상태이며 명시 적으로 표시합니다. 두 가지 방법 모두 상태 간 전환이 처리되는 방식을 볼 수 있습니다.

DeliveredState 클래스를 살펴 보겠습니다 .

public class DeliveredState implements PackageState { @Override public void next(Package pkg) { pkg.setState(new ReceivedState()); } @Override public void prev(Package pkg) { pkg.setState(new OrderedState()); } @Override public void printStatus() { System.out.println("Package delivered to post office, not received yet."); } }

다시, 우리는 상태 간의 연결을 봅니다. 패키지의 상태가 주문 됨에서 전달됨으로 변경되고 printStatus () 의 메시지도 변경됩니다.

마지막 상태는 ReceivedState입니다 .

public class ReceivedState implements PackageState { @Override public void next(Package pkg) { System.out.println("This package is already received by a client."); } @Override public void prev(Package pkg) { pkg.setState(new DeliveredState()); } }

이것은 우리가 마지막 상태에 도달하는 곳이며 이전 상태로만 롤백 할 수 있습니다.

우리는 이미 한 주가 다른 주에 대해 알고 있기 때문에 약간의 보상이 있음을 알고 있습니다. 우리는 그것들을 단단히 결합시키고 있습니다.

5. 테스트

구현이 어떻게 작동하는지 봅시다. 먼저 설정 전환이 예상대로 작동하는지 확인합니다.

@Test public void givenNewPackage_whenPackageReceived_thenStateReceived() { Package pkg = new Package(); assertThat(pkg.getState(), instanceOf(OrderedState.class)); pkg.nextState(); assertThat(pkg.getState(), instanceOf(DeliveredState.class)); pkg.nextState(); assertThat(pkg.getState(), instanceOf(ReceivedState.class)); }

그런 다음 패키지가 상태로 되돌아 갈 수 있는지 빠르게 확인합니다.

@Test public void givenDeliveredPackage_whenPrevState_thenStateOrdered() { Package pkg = new Package(); pkg.setState(new DeliveredState()); pkg.previousState(); assertThat(pkg.getState(), instanceOf(OrderedState.class)); }

그런 다음 상태 변경을 확인하고 printStatus () 메서드의 구현이 런타임에 구현을 변경하는 방법을 살펴 보겠습니다 .

public class StateDemo { public static void main(String[] args) { Package pkg = new Package(); pkg.printStatus(); pkg.nextState(); pkg.printStatus(); pkg.nextState(); pkg.printStatus(); pkg.nextState(); pkg.printStatus(); } }

그러면 다음과 같은 결과가 나옵니다.

Package ordered, not delivered to the office yet. Package delivered to post office, not received yet. Package was received by client. This package is already received by a client. Package was received by client.

컨텍스트의 상태를 변경함에 따라 동작이 변경되었지만 클래스는 동일하게 유지됩니다. 뿐만 아니라 우리가 사용하는 API.

또한 상태 간 전환이 발생하여 클래스가 상태와 결과적으로 동작을 변경했습니다.

6. 단점

상태 패턴의 단점은 상태 간 전환을 구현할 때의 보상입니다. 이는 상태를 하드 코딩하게하여 일반적으로 나쁜 습관입니다.

그러나 우리의 필요와 요구 사항에 따라 문제가 될 수도 있고 아닐 수도 있습니다.

7. 상태 대 전략 패턴

두 디자인 패턴은 매우 유사하지만 UML 다이어그램은 동일하며 그 뒤에있는 아이디어가 약간 다릅니다.

첫째, 전략 패턴은 상호 교환 가능한 알고리즘 제품군을 정의합니다 . 일반적으로 동일한 목표를 달성하지만 다른 구현 (예 : 정렬 또는 렌더링 알고리즘)을 사용합니다.

상태 패턴에서 동작은 실제 상태에 따라 완전히 변경 될 수 있습니다 .

다음으로, 전략에서 클라이언트는 명시 적으로 사용하고 변경할 수있는 가능한 전략을 알고 있어야합니다. 상태 패턴에서는 각 상태가 다른 상태에 연결되어 Finite State Machine에서와 같이 흐름을 생성합니다.

8. 결론

상태 디자인 패턴은 원시 if / else 문피하고 싶을 때 좋습니다 . 대신, 논리를 추출하여 클래스를 분리 하고 컨텍스트 개체가 동작 을 상태 클래스에 구현 된 메서드에 위임 하도록합니다. 게다가 하나의 상태가 컨텍스트의 상태를 변경할 수있는 상태 간의 전환을 활용할 수 있습니다.

일반적으로이 디자인 패턴은 비교적 단순한 애플리케이션에 적합하지만 더 고급 접근 방식을 위해 Spring의 State Machine 튜토리얼을 살펴볼 수 있습니다.

평소와 같이 전체 코드는 GitHub 프로젝트에서 사용할 수 있습니다.