设计模式之观察者模式

观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到并自动更新。

来解析这个定义:

  1. “一对多依赖”说明被观察的主题需要维护一个观察者列表,当然要提供加入列表和从列表删除的能力,可以命名为attach、detach方法,来实现观察者的添加和移除。
  2. 对象改变状态”说明了被观察者有一个被关注的参数属性。
  3. 一个对象改变状态时,它的所有依赖者都会收到并自动更新”说明当主题发生变化时,依赖者有一个方法可以被触发执行更新操作,所谓“自动”指依赖者不必要主动调用,而是被动调用,因此依赖者需要有一个被调用的方法,可以命名为update。
  4. 结合关联抽象的经验,所有依赖者应该有一个共同的抽象,给到主题维护

可以初步得到以下类图。

学习设计模式,一个关键的自我校验,是能够自己给自己出题,构建场景,并通过目标设计模式来解决。

💡
新闻订阅(RSS 订阅)是一个日常生活中经常碰到的场景,这个场景中新闻发布者(NewsPublisher) 是被观察者(Subject),而 订阅者(Subscriber) 是观察者(Observer)。当新闻发布者发布新新闻时,所有订阅者都会收到通知并更新。
  1. 定义观察者接口(Observer)
// 观察者接口
public interface Subscriber {
    void update(String news); // 更新方法,接收新闻内容
}
  1. 定义被观察者(Subject)
import java.util.ArrayList;
import java.util.List;

// 被观察者(新闻发布者)
public class NewsPublisher {
    private List<Subscriber> subscribers = new ArrayList<>(); // 订阅者列表
    private String latestNews; // 最新新闻

    // 添加订阅者
    public void attach(Subscriber subscriber) {
        subscribers.add(subscriber);
    }

    // 移除订阅者
    public void detach(Subscriber subscriber) {
        subscribers.remove(subscriber);
    }

    // 通知所有订阅者
    public void notifySubscribers(String news) {
        for (Subscriber subscriber : subscribers) {
            subscriber.update(news);
        }
    }

    // 发布新闻
    public void publishNews(String news) {
        this.latestNews = news;
        System.out.println("新闻发布者发布了新新闻: " + news);
        notifySubscribers(news); // 通知所有订阅者
    }
}
  1. 实现具体观察者(Concrete Observer)
// 具体观察者(订阅者)
public class EmailSubscriber implements Subscriber {
    private String name;

    public EmailSubscriber(String name) {
        this.name = name;
    }

    @Override
    public void update(String news) {
        System.out.println(name + " 收到新新闻: " + news);
    }
}
  1. 测试代码
public class ObserverPatternDemo {
    public static void main(String[] args) {
        // 创建新闻发布者
        NewsPublisher publisher = new NewsPublisher();

        // 创建订阅者
        Subscriber subscriber1 = new EmailSubscriber("订阅者A");
        Subscriber subscriber2 = new EmailSubscriber("订阅者B");

        // 订阅新闻
        publisher.attach(subscriber1);
        publisher.attach(subscriber2);

        // 发布新闻
        publisher.publishNews("Java 17 正式发布!");
        publisher.publishNews("人工智能技术新突破!");

        // 取消订阅
        publisher.detach(subscriber2);

        // 再次发布新闻
        publisher.publishNews("观察者模式详解!");
    }
}

至此观察者模式的实例已经展示出来了,有没有发现,Subject是一个具体的类,而不是一个抽象,在观察者模式中,Subject 是否应该设计为抽象类或接口,取决于具体的应用场景和设计需求。

  1. Subject作为具体类的场景,就是上面的示例,这种场景中,Subject的行为是固定的,且不需要扩展和修改,可以直接将其设计为具体类,方便快速实现。
  2. Subject 作为抽象类或接口的场景,如果 Subject 的行为需要支持多种实现或扩展,应该将其设计为抽象类或接口。例如有多个不同的新闻发布者,或者未来可能增加新的通知方式,就可以通过抽象来与具体类进行解耦。先看看接口实现。
// Subject 作为接口
public interface Subject {
    void attach(Subscriber subscriber);
    void detach(Subscriber subscriber);
    void notifySubscribers(String news);
}

// 具体 Subject 实现
public class NewsPublisher implements Subject {
    private List<Subscriber> subscribers = new ArrayList<>();

    @Override
    public void attach(Subscriber subscriber) {
        subscribers.add(subscriber);
    }

    @Override
    public void detach(Subscriber subscriber) {
        subscribers.remove(subscriber);
    }

    @Override
    public void notifySubscribers(String news) {
        for (Subscriber subscriber : subscribers) {
            subscriber.update(news);
        }
    }

    public void publishNews(String news) {
        System.out.println("发布新闻: " + news);
        notifySubscribers(news);
    }
}
  1. Subject 作为抽象类的场景,与接口的区别是,多个Subject有可以共享的部分逻辑,例如维护订阅者列表。
// Subject 作为抽象类
public abstract class Subject {
    private List<Subscriber> subscribers = new ArrayList<>();

    public void attach(Subscriber subscriber) {
        subscribers.add(subscriber);
    }

    public void detach(Subscriber subscriber) {
        subscribers.remove(subscriber);
    }

    public void notifySubscribers(String news) {
        for (Subscriber subscriber : subscribers) {
            subscriber.update(news);
        }
    }

    public abstract void publishNews(String news); // 子类实现具体发布逻辑
}

// 具体 Subject 实现
public class NewsPublisher extends Subject {
    @Override
    public void publishNews(String news) {
        System.out.println("发布新闻: " + news);
        notifySubscribers(news);
    }
}

那么何时使用抽象类或接口。

  • 使用接口
    • 当需要支持多继承时(Java 中类只能单继承,但可以实现多个接口)。
    • 当 Subject 的行为完全由子类决定时。
    • 当希望将 Subject 的定义与实现完全解耦时。
  • 使用抽象类
    • 当多个 Subject 实现共享部分逻辑时。
    • 当需要为子类提供默认实现时。
    • 当 Subject 的行为部分固定、部分需要扩展时。

在实际生产环境中,某些观察者只对特定类型的新闻感兴趣(例如科技新闻、体育新闻),可以改进设计以支持更细粒度的通知。如果 NewsPublisher 和 Subscriber 在多线程环境下使用(例如新闻发布者和订阅者运行在不同的线程中),需要确保线程安全。

当前的实现是同步通知,即 notifySubscribers 会阻塞直到所有观察者处理完新闻。如果观察者的处理逻辑较慢,可能会影响性能,可使用异步机制(如线程池或消息队列)来通知观察者。

某些场景下,可能需要为观察者设置优先级(例如 VIP 订阅者优先接收新闻)。可在 Subscriber 中增加优先级字段,并在通知时按优先级排序。

当前的实现中,观察者需要直接依赖被观察者。如果希望进一步解耦,可以使用事件总线(Event Bus)或消息队列(Message Queue)作为中间层,引入事件总线,被观察者发布事件,观察者订阅事件。

Read more

痛风带来的思考

昨晚一罐冰啤酒下去,睡觉时就感觉脚踝隐隐发作,果然早上起床直接下不来地。跟崴脚的感觉十分相似,无法行走,只能坐在一起上滑动,公司上班也去不了了,呆呆得躺在家里,下午疼痛感加剧,整个心思都在左脚的疼痛上,没有其他任何多余的精力去关注其他事情,而此刻的阳台,乃最美人间四月天,春日的微风吹拂着阳台的花儿,温暖的阳光抛洒下来,一切都如此惬意,而我却无心欣赏。 人在健康时,生活中有好多问题,但人在不健康时,生活中只剩一个问题。 我对这句话的理解更深刻了。人是健忘的,在疫情期间、在手术期间,这种感悟其实已经很深刻了,但是病情好转之后,人还是会被日常的琐碎、工作的烦扰搅乱心绪,没有专注的去享受生活本身的美好。 幸福的秘密在于,去享受我们所拥有的,而不是顽固的去追求所没有拥有的。阳光、草木、微风,都是幸福的玩意儿,应尽情的享受。 再等两天,脚完全恢复好了,身体健健康康后,我要以更轻盈的姿态去生活,不纠结他人的看法,不执着别人的认可,关注自己的能力,享受拥有的生后。 还有一个反省,针对咖啡、酒、烟,

By 李浩

设计模式之命令模式

命令模式将“请求”封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象,命令模式也支持可撤销的操作。 来解析这个定义: 1. “将一个请求封装为一个对象”,请求原本是一个方法,现在要封装成一个对象,说明要新增类来完成。 2. “可以用不同的请求对客户进行参数化”,说明是将命令对象作为参数进行传递。 3. “队列”说明需要维护命令多个命令的列表队列。 4. “撤销”说明有命令对象有undo撤销方法。 命令模式在设计模式中,算是一个比较不好理解的模式,很重要的原因是不清楚设计意图,不清楚不用这个模式前有何问题,这个模式带了哪些好处,能解决什么问题。 上一篇状态模式中,看到了状态模式抽离的是状态(属性),向上提成状态对象。有了这个基础,再来理解命令模式就相对简单了。命令模式抽离的是行为(方法),向上提成命令对象。 两者都通过“对象化”来解耦和扩展系统,但解决的问题不同: * 状态模式:处理对象内部状态驱动的行为变化。 * 命令模式:处理行为请求的封装与调度。 💡智能家居遥控器,假设我们有一个智能家居遥控器,可以控制 灯(Light) 和

By 李浩

设计模式之状态模式

状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。 来解析这个定义: 1. “内部状态”表明对象内部有一个属性来表示状态。 2. “内部状态改变时改变它的行为,对象看起来好像修改了它的类”说明状态改变后对象的行为发生了非常大的变化,不像是同一类的行为。 从目前的分析中似乎无法推导出状态模式的类图结构。 从实际的例子出来,来看看状态模式是如何演进而来。 💡我们有一个文档审批系统,文档有以下状态和转换: 1. 草稿(Draft) → 提交 → 待审批(PendingReview) 2. 待审批 → 批准 → 已发布(Published) 3. 待审批 → 拒绝 → 草稿 4. 已发布 → 撤回 → 草稿 从直觉出发,会使用条件语句实现需求逻辑。 public class Document { private String state = "DRAFT"; // 初始状态为草稿 public void submit(

By 李浩

设计模式之代理模式

代理模式为另一个对象提供一个替身或占位符以控制对这个对象的访问。 解析这个定义: 1. “替身”表明在客户端看来,代理类与被代理类是同一类别,对客户端来说看上去没什么区别,依然能够满足诉求。如此可以看出代理类与被代理类来自同一个超类。 2. “控制对这个对象的访问”,能够控制访问,说明前提是能够访问,才能在访问之前做这个限制,即代理持有对真实对象的引用(或能创建它)。 当然可能会质疑,用继承的方式不是也能完成目标吗,用代理类去继承被代理类,然后重写方法,加入控制逻辑。但这违背了"组合优于继承"原则,代理类与被代理类强耦合。 💡我们要开发一个图片查看器,需求如下: 1. 图片加载开销大(从磁盘或网络加载耗时),希望首次显示时才加载(延迟加载)。 2. 某些图片需要权限校验,只有授权用户才能查看。 3. 客户端代码应统一接口,无需关心是直接加载图片还是通过代理。 // 1. 抽象接口(Subject) interface Image { void display(); } // 2. 真实对象(RealSubject)

By 李浩