捕捉变化的风-用观察者模式提升用户体验

本文涉及的产品
应用实时监控服务-可观测链路OpenTelemetry版,每月50GB免费额度
注册配置 MSE Nacos/ZooKeeper,118元/月
Serverless 应用引擎免费试用套餐包,4320000 CU,有效期3个月
简介: 观察者模式是一种行为设计模式,允许对象之间定义一种订阅机制,以便在对象状态变化时通知多个观察者。它广泛应用于实现动态事件处理系统、用户界面元素的交互,或监测状态变化等场景。文章中通过丰富的场景案例,展示了不使用观察者模式可能带来的问题,如紧耦合和难以维护;接着解释了如何应用观察者模式成功解决这些问题,通过主题和观察者的解耦,增强系统的灵活性和可扩展性。进一步解释了观察者模式的工作原理,并介绍了其结构图和运行机制。该模式有助于在维护一致性和实时性方面提供优势,同时促使我们在高层次上分类对象间的交互。最后


一、引言

核心概念

    优雅方案

  • 在现代软件开发中,我们经常面临着如何实现不同组件之间的高效通信动态响应的问题。而观察者模式就是一个重要的设计模式,它提供了一种 elegant 的解决方案。

    观察者模式的核心概念主要包含以下几个关键部分:

 1. 主题(Subject)

  • 也称为被观察者或发布者,是核心组件之一。主题拥有维护一系列观察者的能力,提供注册(添加)和注销(移除)观察者的接口。当主题对象的状态发生变化时,它负责通知所有注册的观察者。

 2. 观察者(Observer)

  • 是一个接口或抽象类,规定了当主题的状态发生变化时应该执行的更新操作。具体观察者实现该接口,定义了在接收到主题状态变化通知时的具体行为。

 3. 具体主题(Concrete Subject)

  • 实现或继承主题接口的类。维护观察者的列表,并实现通知观察者的具体逻辑。

 4. 具体观察者(Concrete Observer)

  • 实现或继承观察者接口的类。当接收到主题的状态变化通知时,具体观察者会按定义的逻辑进行更新或其他响应。

 5. 状态(State)

  • 就是主题对象维护的、观察者感兴趣的数据。当状态发生变化时,主题会触发一个通知流程来更新所有的观察者。

 6. 更新接口(Update Method)

  • 是观察者定义的方法,在被主题通知时被调用用以更新自身状态。

 简单而言

    观察者模式允许对象之间建立一种一对多的依赖关系,当一个对象改变状态时,所有依赖它的对象都会得到通知并且自动更新。这种通知机制使得对象之间的通信更加松散耦合、灵活可扩展。无需直接相互引用,各个对象能够独立变化而互不影响。通过观察者模式,我们能够轻松实现实时更新、动态同步等功能,从而为我们的应用带来更好的用户体验和可维护性。无论是构建响应式界面、事件驱动系统,还是实现即时通信、发布-订阅模式,观察者模式都是一个不可或缺的设计选择。

   接下来,让我们深入探索观察者模式的内部工作原理和实际应用案例,享受软件开发的乐趣吧!

应用场景

 观察者模式在软件设计中的应用场景

 1. 用户界面(UI)交互

  • 当用户操作引发状态变化,比如一个按钮点击导致数据更新,观察者模式确保相关UI组件同步刷新。

 2. 事件监测系统

  • 系统中的某些事件发生时,如文件下载完成或硬件状态变更,观察者模式通知所有订阅者执行适当的动作。

 3. 发布/订阅系统

  • 在消息队列或实时消息服务中,产生消息的发布者与消费消息的订阅者无需知晓对方的存在。

 4. 数据模型与视图同步

  • 在MVC架构中,数据模型的更改需要能够实时地反映到视图上,观察者模式在此场景下保持视图和数据的一致性。

可以解决的问题

    在现代软件开发中,组件间的交互和状态同步是一项常见而又至关重要的挑战。随着业务逻辑的复杂化及用户需求的不断变化,如何设计出灵活、低耦合的系统成为软件工程师面临的一大课题。对于系统中存在的一个实体状态改变需要通知到一个或多个依赖该状态的实体的场景,如何高效率地处理这种状态同步与通知呢?传统的紧耦合联系很难应对系统的迅速变化和扩展,而观察者模式在此场景下应运而生,提供了一种优雅且实用的设计解决方案。

 使用观察者模式可以解决的问题包括但不限于

  1. 解耦系统组件:降低对象之间的直接交互,实现松耦合,从而改善组件间的交互模式。
  2. 实现广播通信:当我们希望状态的改变能够通知到所有相关的依赖者而不是单一对象时,观察者模式提供了有效的机制。
  3. 动态交互:允许系统在运行时动态地新增或移除观察者,无需修改主题或其他观察者的代码。
  4. 推拉模型的支持:提供了推模型(主题向观察者推送详细信息)和拉模型(观察者自行获取其所需要的信息)两种方式。

       

二、场景案例

经典场景:新闻发布系统

    最经典的观察者模式场景之一是“新闻发布系统”。在这个场景中,有多个订阅者(观察者)对新闻感兴趣,他们希望在有新闻发布时能够立即得到通知。系统管理员(主题)负责发布新闻。

2.1 不用设计模式实现

一坨坨代码实现

import java.util.ArrayList;  
import java.util.List;  
  
// 新闻类  
class News {  
    private String content;  
  
    public News(String content) {  
        this.content = content;  
    }  
  
    public String getContent() {  
        return content;  
    }  
}  
  
// 新闻发布器  
class NewsPublisher {  
    private List<NewsSubscriber> subscribers = new ArrayList<>();  
  
    // 订阅新闻  
    public void subscribe(NewsSubscriber subscriber) {  
        subscribers.add(subscriber);  
    }  
  
    // 取消订阅  
    public void unsubscribe(NewsSubscriber subscriber) {  
        subscribers.remove(subscriber);  
    }  
  
    // 发布新闻  
    public void publish(News news) {  
        for (NewsSubscriber subscriber : subscribers) {  
            subscriber.receiveNews(news);  
        }  
    }  
}  
  
// 新闻订阅者接口  
interface NewsSubscriber {  
    void receiveNews(News news);  
}  
  
// 网页新闻订阅者  
class WebNewsSubscriber implements NewsSubscriber {  
    @Override  
    public void receiveNews(News news) {  
        System.out.println("WebNewsSubscriber: News received - " + news.getContent());  
    }  
}  
  
// 邮件新闻订阅者  
class EmailNewsSubscriber implements NewsSubscriber {  
    @Override  
    public void receiveNews(News news) {  
        System.out.println("EmailNewsSubscriber: News received - " + news.getContent());  
    }  
}  
  
// 客户端代码  
public class NewsSystemClient {  
    public static void main(String[] args) {  
        NewsPublisher publisher = new NewsPublisher();  
        NewsSubscriber webSubscriber = new WebNewsSubscriber();  
        NewsSubscriber emailSubscriber = new EmailNewsSubscriber();  
  
        publisher.subscribe(webSubscriber);  
        publisher.subscribe(emailSubscriber);  
  
        News news = new News("Breaking News: World Peace Achieved!");  
        publisher.publish(news);  
    }  
}

image.gif

   在这个实现中,NewsPublisher 类充当了新闻发布的中心角色,它维护了一个订阅者列表。当有新闻发布时,NewsPublisher 会遍历订阅者列表,并调用每个订阅者的 receiveNews 方法来传递新闻。

2.2 存在问题

    上述不使用设计模式实现的新闻发布系统确实存在一些问题,尽管它能够实现基本的新闻发布和接收功能。以下还是存在如紧耦合缺乏灵活性可扩展性差错误处理不足缺乏抽象层次 动态性受限 等问题。

2.3 使用设计模式实现

1. 观察者接口(Observer):定义一个更新方法,当新闻发布时,所有观察者都将调用此方法。

public interface Observer {  
    void update(String news);  
}

image.gif

2. 具体观察者(ConcreteObserver):实现观察者接口,当有新闻发布时,执行具体的操作。例如,将新闻显示在网页上、发送到用户的电子邮箱等。

public class WebObserver implements Observer {  
    @Override  
    public void update(String news) {  
        System.out.println("WebObserver: Display news on website - " + news);  
    }  
}  
  
public class EmailObserver implements Observer {  
    @Override  
    public void update(String news) {  
        System.out.println("EmailObserver: Send news to email - " + news);  
    }  
}

image.gif

3. 主题接口(Subject):定义一个注册观察者、移除观察者和通知观察者的方法。

import java.util.ArrayList;  
import java.util.List;  
  
public interface Subject {  
    void registerObserver(Observer observer);  
    void removeObserver(Observer observer);  
    void notifyObservers(String news);  
}

image.gif

4. 具体主题(ConcreteSubject):实现主题接口,维护一个观察者列表,并在有新闻发布时通知所有观察者。

public class NewsSubject implements Subject {  
    private List<Observer> observers = new ArrayList<>();  
  
    @Override  
    public void registerObserver(Observer observer) {  
        observers.add(observer);  
    }  
  
    @Override  
    public void removeObserver(Observer observer) {  
        observers.remove(observer);  
    }  
  
    @Override  
    public void notifyObservers(String news) {  
        for (Observer observer : observers) {  
            observer.update(news);  
        }  
    }  
}

image.gif

5. 客户端代码(Client):创建主题和观察者对象,并将观察者注册到主题上。当有新闻发布时,主题会通知所有观察者。

public class Client {  
    public static void main(String[] args) {  
        NewsSubject newsSubject = new NewsSubject();  
  
        Observer webObserver = new WebObserver();  
        Observer emailObserver = new EmailObserver();  
  
        newsSubject.registerObserver(webObserver);  
        newsSubject.registerObserver(emailObserver);  
  
        newsSubject.notifyObservers("Breaking News: World Peace Achieved!");  
    }  
}

image.gif

   在这个场景中,新闻发布系统(主题)负责发布新闻,而网页观察者(WebObserver)和电子邮件观察者(EmailObserver)则负责在新闻发布时显示和发送新闻。通过使用观察者模式,系统管理员可以轻松地添加或移除观察者,而无需修改主题代码。此外,当有新闻发布时,所有观察者都会自动收到通知并更新,从而实现了松耦合和可扩展性。

2.4 成功克服

   使用观察者模式在上述示例中成功克服了多个问题,这些问题在使用直接的方法调用和对象间显式交互时可能会出现。以下是观察者模式成功克服的问题:

 1. 紧耦合

  • 观察者模式通过引入抽象和接口,降低了新闻发布器(主题)和订阅者(观察者)之间的耦合度。这意味着如果新闻发布器的内部实现发生变化,只要它继续遵循观察者模式的接口,订阅者的代码就不需要修改。

 2. 缺乏灵活性

  • 观察者模式允许主题和观察者之间的解耦,从而提高了系统的灵活性。这意味着可以更容易地支持多种不同的订阅场景,比如特定的时间间隔接收新闻或只接收特定类型的新闻。

 3. 可扩展性差

  • 通过引入抽象和接口,观察者模式允许更容易地扩展系统。例如,可以轻松地添加新的观察者类型,而不需要修改现有的代码。此外,由于观察者模式的通知机制是异步的,它也可以更好地处理大量观察者的情况,避免性能问题。

 4. 错误处理不足

  • 观察者模式允许在观察者中实现错误处理逻辑。这意味着如果某个订阅者在接收新闻时出现问题,它可以优雅地处理这些错误,而不会影响到新闻发布器或其他订阅者。

 5. 缺乏抽象层次

  • 观察者模式通过引入抽象和接口,为系统提供了更好的抽象层次。这使得系统更易于进行单元测试和维护,因为可以针对抽象接口编写测试,而不是针对具体的实现。

 6. 动态性受限

  • 观察者模式允许观察者在运行时动态地注册和注销,从而提高了系统的动态性。这意味着可以更容易地支持动态添加或删除订阅者的需求。

       

三、工作原理

3.1 结构图和说明

image.png

  • Subject:目标对象,通常具有如下功能。
  • Observer:定义观察者的接又,提供目标通知时对应的更新方法,这个更新方法进行相应的业务处理,可以在这个方法里面回调目标对象,以获取目标对象的数据。
  • ConcreteSubject:具体的目标实现对象,用来维护目标状态,当目标对象的状态发生改变时,通知所有注册的、有效的观察者,让观察者执行相应的处理
  • ConcreteObserver:观察者的具体实现对象,用来接收目标的通知,并进行相应的后续处理,比如更新自身的状态以保持和目标的相应状态一致。

3.2 工作原理详解

 1. 注册与订阅

  • 首先,观察者需要向被观察者注册或订阅感兴趣的事件或主题。这通常是通过调用被观察者的某个注册方法来实现的,该方法将观察者添加到其内部维护的观察者列表中。

 2. 状态变化

  • 当被观察者的状态发生变化时,它会通知所有已注册的观察者。这通常是通过调用一个通知方法来实现的,该方法遍历观察者列表并调用每个观察者的更新方法。

 3. 通知更新

  • 观察者接收到通知后,会根据自己的需要执行相应的操作或更新自己的状态。这通常是通过实现一个更新方法来完成的,该方法在被调用时会根据被观察者的新状态进行相应的处理。

 4. 解耦

  • 观察者模式的核心优势在于它实现了观察者和被观察者之间的解耦。被观察者不需要知道具体的观察者是谁,也不需要关心观察者如何响应状态变化。同样,观察者也不需要知道被观察者的具体实现细节,只需要关心自己感兴趣的事件或主题。

 5. 灵活性和扩展性

  • 由于观察者和被观察者之间的解耦关系,我们可以在不修改现有代码的情况下添加新的观察者或更改观察者的行为。这为软件系统的灵活性和扩展性提供了很好的支持。

3.3 实现步骤

   使用观察者模式实现功能时,你可以按照以下思考步骤进行:

 1. 定义主题和观察者之间的关系

  • 确定哪个对象应当作为主题,即数据变化的源头。
  • 识别哪些对象应当作为观察者,即需要响应数据变化的对象。
  • 设计一种方式让观察者能够订阅并从主题接收更新。

 2. 设计通用接口

  • 为观察者定义统一的接口,以便主题更新数据时能够通知所有观察者。
  • 考虑定义主题接口,描述如何注册、注销观察者,以及怎样发出通知。

 3. 实现注册与移除观察者功能

  • 主题需要提供方法让观察者能够注册自己或者被移除。
  • 主题内部需要有机制跟踪所有注册的观察者。

 4. 定义通知机制

  • 确定更新状态后观察者被通知的机制是推送还是拉取。
  • 设计主题在状态更改时如何通知观察者的逻辑。

 5. 更新观察者状态

  • 观察者接口中应定义如何更新其状态以响应主题状态变化的方法。
  • 实现观察者根据接收到的更新来进行自身状态更改或相应行为的逻辑。

 6. 考虑线程安全与性能问题

  • 如果在多线程环境下,确保主题在通知观察者时的线程安全性。
  • 分析性能问题,确保通知机制不会成为瓶颈。

 7. 实现解耦

  • 观察者和主题应该保持松耦合,以便独立变化而不影响彼此。

 8. 处理异常与错误

  • 确保即便某个观察者处理失败,也不应影响到主题和其他观察者。

 9. 测试

  • 对主题、观察者以及整个通知机制进行测试,确保满足需求。
  • 验证在增加或删除观察者、变更主题状态时,系统行为正确无误。

 10. 细节优化

  • 根据反馈和测试结果优化设计,可能涉及提高性能、简化接口、增强用户体验等。

    在应用观察者模式时,始终需要关注设计的整体清晰度、灵活性以及扩展性,确保最终实现的模式适合应用的上下文环境。

       

四、 优势

4.1 好处和优势

   使用观察者模式可以带来以下明显的好处和优势:

 1. 解耦

  • 观察者模式实现了观察者和被观察者之间的解耦,这意味着两者之间的依赖关系变得更加松散。这种解耦关系有助于提高代码的可维护性和可重用性,因为你可以在不修改被观察者代码的情况下添加或删除观察者。

 2. 灵活性

  • 由于观察者和被观察者之间的解耦关系,你可以灵活地添加、删除或更改观察者,而无需修改被观察者的代码。这为系统的扩展和修改提供了极大的便利。

 3. 动态响应

  • 观察者模式允许被观察者在其状态发生变化时自动通知所有相关的观察者。这种动态响应机制使得系统能够实时地响应变化,从而提高了系统的响应速度和效率。

 4. 简化通信

  • 观察者模式提供了一种简洁而有效的方式来处理不同组件之间的通信。通过观察者和被观察者之间的注册、通知和更新机制,你可以轻松地实现组件之间的通信和协作。

4.2 应用示例

    考虑一个电商平台上的商品定价系统。传统的设计方法可能会要求每个依赖商品价格信息的组件都直接从价格数据库中获取更新。随着系统的发展,这种紧耦合的方式导致了若干问题:每当价格更新逻辑变化时,所有依赖组件都需要作出相应修改;系统的可扩展性差,添加新的依赖组件会带来额外的维护负担。
    采用观察者模式改进后,价格系统作为主题,各个依赖组件(如库存管理、促销引擎、前端显示等)作为观察者。当商品价格更新时,价格系统仅需通知这些观察者。这样,库存管理系统可以自动调整库存采购策略,促销引擎可以同步更新促销活动,用户界面也可以即时显示最新价格。这种方式不仅使得价格更新流程更加清晰,而且让各组件能够更加独立地开发和维护。

4.3 系统性优势

   观察者模式通过解耦观察者和被观察者之间的关系,提高了系统的灵活性、扩展性和可维护性。具体来说:

 1. 灵活性

  • 由于观察者和被观察者之间的解耦关系,你可以在不修改现有代码的情况下添加新的观察者或更改观察者的行为。这为系统的灵活性提供了很好的支持。

 2. 扩展性

  • 观察者模式允许你轻松地扩展系统的功能。例如,你可以添加新的观察者来处理新的事件或主题,而无需修改现有的代码。这种扩展性使得系统能够适应不断变化的需求。

 3. 可维护性

  • 由于观察者模式降低了对象之间的耦合度,代码变得更加清晰和易于维护。当一个对象的状态发生变化时,你只需要修改被观察者的代码,而无需关心与之相关的多个对象的代码。这大大降低了维护成本和出错的可能性。

   综上所述,观察者模式通过解耦观察者和被观察者之间的关系,提高了系统的灵活性、扩展性和可维护性。它简化了对象之间的通信和协作,使得代码更加清晰、简洁和易于维护。因此,在现代软件开发中,观察者模式被广泛应用于处理不同组件之间的高效通信和动态响应问题。

       

五、局限性和注意事项

5.1 局限性与不适用的场景

   尽管观察者模式为软件开发带来了许多好处,但在某些情况下,它也可能存在局限性和不适用的场景:

 1. 复杂的依赖关系

  • 当系统中的依赖关系变得非常复杂时,观察者模式可能会增加理解和维护的难度。如果观察者之间或观察者与被观察者之间存在复杂的交互逻辑,可能会导致代码变得难以理解和维护。

 2. 性能考虑

  • 在大型系统中,如果观察者数量众多,每次被观察者状态变化时都需要通知所有观察者,这可能会导致性能问题。过多的通知操作可能会消耗大量的计算资源和带宽,影响系统的整体性能。

 3. 循环依赖

  • 如果不小心处理,观察者模式可能会导致循环依赖的问题。例如,观察者A订阅了被观察者B的变化,同时被观察者B又订阅了观察者A的变化。这种情况下,当被观察者B的状态发生变化时,它会通知观察者A,而观察者A在更新自己的状态时又会触发被观察者B的通知,从而形成一个无限循环。

 4. 错误处理

  • 在观察者模式中,当通知观察者时,如果被观察者的通知方法抛出异常,这可能会导致整个系统的不稳定。如果没有妥善处理这些异常,可能会导致系统崩溃或不可预知的行为。

5.2 实际应用中的注意事项与建议

 1. 推送 vs 拉取

  • 在通知观察者时,可以选择推送通知(将状态变化的具体数据发送给观察者)或拉取通知(仅通知观察者去主题上获取需要的数据)。推送方式可能会导致观察者接收不需要的数据,而拉取方式可能导致观察者不知道哪些数据有更新。设计时需要根据实际情况选择更合适的通知机制。

 2. 通知顺序

  • 多个观察者注册到同一个主题时,观察者接收通知的具体顺序可能会影响系统行为。如果某种顺序有特定的业务意义,应该在设计中明确其顺序,并在可能的情况下保持这个顺序的一致性。

 3. 通知效率

  • 如果观察者数量较多或者更新操作比较耗时,主题在通知所有观察者时可能会遭遇性能瓶颈。想要缓解这种情况,可以考虑异步通知机制或使用消息队列处理。

 4. 循环依赖

  • 某个观察者在接收到更新通知后,可能会反过来影响主题状态,从而触发新的通知。该情况如果不加以控制,可能会造成循环调用的问题。设计时要注意辨识并处理潜在的循环依赖问题。

 5. 内存管理

  • 主题通常持有对所有观察者的引用,如果不恰当地进行监听和解除监听,可能会导致内存泄漏。需要确保在观察者生命周期结束时,从主题中正确移除其引用。

 6. 异常处理

  • 当观察者在接收通知时发生异常,不应中断整个通知过程。应该处理每个观察者的异常,尽量减少他们对其他观察者和主题的影响。

 7. 状态一致性

  • 确保观察者在任何时候获取的状态都是一致的。这意味着在状态变更和通知期间,应阻止对状态的任何修改,或者采用一些机制(如状态快照)来保持状态的一致性。

 8. 设计模式的组合使用

  • 有时单纯使用观察者模式可能不足以解决所有问题,可能需要与其他设计模式结合使用,比如命令模式、状态模式或策略模式等,以实现更灵活和健壮的设计。

   在遵守这些注意事项和建议的同时,应该记住设计模式不是万能的,不应该强行适配模式。在选择应用观察者模式前,确保它适合当前的问题场景,并充分考虑它可能带来的设计复杂性。

相关文章
|
1月前
|
数据可视化 搜索推荐
如何利用动画效果来提高用户对产品的记忆度?
利用动画效果提高用户对产品的记忆度,需要从多个方面入手
39 1
|
3月前
|
图形学 开发者
透视与正交之外的奇妙视界:深入解析Unity游戏开发中的相机与视角控制艺术,探索打造沉浸式玩家体验的奥秘与技巧
【8月更文挑战第31天】在Unity中,相机不仅是玩家观察游戏世界的窗口,更是塑造氛围和引导注意力的关键工具。通过灵活运用相机系统,开发者能大幅提升游戏的艺术表现力和沉浸感。本文将探讨如何实现多种相机控制,包括第三人称跟随和第一人称视角,并提供实用代码示例。
174 0
|
4月前
|
图形学 开发者
【Unity光照艺术手册】掌握这些技巧,让你的游戏场景瞬间提升档次:从基础光源到全局光照,打造24小时不间断的视觉盛宴——如何运用代码与烘焙创造逼真光影效果全解析
【8月更文挑战第31天】在Unity中,合理的光照与阴影设置对于打造逼真环境至关重要。本文介绍Unity支持的多种光源类型,如定向光、点光源、聚光灯等,并通过具体示例展示如何使用着色器和脚本控制光照强度,模拟不同时间段的光照变化。此外,还介绍了动态和静态阴影、全局光照及光照探针等高级功能,帮助开发者创造丰富多样的光影效果,提升游戏沉浸感。
108 0
|
5月前
|
测试技术
软件复用问题之捕捉领域变化,如何解决
软件复用问题之捕捉领域变化,如何解决
|
6月前
全息近眼显示技术如何实现三维图像再现?
【6月更文挑战第26天】全息近眼显示技术如何实现三维图像再现?
57 4
|
5月前
|
人工智能
Sora信息问题之模拟对象状态变化存在的局限如何解决
Sora信息问题之模拟对象状态变化存在的局限如何解决
49 0
|
7月前
|
开发者
所有消除游戏背后都有一张看不见的网格
所有消除游戏背后都有一张看不见的网格
83 0
|
开发者
所有消除游戏背后那张看不见的网格
观察一下上方的这一系列各种各样的消除游戏的图片,它们都有着这样的一个共同点,就是都是按照行列进行布局,有 7 行 7 列,有 10 行 10 列的。这样的行列布局是不是特别的像一个“网格”?这就是我们今天要讲的,所有消除游戏背后都有的那张看不见的“网格”。
115 0
|
存储 传感器 编解码
设计全新动作捕捉,构建水下3D系统,《阿凡达2》的特效背后藏了哪些秘密?
上映一周,票房破5亿。 暌违13年,詹姆斯·卡梅隆终于带来了《阿凡达2:水之道》。
1700 0
设计全新动作捕捉,构建水下3D系统,《阿凡达2》的特效背后藏了哪些秘密?
|
C# 计算机视觉
案例分享:Qt+C#轨道交通行业高性能高流畅度模拟火车移动图像控件
案例分享:Qt+C#轨道交通行业高性能高流畅度模拟火车移动图像控件