从菜鸟到架构师:我与设计模式的爱恨情仇
记得刚毕业那会儿,面试官问我什么是依赖注入,我支支吾吾半天说不清楚。现在回想起来,那时候的自己真是太天真了。经过这些年在不同项目中的摸爬滚打,我对这些概念有了更深的理解。今天就来聊聊我是如何在实战中真正理解并应用这些设计理念的。
一、依赖注入:从手动挡到自动挡的进化
依赖注入(Dependency Injection)这个概念,说简单也简单,说复杂也复杂。刚开始写代码的时候,我经常写出这样的代码:
public class OrderService {
private UserDao userDao = new UserDao();
private ProductDao productDao = new ProductDao();
private PaymentService paymentService = new PaymentService();
public void createOrder(Long userId, Long productId) {
// 业务逻辑
}
}
每次需要修改数据库实现或者写单元测试的时候,都要改一大堆代码。后来终于明白了,这就是典型的高耦合!
1.1 依赖注入的演进历程
我经历过的依赖注入方式演进:
阶段 | 实现方式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
原始阶段 | 直接new对象 | 简单直接 | 耦合度高,难以测试 | 小型demo |
工厂模式 | 工厂类创建对象 | 一定程度解耦 | 工厂类本身复杂 | 中小型项目 |
手动注入 | 构造函数/setter注入 | 灵活可控 | 繁琐,容易出错 | 特定场景 |
容器注入 | Spring/Guice等框架 | 自动化程度高 | 学习成本,运行开销 | 企业级应用 |
编译期注入 | Dagger2 | 性能好,编译期检查 | 学习曲线陡峭 | 性能敏感场景 |
1.2 一个真实的重构案例
去年接手了一个老项目,里面的依赖关系错综复杂,改一个地方要修改十几个文件。我们决定引入依赖注入进行重构。
重构前的痛点:
- 单元测试几乎不可能,因为依赖太多真实组件
- 想切换缓存实现(从Ehcache到Redis),需要修改30多个类
- 新人接手代码,完全搞不清楚依赖关系
重构后的代码结构:
@Service
public class OrderService {
private final UserRepository userRepository;
private final ProductRepository productRepository;
private final PaymentService paymentService;
@Autowired
public OrderService(UserRepository userRepository,
ProductRepository productRepository,
PaymentService paymentService) {
this.userRepository = userRepository;
this.productRepository = productRepository;
this.paymentService = paymentService;
}
}
重构带来的好处立竿见影:
- 单元测试覆盖率从5%提升到了85%
- 切换技术栈只需要修改配置类
- 代码可读性大幅提升
二、多态性:写出优雅代码的基石
多态性(Polymorphism)是面向对象的精髓之一。但说实话,真正理解它的威力是在一次支付系统的开发中。
2.1 没有多态的日子
最开始,我们的支付代码是这样的:
public void processPayment(String paymentType, Order order) {
if ("alipay".equals(paymentType)) {
// 支付宝支付逻辑
} else if ("wechat".equals(paymentType)) {
// 微信支付逻辑
} else if ("unionpay".equals(paymentType)) {
// 银联支付逻辑
}
// 每加一种支付方式,就要改这个方法
}
这种代码的问题显而易见:违反开闭原则,可维护性差,容易出bug。
2.2 多态的实际应用
重构后,我们使用了策略模式结合多态:
支付方式 | 实现类 | 特殊处理 | 回调机制 |
---|---|---|---|
支付宝 | AlipayStrategy | 异步通知 | HTTP回调 |
微信支付 | WechatPayStrategy | 签名验证 | XML格式 |
银联 | UnionPayStrategy | 加密传输 | 同步返回 |
PayPal | PayPalStrategy | 货币转换 | Webhook |
通过多态,每种支付方式都有自己的实现:
public interface PaymentStrategy {
PaymentResult pay(Order order);
void handleCallback(String callbackData);
}
@Component
public class PaymentContext {
private Map<String, PaymentStrategy> strategies;
public PaymentResult executePayment(String type, Order order) {
return strategies.get(type).pay(order);
}
}
这样的设计让我们在后续添加Apple Pay、Google Pay等新支付方式时,完全不需要修改核心代码。
三、单例模式:用对了是蜜糖,用错了是砒霜
单例模式(Singleton Pattern)可能是最被滥用的设计模式了。我曾经也是"单例模式爱好者",直到被坑了几次才明白,不是所有场景都适合用单例。
3.1 单例模式的各种实现
这些年用过的单例实现方式:
实现方式 | 线程安全 | 性能 | 延迟加载 | 推荐指数 |
---|---|---|---|---|
懒汉式 | 否 | 高 | 是 | ★★ |
同步方法 | 是 | 低 | 是 | ★★ |
双重检查锁 | 是 | 中 | 是 | ★★★ |
静态内部类 | 是 | 高 | 是 | ★★★★ |
枚举 | 是 | 高 | 否 | ★★★★★ |
3.2 单例模式的坑
分享一个血泪教训。有一次,我们用单例模式实现了一个配置管理器:
public class ConfigManager {
private static ConfigManager instance;
private Properties config = new Properties();
private ConfigManager() {
loadConfig();
}
public static synchronized ConfigManager getInstance() {
if (instance == null) {
instance = new ConfigManager();
}
return instance;
}
}
上线后发现了严重问题:
- 在分布式环境下,每个JVM都有自己的单例实例
- 配置更新后,需要重启才能生效
- 单元测试时,不同测试用例会相互影响
3.3 正确使用单例的场景
后来我总结了适合使用单例的场景:
- 无状态的工具类
- 线程池、连接池等资源管理器
- 日志对象
- 设备驱动程序
而不适合的场景:
- 需要在集群环境共享的对象
- 包含大量可变状态的对象
- 需要继承的类
四、面向切面编程:横切关注点的优雇解决方案
面向切面编程(Aspect-Oriented Programming, AOP)是我最晚接触但最快爱上的技术。它解决了很多让人头疼的横切关注点问题。
4.1 AOP的实战场景
在实际项目中,我用AOP解决过这些问题:
应用场景 | 切面实现 | 解决的问题 | 效果 |
---|---|---|---|
日志记录 | @LogAspect | 业务代码混杂日志 | 代码清晰度提升80% |
性能监控 | @PerformanceMonitor | 手动计时繁琐易漏 | 自动化监控覆盖率100% |
权限控制 | @SecurityCheck | 权限检查代码重复 | 减少90%重复代码 |
事务管理 | @Transactional | 手动事务易出错 | 事务一致性保证 |
缓存处理 | @Cacheable | 缓存逻辑侵入业务 | 缓存命中率提升50% |
参数校验 | @ValidateParams | 校验代码冗余 | 统一校验规范 |
4.2 一个AOP改造的实例
我们有个老系统,每个方法都要记录执行时间和参数,代码是这样的:
public User getUserById(Long id) {
long start = System.currentTimeMillis();
logger.info("getUserById called with id: " + id);
try {
User user = userDao.findById(id);
logger.info("getUserById executed in " +
(System.currentTimeMillis() - start) + "ms");
return user;
} catch (Exception e) {
logger.error("getUserById failed", e);
throw e;
}
}
每个方法都是这样,维护起来简直是噩梦!
使用AOP改造后:
@Aspect
@Component
public class MethodLoggingAspect {
@Around("@annotation(Loggable)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
logger.info("{} called with args: {}", methodName, Arrays.toString(args));
try {
Object result = joinPoint.proceed();
long executionTime = System.currentTimeMillis() - start;
logger.info("{} executed in {} ms", methodName, executionTime);
return result;
} catch (Exception e) {
logger.error("{} failed", methodName, e);
throw e;
}
}
}
// 业务代码变得无比清爽
@Loggable
public User getUserById(Long id) {
return userDao.findById(id);
}
4.3 AOP的注意事项
使用AOP也踩过不少坑:
- 性能开销:过度使用AOP会带来性能损耗,特别是在高频调用的方法上
- 调试困难:AOP会改变代码执行流程,调试时可能会困惑
- 代理限制:Spring AOP基于代理,对final方法、私有方法无效
- 执行顺序:多个切面的执行顺序需要特别注意
五、设计模式综合实践
经过这些年的实践,我对设计模式有了一些自己的理解:
5.1 设计模式不是银弹
很多人学了设计模式后,恨不得在每个地方都用上。我也经历过这个阶段,结果代码变得过度设计,简单问题复杂化。
5.2 组合使用效果更好
实际项目中,往往需要多个模式配合使用:
- 依赖注入 + 策略模式 = 灵活的业务策略切换
- 单例模式 + 工厂模式 = 资源的统一管理
- AOP + 注解 = 优雅的横切关注点处理
5.3 持续学习和实践
技术在不断演进,设计模式的应用也在变化。比如函数式编程的兴起,让某些传统设计模式有了新的实现方式。
最后想说的是,设计模式只是工具,真正重要的是解决问题的思路。不要为了用设计模式而用设计模式,而是要在合适的场景选择合适的方案。希望我的这些经验能给大家一些参考,少走一些弯路。
你在使用这些设计模式时有什么心得体会?欢迎在评论区分享交流!
- 点赞
- 收藏
- 关注作者
评论(0)