设计模式:观察者模式详解与 Java 实现
观察者模式定义了一对多的依赖关系,当一个对象状态改变时所有依赖者都会收到通知。该模式的组成部分(主题与订阅者),通过 Java 代码实现了水果价格变动通知门店的场景,分析了其降低耦合的优势及潜在的死循环风险。文章还探讨了线程安全实现、性能优化及内存泄漏预防等工程实践,并结合购物车业务说明了实际应用场景。最后总结了适用条件,即抽象模型存在一对多关系且需要动态联动时,强调了根据业务抽象能力灵活运用设计原则的重要性。

观察者模式定义了一对多的依赖关系,当一个对象状态改变时所有依赖者都会收到通知。该模式的组成部分(主题与订阅者),通过 Java 代码实现了水果价格变动通知门店的场景,分析了其降低耦合的优势及潜在的死循环风险。文章还探讨了线程安全实现、性能优化及内存泄漏预防等工程实践,并结合购物车业务说明了实际应用场景。最后总结了适用条件,即抽象模型存在一对多关系且需要动态联动时,强调了根据业务抽象能力灵活运用设计原则的重要性。

在软件系统中,对象之间往往存在一对多的依赖关系。当其中一个对象(主题)的状态发生改变时,所有依赖于它的对象(观察者)都需要得到通知并自动更新。这种设计模式被称为观察者模式(Observer Pattern),也称为发布 - 订阅模式(Publish-Subscribe Pattern)。
该模式属于行为型模式,旨在定义对象间的一种一对多的依赖关系,使得当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。在实际业务场景中,如事件驱动架构、GUI 界面响应、消息队列等,观察者模式都有着广泛的应用。
观察者模式主要包含以下四个角色:
抽象主题(Subject): 又称被观察者,是具体主题的父类或接口。它负责存储所有观察者的引用,并提供注册、删除和通知观察者的方法。
抽象观察者(Observer): 为所有具体的观察者定义一个更新接口。当主题发生变化时,调用此接口通知观察者。
具体主题(Concrete Subject): 实现抽象主题接口,存储与具体业务相关的状态信息。当状态改变时,发送通知。
具体观察者(Concrete Observer): 实现抽象观察者接口,维护一个指向具体主题的引用,以便在接收到通知时更新自身状态。
为了更直观地理解,我们构建一个水果店价格变动通知系统。当某种水果(如车厘子)的价格发生变化时,所有订阅了该信息的门店都会收到通知并刷新价格显示。
抽象主题需要维护一个观察者列表,并提供添加、移除和通知的方法。为了保证线程安全,这里使用 CopyOnWriteArrayList。
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Logger;
/**
* 抽象主题类
*/
public abstract class Subject {
private static final Logger logger = Logger.getLogger(Subject.class.getName());
/** 保存订阅者对象集合 */
protected final List<Observer> observerList = new CopyOnWriteArrayList<>();
/**
* 新增订阅者
*/
public boolean attach(Observer observer) {
if (observer != null && !observerList.contains(observer)) {
observerList.add(observer);
logger.info(observer.toString() + " 添加成功");
return true;
}
return false;
}
/**
* 解除已经绑定的订阅者的关系
*/
public boolean detach(Observer observer) {
if (observer != null) {
boolean removed = observerList.remove(observer);
if (removed) {
logger.info(observer.toString() + " 解绑成功");
}
return removed;
}
return false;
}
/**
* 通知所有观察者
*/
protected void notifyObservers() {
for (Observer observer : observerList) {
try {
observer.update(this);
} catch (Exception e) {
logger.warning("通知观察者时发生异常:" + e.getMessage());
}
}
}
}
观察者接口定义了更新方法。在标准模式中通常命名为 update,此处根据业务场景命名为 priceChanged。
/**
* 抽象观察者接口
*/
public interface Observer {
/**
* 当主题状态改变时被调用
* @param subject 触发变化的主题对象
*/
void priceChanged(Subject subject);
}
具体主题持有业务数据(如价格、名称),并在数据变更时主动通知观察者。
/**
* 具体主题:车厘子
*/
public class Cherry extends Subject {
private long price;
private String name;
public Cherry(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public long getPrice() {
return price;
}
/**
* 设置价格,同时触发通知
*/
public void setPrice(long price) {
this.price = price;
// 状态改变,通知所有订阅者
notifyObservers();
}
@Override
public String toString() {
return "Cherry{" + "price=" + price + ", name='" + name + '\' + '";}';
}
}
具体观察者实现更新逻辑,例如门店接收价格更新后打印日志。
/**
* 具体观察者:门店
*/
public class Store implements Observer {
private String storeName;
public Store(String storeName) {
this.storeName = storeName;
}
public String getStoreName() {
return storeName;
}
public void setStoreName(String storeName) {
this.storeName = storeName;
}
private void show(Subject subject) {
System.out.println(this.storeName + " | " + subject.toString());
}
@Override
public void priceChanged(Subject subject) {
System.out.println(this.storeName);
System.out.println("Price Changed - Refreshing price...");
show(subject);
System.out.println("---------------------------------------->");
}
}
通过主函数模拟价格变动流程,验证观察者模式的动态联动效果。
/**
* 测试类
*/
public class Demo {
public static void main(String[] args) {
// 创建主题
Cherry cherry = new Cherry("车厘子");
// 创建观察者
Store one = new Store("门店一");
Store two = new Store("门店二");
// 订阅
cherry.attach(one);
cherry.attach(two);
// 第一次价格变动
cherry.setPrice(100);
// 第二次价格变动
cherry.setPrice(200);
// 门店一退订
cherry.detach(one);
// 第三次价格变动,仅门店二收到通知
cherry.setPrice(99);
}
}
运行结果预期: 前两次价格变动时,门店一和门店二均会收到通知;门店一退订后,第三次变动仅门店二收到通知。
在上述代码中,Subject 使用了 CopyOnWriteArrayList 来存储观察者列表。这是因为在多线程环境下,遍历列表的同时可能会有其他线程调用 attach 或 detach 修改列表。CopyOnWriteArrayList 保证了迭代过程中的可见性和一致性,避免了 ConcurrentModificationException。
虽然观察者模式降低了耦合度,但在高并发或观察者数量巨大的场景下,通知机制可能会成为性能瓶颈。每次状态改变都会遍历所有观察者,如果观察者执行耗时操作,会导致主题阻塞。优化方案包括:
在实际开发中,观察者模式的应用非常广泛。例如在 Android 购物车模块中,当商品数量变化、总价变动或库存更新时,多个 UI 组件(如底部结算栏、商品列表页)需要同步刷新。
// 简化的购物车观察者接口
public interface OnCartObserver {
void onUpdateProduct(int position, CartProductVO product);
void onCartClear();
void onDeleteProduct(int position, int needUpdateCount);
}
// 业务逻辑中注册观察者
public class CartGoodsFragment extends BaseFragment {
private final OnCartObserver onCartObserver = new OnCartObserver() {
@Override
public void onUpdateProduct(int position, CartProductVO product) {
// 更新 UI 逻辑
}
@Override
public void onCartClear() {
// 清空 UI 逻辑
}
@Override
public void onDeleteProduct(int position, int needUpdateCount) {
// 删除逻辑
}
};
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 注册观察者
XX.provideController.addObserver(onCartObserver);
}
@Override
public void onDestroy() {
super.onDestroy();
// 移除观察者防止内存泄漏
XX.provideController.removeObserver(onCartObserver);
}
}
并非所有情况都适合使用观察者模式。以下是推荐的适用条件:
反之,如果依赖关系复杂且嵌套深,或者通知过程涉及大量同步计算,可能需要考虑中介者模式或命令模式等其他方案。
观察者模式是面向对象设计中最重要的模式之一。它通过解耦发布者和订阅者,提高了系统的灵活性和可维护性。然而,设计模式没有银弹,开发者应根据具体业务场景权衡利弊,避免过度设计。理解其本质——状态变更的自动传播,比生搬硬套 UML 图更为重要。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online