在上篇文章中我们说到创建型设计模式。 现在我们来看看行为设计模式。 这些模式关注我们的对象如何相互交互或者我们如何与它们交互。
责任链模式
责任链模式允许对象实现一个公共接口,并且每个实现都可以在适当的情况下委托给下一个实现。 然后,这允许我们构建一个实现链,其中每个实现在调用链中的下一个元素之前或之后执行一些操作:
例如:
interface ChainOfResponsibility {
void perform();
}
class LoggingChain {
private ChainOfResponsibility delegate;
public void perform() {
System.out.println("Starting chain");
delegate.perform();
System.out.println("Ending chain");
}
}
在这里,我们可以看到一个示例,其中我们的实现在委托调用之前和之后打印出来。
我们没有必要调用委托。我们可以决定我们不应该这样做,而是提前终止链。 例如,如果有一些输入参数,我们可以验证它们并在它们无效时提前终止。
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);
}
}
命令模式
命令模式允许我们将一些具体的行为——或命令——封装在一个公共接口后面,这样它们就可以在运行时被正确地触发。
通常我们会有一个 Command 接口,一个接收命令实例的 Receiver 实例,以及一个负责调用正确命令实例的 Invoker。 然后,我们可以定义 Command 接口的不同实例来在接收器上执行不同的操作:
interface DoorCommand {
perform(Door door);
}
class OpenDoorCommand implements DoorCommand {
public void perform(Door door) {
door.setState("open");
}
}
在这里,我们有一个命令实现,它将门作为接收器,并导致门“打开”。 然后,我们的调用者可以在希望打开给定门时调用此命令,并且该命令封装了如何执行此操作。
将来,我们可能需要更改我们的 OpenDoorCommand 以检查门是否首先被锁定。 此更改将完全在命令中,并且接收者和调用者类不需要进行任何更改。
JVM实例
这种模式的一个非常常见的例子是 Swing 中的 Action 类:
Action saveAction = new SaveAction();
button = new JButton(saveAction)
这里,SaveAction 是命令,使用这个类的 Swing JButton 组件是调用者,Action 实现被调用,ActionEvent 作为接收者。
迭代器模式
迭代器模式允许我们跨集合中的元素工作并依次与每个元素交互。 我们使用它来编写对某些元素进行任意迭代的函数,而不考虑它们来自何处。 源可以是有序列表、无序集或无限流:
void printAll<T>(Iterator<T> iter) {
while (iter.hasNext()) {
System.out.println(iter.next());
}
}
JVM实例
所有 JVM 标准集合都通过公开一个 iterator() 方法来实现迭代器模式,该方法在集合中的元素上返回一个 Iterator。 流也实现了相同的方法,除了在这种情况下,它可能是一个无限流,因此迭代器可能永远不会终止。
备忘录模式
备忘录模式允许我们编写能够改变状态的对象,然后恢复到之前的状态。 本质上是对象状态的“撤消”功能。
这可以通过在任何时候调用 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。 这允许在单个操作中保存和恢复整个状态,而不必单独保存每个字段。
虚拟机实例
JavaServer Faces 提供了一个名为 StateHolder 的接口,允许实现者保存和恢复其状态。 有几个标准组件可以实现这一点,包括单独的组件——例如,HtmlInputFile、HtmlInputText 或 HtmlSelectManyCheckbox——以及复合组件,例如 HtmlForm。
观察者模式
观察者模式允许一个对象向其他人表明发生了变化。 通常我们会有一个 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);
}
}
}
这需要一组事件侦听器,并在每次状态随新状态值更改时调用每个侦听器。
虚拟机实例
java 有一对标准的类允许我们这样做——java.beans.PropertyChangeSupport 和 java.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.Observer 和 java.util.Observable。 但是,由于不灵活且不可靠,这些在 Java 9 中已被弃用。
策略模式
策略模式允许我们编写通用代码,然后将特定策略插入其中,为我们的具体情况提供所需的特定行为。
这通常通过一个代表策略的接口来实现。 然后,客户端代码能够根据具体情况的需要编写实现此接口的具体类。 例如,我们可能有一个系统,我们需要通知最终用户并将通知机制实现为可插拔策略:
interface NotificationStrategy {
void notify(User user, Message message);
}
class EmailNotificationStrategy implements NotificationStrategy {
....
}
class DingTalkNotificationStrategy implements NotificationStrategy {
....
}
然后,我们可以在运行时准确地决定实际使用这些策略中的哪一个来将此消息发送给该用户。 我们还可以编写新的策略来使用,而对系统的其余部分影响最小。
虚拟机实例
标准 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());
模板方法模式
当我们想要协调几种不同的方法一起工作时,使用模板方法模式。 我们将使用模板方法和一组一个或多个抽象方法定义一个基类——要么未实现,要么以某些默认行为实现。 然后模板方法以固定模式调用这些抽象方法。 然后我们的代码实现这个类的一个子类,并根据需要实现这些抽象方法:
class Component {
public void render() {
doRender();
addEventListeners();
syncData();
}
protected abstract void doRender();
protected void addEventListeners() {}
protected void syncData() {}
}
在这里,我们有一些任意的 UI 组件。 我们的子类将实现 doRender() 方法来实际渲染组件。 我们还可以选择实现 addEventListeners() 和 syncData() 方法。 当我们的 UI 框架呈现这个组件时,它将保证所有三个都以正确的顺序被调用。
虚拟机实例
Java 集合使用的 AbstractList、AbstractSet 和 AbstractMap 有很多这种模式的例子。 例如, indexOf() 和 lastIndexOf() 方法都根据 listIterator() 方法工作,该方法具有默认实现,但在某些子类中会被覆盖。 同样, add(T) 和 addAll(int, T) 方法都根据 add(int, T) 方法工作,该方法没有默认实现,需要由子类实现。
Java IO 也在 InputStream、OutputStream、Reader 和 Writer 中使用了这种模式。 例如,InputStream 类有几个以 read(byte[], int, int) 方式工作的方法,需要子类来实现。
访问者模式
访问者模式允许我们的代码以类型安全的方式处理各种子类,而无需求助于 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 调用适当的方法,在 AdminUser 和 Superuser 中也会这样做。 然后,我们可以根据需要编写访问者以使用这些:
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 可能有权限,但这需要在用户本身中查找。
虚拟机实例
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);
}
});
结论
在本文中,我们了解了用于对象行为的各种设计模式。 我们还查看了在核心 JVM 中使用的这些模式的示例,因此我们可以看到它们以许多应用程序已经受益的方式使用。
参考文献:
https://www.baeldung.com/java-behavioral-patterns-jdk
https://gameprogrammingpatterns.com/behavioral-patterns.html
https://en.wikipedia.org/wiki/Behavioral_pattern