我们是如何用AOP、IoC和设计模式拯救祖传代码的

举报
8181暴风雪 发表于 2025/07/26 18:33:16 2025/07/26
【摘要】 去年接手了一个"祖传"项目,代码写于2015年,经过20多位程序员的"精心呵护",已经进化成了一坨谁都不敢动的意大利面。最夸张的是,有个Service类居然有8000多行,构造函数要传37个参数!老板说:"这系统太难维护了,但不能推倒重来,你们想办法重构一下。"看着这坨代码,我差点当场辞职。但最终,我们用了3个月时间,通过引入AOP、IoC容器和合理的设计模式,把这个项目从地狱拉回了人间。 ...

去年接手了一个"祖传"项目,代码写于2015年,经过20多位程序员的"精心呵护",已经进化成了一坨谁都不敢动的意大利面。最夸张的是,有个Service类居然有8000多行,构造函数要传37个参数!

老板说:"这系统太难维护了,但不能推倒重来,你们想办法重构一下。"看着这坨代码,我差点当场辞职。但最终,我们用了3个月时间,通过引入AOP、IoC容器和合理的设计模式,把这个项目从地狱拉回了人间。

噩梦的开始:祖传代码有多可怕

先给大家看看重构前的代码是什么样的:

public class UserService {
    private DBConnection dbConnection;
    private LogWriter logWriter;
    private CacheManager cacheManager;
    private EmailSender emailSender;
    // ... 还有30多个依赖
    
    public UserService() {
        this.dbConnection = new MySQLConnection("localhost", 3306, "root", "123456");
        this.logWriter = new FileLogWriter("/var/log/app.log");
        this.cacheManager = new RedisCacheManager("localhost", 6379);
        this.emailSender = new SMTPEmailSender("smtp.gmail.com", 587);
        // ... 初始化其他30多个依赖
    }
    
    public User getUserById(String userId) {
        // 记录日志
        logWriter.write("Getting user: " + userId);
        long startTime = System.currentTimeMillis();
        
        try {
            // 检查缓存
            User cachedUser = (User) cacheManager.get("user_" + userId);
            if (cachedUser != null) {
                logWriter.write("Cache hit for user: " + userId);
                return cachedUser;
            }
            
            // 查询数据库
            User user = dbConnection.executeQuery("SELECT * FROM users WHERE id = ?", userId);
            
            // 放入缓存
            cacheManager.put("user_" + userId, user, 3600);
            
            // 记录性能日志
            long endTime = System.currentTimeMillis();
            logWriter.write("Get user took: " + (endTime - startTime) + "ms");
            
            return user;
        } catch (Exception e) {
            logWriter.write("Error getting user: " + e.getMessage());
            emailSender.send("admin@company.com", "Error in UserService", e.toString());
            throw e;
        }
    }
    
    // 还有200多个这样的方法...
}

看到这代码,我统计了一下问题:

问题类型 具体表现 影响范围 严重程度
强耦合 直接new依赖对象 所有类 ★★★★★
横切关注点 日志/缓存/事务代码重复 500+方法 ★★★★★
违反单一职责 一个类做所有事 核心业务类 ★★★★☆
硬编码配置 数据库密码写死 全局配置 ★★★★★
无法测试 依赖太多mock不了 0%测试覆盖率 ★★★★★

控制反转(IoC):解开耦合的第一步

什么是控制反转?

简单说,就是把"创建对象"的控制权交出去。以前是自己new,现在让容器帮你创建和管理。

重构第一步,我们引入了Spring IoC容器:

@Service
public class UserService {
    private final UserRepository userRepository;
    private final CacheManager cacheManager;
    
    @Autowired
    public UserService(UserRepository userRepository, CacheManager cacheManager) {
        this.userRepository = userRepository;
        this.cacheManager = cacheManager;
    }
    
    public User getUserById(String userId) {
        return userRepository.findById(userId);
    }
}

IoC带来的改变

引入IoC后的效果对比:

指标 重构前 重构后 改善
类的依赖数 平均37个 平均3个 91.9%
代码行数 8000行/类 200行/类 97.5%
启动时间 45秒 12秒 73.3%
单元测试编写时间 无法测试 10分钟/测试
修改一处的影响范围 平均15个类 平均2个类 86.7%

依赖注入(DI):IoC的具体实现

三种注入方式对比

我们尝试了不同的依赖注入方式:

注入方式 优点 缺点 使用场景 我们的选择
构造器注入 依赖明确、不可变 参数多时繁琐 必需依赖 ✓ 主要方式
Setter注入 灵活、可选依赖 可能忘记注入 可选依赖 ✓ 辅助方式
字段注入 简洁 难测试、耦合高 原型开发 ✗ 禁止使用

循环依赖问题

使用DI时遇到的最大坑就是循环依赖:

// A依赖B,B依赖A,Spring启动失败!
@Service
public class ServiceA {
    @Autowired
    private ServiceB serviceB;
}

@Service  
public class ServiceB {
    @Autowired
    private ServiceA serviceA;
}

解决方案和效果:

解决方案 实现难度 代码侵入性 推荐指数 实际采用率
重新设计避免循环 ★★★★☆ ★★★★★ 70%
使用@Lazy ★☆☆☆☆ ★★★☆☆ 20%
Setter注入 ★★☆☆☆ ★★☆☆☆ 5%
引入第三方类 ★★★☆☆ ★★★★☆ 5%

面向切面编程(AOP):优雅处理横切关注点

AOP解决了什么问题?

原来的代码里,每个方法都要写日志、处理事务、检查权限,代码重复得令人发指。AOP让我们能够把这些"横切关注点"抽离出来。

实施AOP前后的代码对比:

// Before: 业务逻辑淹没在各种非业务代码中
public User getUserById(String userId) {
    // 权限检查
    if (!hasPermission(userId)) {
        throw new UnauthorizedException();
    }
    
    // 记录日志
    logger.info("Getting user: " + userId);
    long startTime = System.currentTimeMillis();
    
    // 开启事务
    Transaction tx = transactionManager.beginTransaction();
    
    try {
        User user = userRepository.findById(userId);
        tx.commit();
        
        // 性能日志
        long endTime = System.currentTimeMillis();
        logger.info("Operation took: " + (endTime - startTime) + "ms");
        
        return user;
    } catch (Exception e) {
        tx.rollback();
        logger.error("Error: " + e.getMessage());
        throw e;
    }
}

// After: 只关注核心业务
@Transactional
@PreAuthorize("hasPermission(#userId)")
@LogExecution
public User getUserById(String userId) {
    return userRepository.findById(userId);
}

AOP切面统计

我们创建的主要切面:

切面类型 应用范围 代码减少量 性能影响 维护成本降低
日志切面 所有Service方法 3000行 <1ms 80%
事务切面 数据库操作 1500行 <2ms 90%
缓存切面 查询方法 800行 -50ms(提升) 70%
权限切面 API接口 1200行 <1ms 85%
性能监控切面 关键业务 500行 <1ms 75%

AOP性能开销

很多人担心AOP的性能开销,我们做了详细测试:

测试场景 无AOP 有AOP 增加耗时 可接受度
简单方法调用 0.01ms 0.012ms 20%
数据库查询 10ms 10.1ms 1%
复杂业务逻辑 100ms 100.5ms 0.5%
批量操作(1000次) 1000ms 1020ms 2%

结论:AOP的性能开销几乎可以忽略不计。

单例模式:不是所有东西都要new

单例模式的演进

项目里有些重量级对象(如连接池、配置管理器)被到处new,既浪费资源又容易出问题。我们逐步改造成单例:

单例实现方式 线程安全 性能 延迟加载 推荐度
饿汉式 ★★★★★ ★★★☆☆
懒汉式(无锁) ★★★★★ ★☆☆☆☆
懒汉式(同步方法) ★☆☆☆☆ ★★☆☆☆
双重检查锁 ★★★★☆ ★★★★☆
静态内部类 ★★★★★ ★★★★★
枚举 ★★★★★ ★★★★★

Spring中的单例

在Spring容器中,Bean默认就是单例的。我们统计了单例化的效果:

对象类型 重构前实例数 重构后实例数 内存节省 初始化时间节省
数据库连接池 50+ 1 98% 10秒
配置管理器 100+ 1 99% 5秒
消息发送器 200+ 1 99.5% 8秒
缓存客户端 30+ 1 97% 3秒

单例模式的坑

但单例也不是万能的,我们踩过的坑:

问题 表现 原因 解决方案
内存泄漏 内存持续增长 单例持有大量数据 定期清理
并发问题 数据错乱 单例非线程安全 加锁/无状态设计
测试困难 Mock困难 全局状态 依赖注入
集群问题 数据不一致 每个JVM一个实例 分布式缓存

综合使用:架构的蜕变

重构后的架构

经过3个月的重构,整个项目架构焕然一新:

原架构:
UserController -> UserService(8000) -> 直接JDBC

新架构:
UserController 
     (AOP: 日志、权限)
UserService (业务逻辑)
     (AOP: 事务、缓存)  
UserRepository (数据访问)
    ↓
数据库

所有组件通过IoC容器管理,依赖注入

重构效果数据

指标 重构前 重构后 改善
代码总行数 150,000 45,000 70%
平均类大小 2000行 150行 92.5%
圈复杂度 平均25 平均5 80%
测试覆盖率 0% 85% +85%
发布时间 2小时 15分钟 87.5%
线上bug数/月 50+ 5 90%
新功能开发时间 2周 3天 78.6%

团队反馈

更重要的是团队的变化:

方面 之前 现在
代码可读性 “看不懂前人写的啥” “像读文档一样清晰”
修改信心 “不敢动,怕改出bug” “改哪儿影响哪儿一目了然”
新人上手 需要1个月 1周即可开始开发
加班情况 经常通宵改bug 基本不加班

经验总结

1. 渐进式重构

不要试图一次改完,我们的策略:

  • 第1个月:引入IoC容器,解耦依赖
  • 第2个月:应用AOP,消除重复代码
  • 第3个月:优化设计模式,提升代码质量

2. 不要过度设计

设计原则 过度使用的后果 正确做法
AOP 到处都是切面,找不到真实代码 只在横切关注点使用
IoC 所有对象都注入,启动巨慢 合理划分Bean范围
设计模式 为了模式而模式 解决实际问题才用

3. 性能不是借口

很多人说"设计模式影响性能",但实测证明:

  • 良好的设计带来的维护性提升远大于微小的性能损失
  • 很多时候,好的设计反而提升了性能(如缓存切面)
  • 真正的性能瓶颈往往在IO,不在这些框架开销

写在最后

这次重构经历让我深刻理解了Martin Fowler的话:“任何傻瓜都能写出计算机能理解的代码,但只有优秀的程序员才能写出人类能理解的代码。”

AOP、IoC、设计模式,这些不是为了炫技,而是为了让代码更容易理解、修改和维护。当你的同事(包括三个月后的自己)能轻松理解并修改你的代码时,这些技术就值了。

最后想说,重构祖传代码确实很痛苦,但看到整个系统焕发新生,团队重拾信心,这种成就感是无法替代的。如果你也在维护祖传代码,不要绝望,制定好计划,一步步来,总能看到曙光的。

记住:优秀的代码是重构出来的,不是一次写成的

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。