完整项目实战(三):代码实现——核心模块编码与集成

好,到了这一步,前面那些设计图、类图、接口定义,终于要落地成代码了。说实话,每次到这个阶段,我反而会松一口气——因为抽象的东西已经想透了,剩下的就是敲键盘的体力活。但别大意,编码阶段最容易出幺蛾子,尤其是模块之间的集成。

今天我们就来把单例、工厂、观察者这三个模式,真正揉进一个项目里。我会带着你一步步写核心代码,然后看它们怎么拼到一起。

项目背景回顾

我们做一个简单的日志监控系统。需求是这样的:

  • 系统里只有一个日志管理器(单例模式)
  • 根据配置创建不同的日志处理器(工厂模式)
  • 当日志级别达到告警阈值时,通知所有订阅者(观察者模式)

说白了,就是一个能写日志、能报警、还能灵活切换输出方式的小框架。我在实际项目中遇到过类似的需求,当时客户要求日志既要写文件,又要发邮件,还要推送到消息队列——嗯,观察者模式就是为这种场景量身定做的。

第一步:单例模式——日志管理器的实现

先写最核心的 LoggerManager。这个类全局只有一个实例,所有模块都通过它来记录日志。

public class LoggerManager {
    private static volatile LoggerManager instance;
    private List<LogHandler> handlers;
    private List<AlertObserver> observers;

    private LoggerManager() {
        handlers = new ArrayList<>();
        observers = new ArrayList<>();
        System.out.println("[LoggerManager] 初始化完成");
    }

    public static LoggerManager getInstance() {
        if (instance == null) {
            synchronized (LoggerManager.class) {
                if (instance == null) {
                    instance = new LoggerManager();
                }
            }
        }
        return instance;
    }

    public void log(String level, String message) {
        for (LogHandler handler : handlers) {
            handler.handle(level, message);
        }
        // 如果级别是 ERROR,触发观察者通知
        if ("ERROR".equals(level)) {
            notifyObservers(message);
        }
    }

    public void addHandler(LogHandler handler) {
        handlers.add(handler);
    }

    public void addObserver(AlertObserver observer) {
        observers.add(observer);
    }

    private void notifyObservers(String message) {
        for (AlertObserver observer : observers) {
            observer.onAlert(message);
        }
    }
}

这里用了双重检查锁定的方式。为什么?因为既要保证线程安全,又不能每次调用都加锁影响性能。我早期写过一段代码,直接用 synchronized 修饰整个方法,结果在高并发下性能惨不忍睹。后来改成这种写法,才算是两全其美。

个人习惯:单例的构造函数里尽量别做太重的初始化工作。如果非要加载配置或连接数据库,建议用懒加载的方式,或者单独提供一个 init() 方法。

第二步:工厂模式——日志处理器的创建

接下来是工厂。我们需要根据配置字符串,创建不同的 LogHandler 实现类。比如 "file" 对应文件日志,"console" 对应控制台输出。

public interface LogHandler {
    void handle(String level, String message);
}

public class FileLogHandler implements LogHandler {
    @Override
    public void handle(String level, String message) {
        System.out.println("[文件日志] " + level + ": " + message);
        // 实际项目中这里会写文件
    }
}

public class ConsoleLogHandler implements LogHandler {
    @Override
    public void handle(String level, String message) {
        System.out.println("[控制台] " + level + ": " + message);
    }
}

public class LogHandlerFactory {
    public static LogHandler createHandler(String type) {
        switch (type.toLowerCase()) {
            case "file":
                return new FileLogHandler();
            case "console":
                return new ConsoleLogHandler();
            default:
                throw new IllegalArgumentException("未知的日志处理器类型: " + type);
        }
    }
}

工厂模式的好处是什么?你想想看,如果以后要加一个 DatabaseLogHandler,只需要新增一个类,然后在工厂里加一个 case 分支就行。调用方完全不用改代码。我在项目中用这个模式重构过一个支付渠道对接的模块,原来每次加新渠道都要改十几个文件,改成工厂之后,只改一个地方。

注意:工厂模式虽然灵活,但别滥用。如果处理器类型只有两三种,硬编码 switch 完全够用。我曾经见过有人为了“优雅”,给三个实现类搞了一套 SPI 加载机制,结果维护成本翻了好几倍。

第三步:观察者模式——告警通知机制

最后是观察者。当日志级别为 ERROR 时,所有注册的观察者都会收到通知。比如发邮件、发短信、或者弹窗。

public interface AlertObserver {
    void onAlert(String message);
}

public class EmailAlertObserver implements AlertObserver {
    private String email;

    public EmailAlertObserver(String email) {
        this.email = email;
    }

    @Override
    public void onAlert(String message) {
        System.out.println("[邮件告警] 发送至 " + email + ": " + message);
    }
}

public class SmsAlertObserver implements AlertObserver {
    private String phone;

    public SmsAlertObserver(String phone) {
        this.phone = phone;
    }

    @Override
    public void onAlert(String message) {
        System.out.println("[短信告警] 发送至 " + phone + ": " + message);
    }
}

观察者模式的核心思想就是“发布-订阅”。日志管理器是发布者,各种告警器是订阅者。两者之间通过接口解耦。我做过一个实时风控系统,里面用了类似的模式——交易异常时,同时通知风控审核、用户、以及运维团队。如果不用观察者,那代码里就得写一堆 if-else 调用,想想就头疼。

第四步:模块集成——让三个模式协同工作

现在三个核心模块都写好了,怎么把它们串起来?我们写一个 Main 类来演示集成过程。

public class Main {
    public static void main(String[] args) {
        // 1. 获取单例
        LoggerManager logger = LoggerManager.getInstance();

        // 2. 通过工厂创建处理器
        LogHandler fileHandler = LogHandlerFactory.createHandler("file");
        LogHandler consoleHandler = LogHandlerFactory.createHandler("console");

        // 3. 注册处理器
        logger.addHandler(fileHandler);
        logger.addHandler(consoleHandler);

        // 4. 注册观察者
        logger.addObserver(new EmailAlertObserver("admin@example.com"));
        logger.addObserver(new SmsAlertObserver("13800138000"));

        // 5. 模拟日志输出
        logger.log("INFO", "系统启动成功");
        logger.log("WARN", "磁盘空间不足");
        logger.log("ERROR", "数据库连接超时");
    }
}

运行结果应该是这样的:

[LoggerManager] 初始化完成
[文件日志] INFO: 系统启动成功
[控制台] INFO: 系统启动成功
[文件日志] WARN: 磁盘空间不足
[控制台] WARN: 磁盘空间不足
[文件日志] ERROR: 数据库连接超时
[控制台] ERROR: 数据库连接超时
[邮件告警] 发送至 admin@example.com: 数据库连接超时
[短信告警] 发送至 13800138000: 数据库连接超时

你看,INFO 和 WARN 级别的日志只走了处理器,而 ERROR 级别的日志除了写文件和控制台,还触发了邮件和短信告警。这就是三个模式协同工作的效果。

核心模块关系图

下面这张图展示了三个模式在项目中的协作关系。我特意把箭头画得清晰一些,方便你理解数据流向。

核心模块协作关系图 LoggerManager 单例模式 LogHandlerFactory 工厂模式 FileLogHandler ConsoleLogHandler AlertObserver 观察者模式 EmailAlertObserver SmsAlertObserver 注册处理器 通知观察者

集成时容易踩的坑

代码写完了,但集成的时候有几个地方特别容易出问题。我把自己踩过的坑列出来,你注意避开。

  • 单例被多次初始化:如果用了序列化或者反射,单例可能会被破坏。解决方案是在 readResolve() 方法里返回同一个实例,或者直接用枚举实现单例。
  • 工厂的扩展性:如果处理器类型很多,switch 会变得臃肿。我建议用注册表的方式,把类型和实现类的映射关系放在一个 Map 里,通过配置文件动态加载。
  • 观察者的内存泄漏:如果观察者注册后没有及时注销,会导致内存泄漏。尤其是在 Web 应用中,每次请求都注册一个新观察者,最后内存就爆了。记得在不需要时调用 removeObserver()
核心要点:三个模式不是孤立的。单例保证了全局唯一入口,工厂解耦了对象的创建过程,观察者实现了事件驱动的通知机制。把它们组合起来,才能发挥出设计模式真正的威力。

好了,这一章的核心代码实现就到这里。代码量不大,但每个模式都承担了明确的职责。你可以在自己的项目里试着搭一套类似的框架,把日志、告警、配置管理这些通用功能抽象出来。相信我,以后维护起来会轻松很多。

避坑指南:我曾经在一个项目里把工厂和观察者写成了紧耦合——工厂创建的对象直接持有观察者的引用。结果后来要换通知方式,改得我头皮发麻。记住,接口隔离是设计模式的灵魂。

公众号:蓝海资料掘金营,微信deep3321