Contents

Java中的行为模式

1. 简介

最近我们研究了创建设计模式  以及在 JVM 和其他核心库中的何处可以找到它们。现在我们来看看行为设计模式这些关注于我们的对象如何相互交互或我们如何与它们交互。

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 中的示例

Servlet 过滤器 是 JEE 生态系统中以这种方式工作的一个示例。单个实例接收 servlet 请求和响应,一个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. 迭代器

迭代器模式允许我们跨集合中的元素工作并依次与每个元素交互。我们使用它来编写对某些元素进行任意迭代的函数,而不考虑它们来自何处。源可以是有序列表、无序集或无限流:

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

4.1.JVM 中的示例

所有 JVM 标准集合都通过公开一个*iterator()*方法来实现迭代器模式 ,该方法在集合中的元素上返回一个Iterator<T> 。流也实现了相同的方法,除了在这种情况下,它可能是一个无限流,因此迭代器可能永远不会终止。

5. 纪念品

**Memento 模式允许我们编写能够改变状态的对象,然后恢复到之前的状态。**本质上是对象状态的“撤消”功能。

这可以通过在任何时候调用 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的接口,它允许实现者保存和恢复他们的状态。有几个标准组件可以实现这一点,包括单独的组件(例如HtmlInputFileHtmlInputTextHtmlSelectManyCheckbox)以及复合组件(例如HtmlForm)。

6. 观察者

观察者 模式允许一个对象向其他人表明发生了变化。通常我们会有一个 Subject - 发出事件的对象,和一系列 Observers - 接收这些事件的对象。观察者将向他们希望被告知更改的主题进行注册。一旦发生这种情况,主题中发生的任何变化都会导致观察者被告知

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()*和其他方法的 lambdas都是提供给泛型方法的可插入策略。

不过,例子可以追溯到更远的地方。Java 1.2 中引入的Comparator接口是一种策略,可以根据需要对集合中的元素进行排序。我们可以提供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 中的示例

**Java Collections 使用的AbstractListAbstractSetAbstractMap有很多这种模式的例子。**例如,*indexOf()lastIndexOf()方法都根据listIterator()*方法工作,该方法具有默认实现,但在某些子类中会被覆盖。同样,*add(T)addAll(int, T)方法都根据add(int, T)*方法工作,该方法没有默认实现,需要由子类实现。

Java IO也在InputStreamOutputStreamReaderWriter中使用了这种模式。例如,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从来没有权限,我们的Superuser总是有权限,我们的AdminUser可能有权限,但这需要在用户本身中查找。

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);
    }
});