26、完整项目实战(二):架构设计——使用单例管理配置、工厂创建通知器、观察者监听状态

好,咱们接着往下走。上一章我们把项目背景和需求理清楚了,这一章,我打算直接动手做架构设计。

说白了,架构设计就是给代码“搭骨架”。骨架搭得稳,后面往里填肉(写业务逻辑)才不慌。我个人习惯,在动手写任何一行代码之前,先想清楚三个问题:

  • 配置怎么管?—— 全局只有一份,不能乱,不能丢。
  • 通知器怎么创建?—— 以后可能会加新的通知方式,不能每次都改核心代码。
  • 状态变化怎么通知?—— 一个变了,所有关心它的地方都得知道。

你想想看,这三个问题,正好对应了我们这节课要讲的三个模式:单例模式工厂模式观察者模式。不是硬套模式,而是问题本身就在那里,模式只是恰好能解决它。

一、单例模式:全局配置管理中心

先聊配置。我在项目中遇到过好几次这种坑:配置散落在各个类里,有的用静态变量,有的用配置文件,有的干脆写死在代码里。结果改一个配置,得翻遍整个项目。嗯,这滋味不好受。

所以这次,我决定用单例模式来管理所有配置。为什么用单例?因为配置信息在整个应用生命周期里,只需要一份就够了。你new出来十个配置对象,内容都一样,浪费内存不说,还容易搞出数据不一致的问题。

来看代码:

public class ConfigManager {
    // 私有静态实例,volatile保证多线程可见性
    private static volatile ConfigManager instance;
    
    // 配置存储
    private Properties properties;
    
    // 私有构造器,防止外部new
    private ConfigManager() {
        properties = new Properties();
        loadConfig();
    }
    
    // 双重检查锁,保证线程安全
    public static ConfigManager getInstance() {
        if (instance == null) {
            synchronized (ConfigManager.class) {
                if (instance == null) {
                    instance = new ConfigManager();
                }
            }
        }
        return instance;
    }
    
    private void loadConfig() {
        // 实际项目中,这里会从文件或数据库加载
        properties.setProperty("app.name", "NotificationSystem");
        properties.setProperty("app.version", "1.0.0");
        properties.setProperty("notifier.email.enabled", "true");
        properties.setProperty("notifier.sms.enabled", "false");
    }
    
    public String getConfig(String key) {
        return properties.getProperty(key);
    }
}
我的小建议:双重检查锁(Double-Checked Locking)是单例模式里比较稳妥的做法。既保证了懒加载,又避免了每次getInstance都加锁的性能损耗。不过要注意,instance变量一定要加volatile,否则指令重排可能让你拿到一个半初始化的对象。

你可能会问:为什么不直接用枚举实现单例?嗯,枚举确实更简洁,而且天然防反射攻击。但有些场景下,比如需要继承父类或者延迟加载复杂配置,我还是倾向于用双重检查锁。说白了,没有银弹,看场景选。

二、工厂模式:灵活创建通知器

接下来是通知器的创建。我们的系统支持邮件、短信、站内信等多种通知方式。如果直接在业务代码里写if-else来判断创建哪种通知器,那以后每加一种新方式,就得改一堆代码。这违反了开闭原则——对扩展开放,对修改关闭。

工厂模式就是干这个的。它把“创建对象”这件事封装起来,调用方只需要告诉工厂“我要什么类型的”,工厂负责把对象造出来。

// 通知器接口
public interface Notifier {
    void send(String message);
}

// 邮件通知器
public class EmailNotifier implements Notifier {
    @Override
    public void send(String message) {
        System.out.println("发送邮件: " + message);
    }
}

// 短信通知器
public class SmsNotifier implements Notifier {
    @Override
    public void send(String message) {
        System.out.println("发送短信: " + message);
    }
}

// 通知器工厂
public class NotifierFactory {
    public static Notifier createNotifier(String type) {
        // 从配置中心读取是否启用
        ConfigManager config = ConfigManager.getInstance();
        
        switch (type.toLowerCase()) {
            case "email":
                if ("true".equals(config.getConfig("notifier.email.enabled"))) {
                    return new EmailNotifier();
                }
                break;
            case "sms":
                if ("true".equals(config.getConfig("notifier.sms.enabled"))) {
                    return new SmsNotifier();
                }
                break;
            default:
                throw new IllegalArgumentException("未知的通知类型: " + type);
        }
        return null;
    }
}

这里有个细节:工厂方法里调用了ConfigManager.getInstance()来检查配置。这意味着,如果配置里把短信通知关了,工厂根本不会创建SmsNotifier对象。配置和创建,在这里优雅地结合了。

我曾经在一个老项目里见过,通知器创建逻辑散落在十几个Service里,每个地方都写一遍if-else。后来要加一个微信通知,改了七八个文件,还漏了两个。从那以后,我坚持用工厂模式来管理对象的创建。你想想看,把变化收敛到一个地方,多省心。

三、观察者模式:状态变化实时通知

最后是观察者模式。这个模式解决的是“一对多”的依赖关系——当一个对象状态发生变化时,所有依赖它的对象都能收到通知。

在我们的项目里,场景是这样的:用户提交了一个订单,订单状态从“待支付”变成“已支付”。这个变化需要触发一系列动作:发送邮件通知、更新积分、记录日志等等。如果把这些逻辑都写在订单状态更新的代码里,那代码会变得又臭又长。

观察者模式的做法是:把“订单”作为被观察者(Subject),把“邮件通知器”、“积分服务”、“日志服务”作为观察者(Observer)。订单状态一变,自动通知所有观察者。

import java.util.ArrayList;
import java.util.List;

// 观察者接口
interface Observer {
    void update(String eventType, String data);
}

// 被观察者(主题)
class OrderSubject {
    private List<Observer> observers = new ArrayList<>();
    
    public void attach(Observer observer) {
        observers.add(observer);
    }
    
    public void detach(Observer observer) {
        observers.remove(observer);
    }
    
    public void notifyObservers(String eventType, String data) {
        for (Observer observer : observers) {
            observer.update(eventType, data);
        }
    }
    
    // 订单状态变更
    public void changeStatus(String orderId, String newStatus) {
        System.out.println("订单 " + orderId + " 状态变更为: " + newStatus);
        notifyObservers("ORDER_STATUS_CHANGE", "订单:" + orderId + ", 新状态:" + newStatus);
    }
}

// 具体观察者:邮件通知
class EmailObserver implements Observer {
    @Override
    public void update(String eventType, String data) {
        if ("ORDER_STATUS_CHANGE".equals(eventType)) {
            Notifier notifier = NotifierFactory.createNotifier("email");
            if (notifier != null) {
                notifier.send("【邮件通知】" + data);
            }
        }
    }
}

// 具体观察者:积分服务
class PointsObserver implements Observer {
    @Override
    public void update(String eventType, String data) {
        if ("ORDER_STATUS_CHANGE".equals(eventType)) {
            System.out.println("【积分服务】更新用户积分,事件数据: " + data);
        }
    }
}
注意:观察者模式虽然好用,但别滥用。如果一个主题有几十上百个观察者,通知性能会下降。另外,观察者之间的执行顺序是不确定的,如果业务上对顺序有要求,得自己加控制逻辑。我曾经在一个实时交易系统里,因为观察者通知顺序问题,导致数据对不上,查了两天才找到原因。

四、架构全景图:三个模式如何协作

好了,三个模式都讲完了。但它们是各自独立的吗?当然不是。在实际项目中,它们会紧密协作。我画了一张图,帮你理清它们之间的关系:

架构设计:三大模式协作全景图 ConfigManager 单例模式 全局配置,唯一实例 NotifierFactory 工厂模式 按需创建通知器 OrderSubject 观察者模式 状态变化通知 读取配置 创建通知器 观察者列表(Observer List) EmailObserver 邮件通知 PointsObserver 积分更新 LogObserver 日志记录 ConfigManager 提供配置 → NotifierFactory 根据配置创建通知器 → OrderSubject 通知所有观察者

从这张图可以看得很清楚:

  • ConfigManager(单例)在最左边,它负责提供全局配置。比如邮件通知是否开启、短信通知的API地址等。
  • NotifierFactory(工厂)在中间,它从ConfigManager读取配置,然后按需创建具体的通知器。
  • OrderSubject(观察者主题)在最右边,它维护了一个观察者列表。当订单状态变化时,遍历列表通知所有观察者。
  • 观察者(如EmailObserver)在收到通知后,会调用NotifierFactory来创建通知器,然后执行具体的发送逻辑。

核心思想:三个模式各司其职,又互相配合。单例管配置,工厂管创建,观察者管通知。每个模式只解决一个问题,组合起来就形成了一个完整的架构闭环。

五、避坑指南与个人经验

讲到这里,我想分享几个我在实际项目中踩过的坑:

  • 单例的线程安全问题:我曾经在一个高并发场景下,忘了给单例加volatile,结果两个线程同时拿到了不同的实例。排查了好久才发现是指令重排搞的鬼。所以,双重检查锁一定要加volatile,或者直接用静态内部类的方式实现。
  • 工厂的扩展性:工厂模式虽然好,但如果通知器类型太多,switch-case会变得很长。我后来改用注册表的方式——每种通知器在启动时自己注册到工厂里,工厂根据类型从Map中查找。这样加新类型时,连工厂类都不用改。
  • 观察者的内存泄漏:观察者模式里,如果观察者注册了但没有及时注销,会导致内存泄漏。尤其是在Android或Web前端场景下,Activity或页面销毁时一定要调用detach方法。我见过一个项目,因为忘了注销观察者,导致页面关闭后还在接收通知,引发了空指针异常。

嗯,这些坑说起来都是泪。但反过来想,踩过坑之后,对模式的理解反而更深了。你想想看,纸上得来终觉浅,绝知此事要躬行。模式本身不难,难的是在合适的场景用合适的模式,并且把细节处理好。

好了,这一章的内容就到这里。架构设计搭好了,下一章我们就可以开始写具体的业务代码了。到时候,你会看到这些模式是如何在真实代码中流畅运转的。