1. 소개

최근에 우리는 생성 디자인 패턴  과 JVM 및 기타 핵심 라이브러리 내에서 찾을 수있는 위치를 살펴 보았습니다 . 이제 우리는 Behavioral Design Patterns 를 살펴볼 것 입니다. 이것들은 우리의 물체가 서로 어떻게 상호 작용하는지 또는 우리가 그것들과 어떻게 상호 작용하는지에 초점을 둡니다.

2. 책임의 사슬

책임의 체인 객체가 공통 인터페이스를 구현하고 각 구현을 위해 다음 하나 해당되는 경우에 위임하는 패턴이 있습니다. 이를 통해 구현 체인을 구축 할 수 있습니다. 여기서 각 구현은 체인의 다음 요소에 대한 호출 전후에 일부 작업을 수행합니다 .

interface ChainOfResponsibility {
    void perform();
}
class LoggingChain {
    private ChainOfResponsibility delegate;

    public void perform() {
        System.out.println("Starting chain");
        delegate.perform();
        System.out.println("Ending chain");
    }
}

여기에서 우리의 구현이 델리게이트 호출 전후에 출력되는 예제를 볼 수 있습니다.

우리는 대리인을 부를 필요가 없습니다. 그렇게하지 말아야한다고 결정하고 대신 체인을 일찍 종료 할 수 있습니다. 예를 들어 입력 매개 변수가있는 경우 유효성을 검사하고 유효하지 않은 경우 조기에 종료 할 수 있습니다.

2.1. JVM의 예

서블릿 필터 는 이러한 방식으로 작동하는 JEE 에코 시스템의 예입니다. 단일 인스턴스는 서블릿 요청 및 응답을 수신하고 FilterChain 인스턴스는 전체 필터 체인을 나타냅니다. 각각은 작업을 수행 한 다음 체인을 종료하거나 chain.doFilter ()호출 하여 다음 필터에 제어를 전달해야합니다 .

public class AuthenticatingFilter implements Filter {
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
      throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        if (!"MyAuthToken".equals(httpRequest.getHeader("X-Auth-Token")) {
             return;
        }
        chain.doFilter(request, response);
    }
}

3. 명령

명령 또는 명령 - - 제대로 실행시 트리거 될 수 있도록 공통 인터페이스, 뒤에 패턴은 우리가 어떤 구체적인 행동을 캡슐화 할 수 있습니다.

일반적으로 Command 인터페이스, 명령 인스턴스를 수신하는 Receiver 인스턴스 및 올바른 명령 인스턴스를 호출하는 Invoker가 있습니다. 그런 다음 Command 인터페이스의 다른 인스턴스를 정의하여 수신자에 대해 다른 작업을 수행 할 수 있습니다 .

interface DoorCommand {
    perform(Door door);
}
class OpenDoorCommand implements DoorCommand {
    public void perform(Door door) {
        door.setState("open");
    }
}

여기에는 Door 를 수신기로 사용하여 문이 "열리게" 되는 명령 구현 이 있습니다. 호출자는 주어진 문을 열고 자 할 때이 명령을 호출 할 수 있으며 명령은이를 수행하는 방법을 캡슐화합니다.

앞으로 문이 먼저 잠겨 있지 않은지 확인하기 위해 OpenDoorCommand변경해야 할 수도 있습니다 . 이 변경 사항은 전적으로 명령 내에서 이루어지며 수신자 및 호출자 클래스는 변경할 필요가 없습니다.

3.1. JVM의 예

이 패턴의 매우 일반적인 예는 Swing 내의 Action 클래스입니다.

Action saveAction = new SaveAction();
button = new JButton(saveAction)

여기서 SaveAction 은 명령이고, 이 클래스를 사용 하는 Swing JButton 구성 요소는 호출자이며 Action 구현은 수신자로 ActionEvent 를 사용하여 호출됩니다 .

4. 반복자

반복자 패턴을 사용하면 컬렉션의 요소에 대해 작업하고 차례로 상호 작용할 수 있습니다. 우리는 이것이 어디에서 왔는지에 관계없이 일부 요소에 대해 랜덤의 반복기를 취하는 함수를 작성하는 데 사용합니다 . 소스는 정렬 된 List, 정렬되지 않은 세트 또는 무한 스트림 일 수 있습니다.

void printAll<T>(Iterator<T> iter) {
    while (iter.hasNext()) {
        System.out.println(iter.next());
    }
}

4.1. JVM의 예

모든 JVM 표준 컬렉션 은 컬렉션의 요소에 대해 Iterator <T> 를 반환하는 iterator () 메서드노출 하여 Iterator 패턴구현합니다 . 스트림은 또한 동일한 메서드를 구현합니다. 단,이 경우 무한 스트림 일 수 있으므로 반복기가 종료되지 않을 수 있습니다.

5. 메멘토

메멘토 패턴은 이전 상태로 되돌리기 다시 변경 상태로 할 수 있습니다, 및 쓰기 객체에 우리를 할 수 있습니다. 본질적으로 객체 상태에 대한 "실행 취소"기능입니다.

setter가 호출 될 때마다 이전 상태를 저장하여 비교적 쉽게 구현할 수 있습니다.

class Undoable {
    private String value;
    private String previous;

    public void setValue(String newValue) {
        this.previous = this.value;
        this.value = newValue;
    }

    public void restoreState() {
        if (this.previous != null) {
            this.value = this.previous;
            this.previous = null;
        }
    }
}

이렇게하면 개체에 적용된 마지막 변경 사항을 취소 할 수 있습니다.

이것은 종종 전체 개체 상태를 Memento라고하는 단일 개체로 래핑하여 구현됩니다. 이를 통해 모든 필드를 개별적으로 저장할 필요없이 전체 상태를 단일 작업으로 저장하고 복원 할 수 있습니다.

5.1. JVM의 예

JavaServer Faces구현자가 자신의 상태를 저장하고 복원 할 수 있도록 StateHolder 라는 인터페이스를 제공합니다 . 이를 구현하는 여러 표준 구성 요소가 있습니다. 개별 구성 요소 (예 : HtmlInputFile , HtmlInputText 또는 HtmlSelectManyCheckbox) HtmlForm 과 같은 복합 구성 요소로 구성됩니다.

6. 관찰자

관찰자의 목적은 변화가 일어난 것을 다른 사람에게 표시하는 패턴이 있습니다. 일반적으로 주제 (객체 방출 이벤트)와 일련의 관찰자 (이러한 이벤트를 수신하는 객체)가 있습니다. 관찰자는 변경 사항에 대한 정보를 받고자하는 주제에 등록합니다. 이 일이 발생하면 주제에서 발생하는 모든 변경으로 인해 관찰자에게 다음과 같은 정보가 전달됩니다 .

class Observable {
    private String state;
    private Set<Consumer<String>> listeners = new HashSet<>;

    public void addListener(Consumer<String> listener) {
        this.listeners.add(listener);
    }

    public void setState(String newState) {
        this.state = state;
        for (Consumer<String> listener : listeners) {
            listener.accept(newState);
        }
    }
}

이것은 이벤트 리스너 세트를 취하고 새로운 상태 값으로 상태가 변경 될 때마다 각각을 호출합니다.

6.1. JVM의 예

Java에는 java.beans.PropertyChangeSupportjava.beans.PropertyChangeListener 등 정확히이를 수행 할 수있는 표준 클래스 쌍이 있습니다 .

PropertyChangeSupport 는 관찰자를 추가 및 제거하고 모든 상태 변경을 알릴 수있는 클래스 역할을합니다. PropertyChangeListener 는 발생한 변경 사항을 수신하기 위해 코드에서 구현할 수있는 인터페이스입니다.

PropertyChangeSupport observable = new PropertyChangeSupport();

// Add some observers to be notified when the value changes
observable.addPropertyChangeListener(evt -> System.out.println("Value changed: " + evt));

// Indicate that the value has changed and notify observers of the new value
observable.firePropertyChange("field", "old value", "new value");

더 적합한 것으로 보이는 또 다른 클래스 쌍이 있습니다. java.util.Observerjava.util.Observable 입니다. 하지만 유연하지 않고 신뢰할 수 없기 때문에 Java 9에서는 더 이상 사용되지 않습니다.

7. 전략

전략 패턴은 우리가 일반적인 코드를 작성하고 우리에게 정확한 경우에 필요한 특정 동작을주고 그것으로 특정 전략을 연결 할 수 있습니다.

이는 일반적으로 전략을 나타내는 인터페이스를 통해 구현됩니다. 그러면 클라이언트 코드는 정확한 경우에 필요한대로이 인터페이스를 구현하는 구체적인 클래스를 작성할 수 있습니다 . 예를 들어 최종 사용자에게 알리고 알림 메커니즘을 플러그 가능한 전략으로 구현해야하는 시스템이있을 수 있습니다.

interface NotificationStrategy {
    void notify(User user, Message message);
}
class EmailNotificationStrategy implements NotificationStrategy {
    ....
}
class SMSNotificationStrategy implements NotificationStrategy {
    ....
}

그런 다음 런타임에이 메시지를이 사용자에게 보내는 데 실제로 사용할 전략을 정확히 결정할 수 있습니다. 시스템의 나머지 부분에 미치는 영향을 최소화하면서 사용할 새로운 전략을 작성할 수도 있습니다.

7.1. JVM의 예

표준 Java 라이브러리는이 패턴을 광범위하게 사용하며, 처음에는 명확하지 않은 방식으로 자주 사용됩니다 . 예를 들어 Java 8에 도입 된 Streams API 는이 패턴을 광범위하게 사용합니다. map () , filter () 및 기타 메서드에 제공되는 람다 는 모두 일반 메서드에 제공되는 플러그 가능한 전략입니다.

하지만 예는 훨씬 더 거슬러 올라갑니다. 비교기 자바 1.2 도입 인터페이스는 필요 컬렉션 내의 소자를 정렬하기 위해 제공 될 수있는 전략이다. 원하는대로 다른 방식으로 동일한 List을 정렬하기 위해 Comparator 의 다른 인스턴스를 제공 할 수 있습니다 .

// Sort by name
Collections.sort(users, new UsersNameComparator());

// Sort by ID
Collections.sort(users, new UsersIdComparator());

8. 템플릿 방법

템플릿 방법 우리가 함께 일하는 여러 가지 방법을 조율 할 때 패턴이 사용됩니다. 템플릿 메서드와 하나 이상의 추상 메서드 집합으로 기본 클래스를 정의 합니다. 구현되지 않았거나 일부 기본 동작으로 구현됩니다. 그런 다음 템플릿 메서드는 고정 된 패턴으로 이러한 추상 메서드를 호출합니다. 그런 다음 코드는이 클래스의 하위 클래스를 구현하고 필요에 따라 이러한 추상 메서드를 구현합니다.

class Component {
    public void render() {
        doRender();
        addEventListeners();
        syncData();
    }

    protected abstract void doRender();

    protected void addEventListeners() {}

    protected void syncData() {}
}

여기에는 랜덤의 UI 구성 요소가 있습니다. 우리의 서브 클래스는 실제로 컴포넌트를 렌더링하기 위해 doRender () 메소드를 구현할 것 입니다. 선택적으로 addEventListeners ()syncData () 메서드를 구현할 수도 있습니다 . UI 프레임 워크가이 컴포넌트를 렌더링 할 때 세 가지 모두 올바른 순서로 호출되도록 보장합니다.

8.1. JVM의 예

AbstractList를 , AbstractSet,AbstractMap 자바 컬렉션에 의해 사용되는이 패턴의 많은 예제가 있습니다. 예를 들어, indexOf ()lastIndexOf () 메서드는 모두 기본 구현이 있지만 일부 하위 클래스에서 재정의되는 listIterator () 메서드 측면에서 작동 합니다. 마찬가지로, add (T)addAll (int, T) 메서드는 모두 기본 구현이없고 하위 클래스에서 구현해야하는 add (int, T) 메서드 측면에서 작동 합니다.

자바 IO는이 패턴을 사용하게 내에서 의 InputStream , OutputStream에 , 리더라이터 . 예를 들어, InputStream 클래스에는 구현할 하위 클래스가 필요한 read (byte [], int, int) 측면에서 작동하는 여러 메서드가 있습니다 .

9. 방문자

방문자 패턴은 우리의 코드에 의존 할 필요없이, 형태 보증 된 방법으로 다양한 서브 클래스를 처리 할 수 있습니다 instanceof를 확인합니다. 지원해야하는 구체적인 하위 클래스마다 하나의 메소드가있는 방문자 인터페이스가 있습니다. 그러면 기본 클래스에 accept (Visitor) 메서드가 있습니다. 하위 클래스는 각각이 방문자에 대해 적절한 메서드를 호출하여 자신을 전달합니다. 그러면 이러한 각 메서드에서 구체적인 동작을 구현할 수 있으며 각 메서드는 구체적인 유형으로 작업 할 것임을 알고 있습니다.

interface UserVisitor<T> {
    T visitStandardUser(StandardUser user);
    T visitAdminUser(AdminUser user);
    T visitSuperuser(Superuser user);
}
class StandardUser {
    public <T> T accept(UserVisitor<T> visitor) {
        return visitor.visitStandardUser(this);
    }
}

여기 에는 세 가지 방문자 방법 이있는 UserVisitor 인터페이스가 있습니다. 우리의 예제 StandardUser 는 적절한 메소드를 호출하고 AdminUserSuperuser 에서도 동일하게 수행됩니다 . 그런 다음 방문자가 필요에 따라 작업하도록 작성할 수 있습니다.

class AuthenticatingVisitor {
    public Boolean visitStandardUser(StandardUser user) {
        return false;
    }
    public Boolean visitAdminUser(AdminUser user) {
        return user.hasPermission("write");
    }
    public Boolean visitSuperuser(Superuser user) {
        return true;
    }
}

우리 StandardUser는 권한이 없다, 우리의 수퍼 유저는 항상 권한을 가지고 있으며, 우리의 관리 사용자가 권한을 가질 수 있지만,이 요구는 사용자 자체에서 조회 할 수 있습니다.

9.1. JVM의 예

Java NIO2 프레임 워크는 Files.walkFileTree ()함께이 패턴을 사용합니다 . 여기 에는 파일 트리 탐색의 다양한 측면을 처리하는 메서드가 있는 FileVisitor 구현이 필요합니다 . 그런 다음 우리 코드는 파일 검색, 일치하는 파일 인쇄, 디렉토리의 많은 파일 처리 또는 디렉토리 내에서 작업해야하는 기타 많은 작업에 이것을 사용할 수 있습니다 .

Files.walkFileTree(startingDir, new SimpleFileVisitor() {
    public FileVisitResult visitFile(Path file, BasicFileAttributes attr) {
        System.out.println("Found file: " + file);
    }

    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
        System.out.println("Found directory: " + dir);
    }
});

10. 결론

이 기사에서는 객체의 동작에 사용되는 다양한 디자인 패턴을 살펴 보았습니다. 또한 코어 JVM 내에서 사용되는 이러한 패턴의 예를 살펴 보았으므로 많은 애플리케이션이 이미 혜택을 받고있는 방식으로 사용중인 것을 확인할 수 있습니다.