1. 项目概述:为什么我们要从代码实现开始聊设计模式?
聊到设计模式,很多刚入行的朋友可能会觉得有点“虚”。书上的UML图看懂了,每个模式的定义也背了,但一到自己写代码,还是不知道该怎么用,或者用起来总觉得别扭,生搬硬套。这就是典型的“理论和实践脱节”。我见过不少项目,为了用模式而用模式,把简单的逻辑搞得异常复杂,类图画出来挺漂亮,代码维护起来却让人头疼。所以,这个系列我们不空谈理论,就从一个资深开发者的视角,聚焦于如何用Java代码干净、落地、恰到好处地实现这些模式,并分享在真实项目中应用它们时的取舍与心得。
设计模式不是银弹,它更像是一套经过验证的“招式库”。高手对决时,不会刻意去想“我现在要用第几式”,而是根据对手的出招(业务需求的变化)自然使出最合适的应对。我们的目标,就是通过具体的代码实现,帮你把这些“招式”内化,让你在面临需求变更、系统扩展时,能下意识地写出更灵活、更健壮的代码。本系列将覆盖创建型、结构型和行为型三大类模式,每一篇都会深入1-2个模式,用最直白的代码和最常见的业务场景,说清楚“怎么实现”以及“为什么这么实现”。
2. 核心思路:从场景到代码的落地路径
在动手写代码之前,我们必须明确一个核心原则:模式服务于业务,而非业务将就模式。我的思路是“场景驱动”:先构想一个简单但典型的业务场景,然后观察这个场景在演化过程中暴露出的设计问题,最后引入设计模式作为解决方案。这样,你就能清晰地看到模式带来的价值,而不是觉得它凭空增加了复杂度。
2.1 场景驱动的学习法
举个例子,假设我们最初有一个简单的任务:创建一个“通知器”,能发送邮件。你可能会写一个EmailNotifier类,里面一个send方法。这没问题。但很快,产品经理说:“我们还要支持短信通知。” 于是你加了SmsNotifier类。接着,又要支持微信推送、App站内信…… 你会发现,每次新增一种通知方式,你都要去修改调用通知的客户端代码,或者写一堆if-else来判断。这时,你就遇到了“变化点”,而“工厂模式”或“策略模式”可能就是你的解药。
我们将遵循“定义场景 -> 暴露痛点 -> 引入模式 -> 代码重构 -> 对比优劣”的路径。每一段代码都会有“模式前”和“模式后”的对比,让你直观感受改进在哪里。
2.2 代码实现的评判标准
实现一个模式,代码可以运行只是最低要求。我认为好的实现需要满足以下几点:
- 符合开闭原则:这是衡量模式应用是否成功的关键。系统应该对扩展开放,对修改关闭。新增功能时,是否只需添加新类,而无需改动现有稳定代码?
- 客户端调用足够简单:模式内部的复杂是为了外部的简单。客户端代码不应该感知到模式内部复杂的对象创建或组装过程。
- 命名清晰,意图明确:类名、方法名要能直接反映其职责和模式角色(如
XxxFactory,XxxStrategy,XxxAdapter)。 - 避免过度设计:如果当前场景很简单,未来变化可能性极低,直接写简单的代码反而更优。模式是工具,不是枷锁。
3. 创建型模式实战:单例模式 (Singleton) 的深潜与避坑
单例模式大概是所有人学到的第一个设计模式,概念简单:一个类只有一个实例,并提供一个全局访问点。但它的实现细节和坑点,足以写满好几页纸。我们不止步于“双重检查锁定”,而要深入其变体、原理及现代最佳实践。
3.1 场景与痛点:为什么需要单例?
想象一个应用配置管理器ConfigManager。它在系统启动时从文件或数据库加载配置(耗时操作),之后在程序的任何地方,各个模块都需要读取同一份配置信息。如果每个模块都自己new一个ConfigManager实例,不仅会产生多次不必要的IO消耗,还可能因为加载时机不同导致配置不一致。这时,我们就需要确保全局只有一个ConfigManager实例。
模式前的糟糕代码:
// 模块A ConfigManager configA = new ConfigManager(); String dbUrl = configA.get("database.url"); // 模块B ConfigManager configB = new ConfigManager(); String cacheHost = configB.get("cache.host"); // configA 和 configB 是不同实例,可能加载了不同的配置内容,造成混乱。3.2 经典实现与演进
1. 饿汉式 (Eager Initialization)
public class ConfigManager { // 1. 私有静态实例,类加载时就初始化 private static final ConfigManager INSTANCE = new ConfigManager(); // 2. 私有构造器,防止外部new private ConfigManager() { // 模拟耗时的配置加载 loadConfigFromFile(); } // 3. 公共静态方法,提供全局访问点 public static ConfigManager getInstance() { return INSTANCE; } private void loadConfigFromFile() { System.out.println("Loading configuration..."); // 实际从文件读取配置 } }注意:优点是实现简单,线程安全(由JVM类加载机制保证)。缺点是无论你用不用,实例都会在类加载时创建,如果实例初始化非常耗时或占用资源多,可能会拖慢应用启动速度,是一种“以空间换时间”的思想。
2. 懒汉式 (Lazy Initialization) 与线程安全
public class ConfigManager { private static ConfigManager INSTANCE; // 不再用final private ConfigManager() {} // 非线程安全版本! public static ConfigManager getInstance() { if (INSTANCE == null) { INSTANCE = new ConfigManager(); // 多线程下可能创建多个实例 } return INSTANCE; } }非线程安全的懒汉式是面试常考点,但在生产环境是绝对禁止的。为了解决线程安全问题,我们引入同步锁。
3. 双重检查锁定 (Double-Checked Locking)
public class ConfigManager { // 使用 volatile 关键字,确保多线程下的可见性和禁止指令重排序 private static volatile ConfigManager INSTANCE; private ConfigManager() {} public static ConfigManager getInstance() { if (INSTANCE == null) { // 第一次检查,避免不必要的同步 synchronized (ConfigManager.class) { if (INSTANCE == null) { // 第二次检查,确保唯一性 INSTANCE = new ConfigManager(); } } } return INSTANCE; } }实操心得:这里的
volatile关键字至关重要。INSTANCE = new ConfigManager();这行代码并非原子操作,它包含:1. 分配内存空间,2. 初始化对象,3. 将引用指向内存地址。JVM可能进行指令重排序,导致其他线程拿到一个未初始化完全的对象(空壳)。volatile可以防止这种重排序,保证线程安全。这是面试高频深水区,务必理解。
4. 静态内部类实现 (Holder) —— 当前最推荐写法
public class ConfigManager { private ConfigManager() {} // 静态内部类 private static class SingletonHolder { private static final ConfigManager INSTANCE = new ConfigManager(); } public static ConfigManager getInstance() { return SingletonHolder.INSTANCE; // 只有调用此方法时,内部类才会被加载,INSTANCE被创建 } }这是我最喜欢也是项目中最常用的一种方式。它利用了JVM的类加载机制:静态内部类SingletonHolder只有在getInstance()方法第一次被调用时才会被加载和初始化其静态字段INSTANCE。这个过程由JVM保证线程安全性,且实现了懒加载。代码简洁,性能高效,无需担心同步问题。
5. 枚举实现 (Enum) —— Joshua Bloch 大神推荐
public enum ConfigManager { INSTANCE; // 可以在此添加实例方法 public void loadConfig() { // ... } } // 使用:ConfigManager.INSTANCE.loadConfig();这是《Effective Java》作者极力推荐的方式。它不仅能避免多线程同步问题,还能防止反序列化和反射攻击创建新的实例。缺点是它并非一种“懒加载”,枚举实例在类被加载时就会初始化。对于大部分场景,这微小的启动开销是可以接受的,换来了绝对的安全和简洁。
3.3 单例模式的常见陷阱与反思
- 序列化与反序列化破坏单例:如果一个单例类实现了
Serializable接口,反序列化时会创建一个新的实例。解决方法是在类中添加readResolve()方法。protected Object readResolve() { return getInstance(); } - 反射攻击:通过反射调用私有构造器可以创建新实例。枚举单例可以天然防御此攻击。对于类实现,可以在构造器中加入判断逻辑。
private ConfigManager() { if (INSTANCE != null) { throw new RuntimeException("Use getInstance() method to get the single instance."); } } - 单例与单元测试的困境:单例的全局状态会使单元测试变得困难,测试之间可能相互影响。一个实用的技巧是,尽量为单例类定义接口,并在生产代码中使用依赖注入的方式获取其实例(虽然实例仍是单例),但在测试时可以方便地替换为Mock对象。
- 是否真的需要单例?这是最该问自己的问题。单例本质上是一个“全局变量”,它带来了便利,也引入了耦合和测试难度。在Spring这类IoC容器管理的应用中,将Bean的作用域定义为
singleton由容器管理生命周期,是比手写单例模式更优雅、更现代的做法。手写单例模式更适用于基础框架、工具类或尚未引入容器的轻量级应用。
4. 创建型模式实战:工厂方法 (Factory Method) 与抽象工厂 (Abstract Factory) 的抉择
工厂模式家族用于封装对象的创建过程,将客户端代码与具体产品类解耦。工厂方法和抽象工厂容易混淆,我们通过对比来厘清。
4.1 工厂方法模式:处理单一产品等级结构
场景:一个日志记录器框架,需要支持将日志写入不同目的地:文件、数据库、控制台。未来可能扩展至Kafka、Elasticsearch。
痛点:如果不使用模式,客户端代码中会充斥大量的if-else或switch,根据配置或参数来new不同的记录器对象。每次新增记录器类型,都要修改客户端代码,违反开闭原则。
工厂方法模式实现: 定义一个创建对象的接口,但让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。
// 1. 产品接口 public interface Logger { void log(String message); } // 2. 具体产品 public class FileLogger implements Logger { @Override public void log(String message) { System.out.println("Log to file: " + message); } } public class DatabaseLogger implements Logger { @Override public void log(String message) { System.out.println("Log to database: " + message); } } // 3. 创建者抽象类(核心) public abstract class LoggerFactory { // 这就是“工厂方法” public abstract Logger createLogger(); // 可以包含一些与产品相关的核心业务逻辑 public void log(String message) { Logger logger = createLogger(); // 调用工厂方法,解耦! logger.log(message); } } // 4. 具体创建者 public class FileLoggerFactory extends LoggerFactory { @Override public Logger createLogger() { // 这里可以包含复杂的初始化逻辑,比如打开文件句柄 return new FileLogger(); } } public class DatabaseLoggerFactory extends LoggerFactory { @Override public Logger createLogger() { // 初始化数据库连接等 return new DatabaseLogger(); } } // 客户端代码 public class Client { public static void main(String[] args) { LoggerFactory factory = new FileLoggerFactory(); // 可通过配置读取 factory.log("An important event."); // 要切换为数据库日志,只需改为:new DatabaseLoggerFactory() } }模式价值:客户端 (
Client) 只依赖LoggerFactory抽象和Logger抽象。当需要新增一个KafkaLogger时,我们只需新增KafkaLogger产品和KafkaLoggerFactory创建者,无需修改任何现有工厂和客户端代码。系统实现了对扩展开放。
4.2 抽象工厂模式:处理多个产品族
场景:升级上面的日志系统。现在日志不仅分目的地,还分格式:简单文本格式和JSON格式。我们需要创建“文件文本日志”、“文件JSON日志”、“数据库文本日志”、“数据库JSON日志”等一系列相关或依赖的对象。
痛点:如果还用工厂方法,我们需要为“文件+文本”、“文件+JSON”分别创建工厂,类会爆炸式增长,且难以保证“文件”工厂创建的产品一定是文件相关的。
抽象工厂模式实现: 提供一个接口,用于创建相关的或依赖对象的家族,而不需要明确指定具体类。
// 1. 抽象产品族:格式 public interface LogFormatter { String format(String message); } public class TextFormatter implements LogFormatter { @Override public String format(String m) { return "[TEXT] " + m; } } public class JsonFormatter implements LogFormatter { @Override public String format(String m) { return "{\"msg\": \"" + m + "\"}"; } } // 2. 抽象产品族:写入器 public interface LogWriter { void write(String content); } public class FileWriter implements LogWriter { @Override public void write(String c) { System.out.println("Write to File: " + c); } } public class DatabaseWriter implements LogWriter { @Override public void write(String c) { System.out.println("Write to DB: " + c); } } // 3. 抽象工厂(核心) public interface LoggerAbstractFactory { LogFormatter createFormatter(); LogWriter createWriter(); } // 4. 具体工厂:每个工厂负责一个产品族 public class FileTextFactory implements LoggerAbstractFactory { @Override public LogFormatter createFormatter() { return new TextFormatter(); } @Override public LogWriter createWriter() { return new FileWriter(); } } public class FileJsonFactory implements LoggerAbstractFactory { @Override public LogFormatter createFormatter() { return new JsonFormatter(); } @Override public LogWriter createWriter() { return new FileWriter(); } } public class DatabaseTextFactory implements LoggerAbstractFactory { @Override public LogFormatter createFormatter() { return new TextFormatter(); } @Override public LogWriter createWriter() { return new DatabaseWriter(); } } // 5. 一个组装好的“日志器” public class AdvancedLogger { private LogFormatter formatter; private LogWriter writer; public AdvancedLogger(LoggerAbstractFactory factory) { this.formatter = factory.createFormatter(); this.writer = factory.createWriter(); // 保证formatter和writer来自同一家族 } public void log(String message) { String formatted = formatter.format(message); writer.write(formatted); } } // 客户端代码 public class Client { public static void main(String[] args) { LoggerAbstractFactory factory = new FileJsonFactory(); // 配置决定 AdvancedLogger logger = new AdvancedLogger(factory); logger.log("User login"); // 输出:Write to File: {"msg": "User login"} // 要切换为数据库文本日志,只需更换工厂:new DatabaseTextFactory() } }核心区别与选择:
- 工厂方法:关注单一产品的创建。
FileLoggerFactory只负责创建FileLogger。解决“单个对象”的创建扩展问题。- 抽象工厂:关注产品族的创建。
FileJsonFactory负责创建一整套能协同工作的产品 (JsonFormatter+FileWriter)。解决“一系列相互关联对象”的创建扩展问题,并保证产品间的兼容性。- 如何选:如果你的系统中有多个属于不同产品等级结构(如电器中的空调、冰箱)但属于同一家族(如海尔、格力)的产品需要一起创建,就用抽象工厂。如果只是要创建一种产品(如各种风格的按钮),用工厂方法更简单。
5. 结构型模式实战:适配器模式 (Adapter) 的两种实现与真实用例
适配器模式就像电源转接头,让原本接口不兼容的两个类可以协同工作。它分为类适配器(继承)和对象适配器(组合)两种,后者更常用,也更符合组合优于继承的原则。
5.1 场景:整合遗留系统
假设我们有一个现代支付系统,定义了一个统一的支付接口ModernPaymentGateway:
public interface ModernPaymentGateway { boolean pay(BigDecimal amount, String currency); }系统里已经实现了CreditCardProcessor和PayPalProcessor。
现在,公司收购了一个老项目,里面有一个古老的支付类LegacyPaymentService,它的方法长这样:
public class LegacyPaymentService { // 方法名、参数顺序、类型都不同 public boolean makePayment(int cents) { System.out.println("Legacy system processing: " + cents + " cents"); return true; } }我们需要让新的系统能够调用这个老的服务。直接修改LegacyPaymentService风险高,也不符合开闭原则。适配器登场。
5.2 对象适配器实现(推荐)
通过组合的方式持有被适配者的引用。
public class LegacyPaymentAdapter implements ModernPaymentGateway { // 组合一个被适配对象 private LegacyPaymentService legacyService; public LegacyPaymentAdapter(LegacyPaymentService legacyService) { this.legacyService = legacyService; } @Override public boolean pay(BigDecimal amount, String currency) { // 在这里进行适配逻辑 // 1. 货币转换(假设只处理USD) if (!"USD".equalsIgnoreCase(currency)) { throw new UnsupportedOperationException("Legacy system only supports USD."); } // 2. 金额单位转换(元转分) int cents = amount.multiply(new BigDecimal(100)).intValue(); // 3. 调用被适配者的方法 return legacyService.makePayment(cents); } }客户端使用:
public class Client { public static void main(String[] args) { ModernPaymentGateway gateway = new LegacyPaymentAdapter(new LegacyPaymentService()); boolean success = gateway.pay(new BigDecimal("99.99"), "USD"); System.out.println("Payment result: " + success); } }优点:灵活。
LegacyPaymentAdapter可以适配LegacyPaymentService及其任何子类。符合“组合优于继承”原则。
5.3 类适配器实现(需要多重继承,Java中不直接支持)
类适配器通过继承被适配者来实现。在Java中,由于单继承限制,如果被适配者是一个类,且适配器需要同时继承它和实现目标接口,这只有在适配器本身不需要继承其他类时才可行。如果被适配者是类,且目标也是类(不是接口),则无法实现。因此,在Java里,当被适配者是一个具体类时,对象适配器是唯一选择。如果被适配者也是一个接口,则可以模拟(但意义不大)。
假设LegacyPaymentService是一个接口ILegacyPayment:
// 假设的接口 public interface ILegacyPayment { boolean makePayment(int cents); } public class ConcreteLegacyService implements ILegacyPayment { ... } // 类适配器(在Java中,这要求Target是接口,Adaptee也是接口或类) public class ClassAdapter extends ConcreteLegacyService implements ModernPaymentGateway { @Override public boolean pay(BigDecimal amount, String currency) { int cents = amount.multiply(new BigDecimal(100)).intValue(); return super.makePayment(cents); // 直接调用父类方法 } }注意:由于Java单继承,
ClassAdapter已经继承了ConcreteLegacyService,就无法再继承其他类了,灵活性受限。因此,在绝大多数Java场景下,优先使用对象适配器。
5.4 适配器模式在JDK和Spring中的身影
- JDK:
java.util.Arrays#asList(T... a)可以看作一个适配器,它将一个数组适配成了List接口。 - Spring:Spring MVC 中的
HandlerAdapter是适配器模式的经典应用。不同的Controller(如基于@Controller注解的、实现Controller接口的)有不同的处理方式。DispatcherServlet通过HandlerAdapter来适配各种Controller,使得Servlet可以以统一的方式调用它们。 - 实际心得:适配器模式常用于系统集成和接口升级。在微服务架构中,调用外部第三方服务时,经常为其编写一个“Client Adapter”,将第三方千奇百怪的API封装成符合我们内部规范的接口,这样核心业务逻辑就与第三方实现解耦了。未来更换第三方服务时,只需换一个Adapter即可。
6. 行为型模式实战:策略模式 (Strategy) 的动态之美
策略模式定义了算法家族,分别封装起来,让它们之间可以互相替换。此模式让算法的变化独立于使用算法的客户端。
6.1 场景:多种促销折扣计算
电商平台有各种促销策略:无折扣、固定折扣、百分比折扣、满减等。而且这些策略经常变动和增加。
痛点:如果使用if-else或switch在订单计算类里硬编码,每次新增或修改策略,都要修改这个核心计算类,容易出错且违反开闭原则。
// 糟糕的代码 public class OrderService { public BigDecimal calculateDiscount(String type, BigDecimal amount) { if ("FIXED".equals(type)) { return amount.subtract(new BigDecimal(10)); // 减10元 } else if ("PERCENT".equals(type)) { return amount.multiply(new BigDecimal("0.9")); // 9折 } else if ("OVER100MINUS20".equals(type)) { if (amount.compareTo(new BigDecimal(100)) >= 0) { return amount.subtract(new BigDecimal(20)); } return amount; } // ... 更多else if return amount; } }6.2 策略模式实现:将“做什么”和“怎么做”分离
// 1. 策略接口 public interface DiscountStrategy { BigDecimal applyDiscount(BigDecimal originalAmount); } // 2. 具体策略 public class NoDiscountStrategy implements DiscountStrategy { @Override public BigDecimal applyDiscount(BigDecimal amount) { return amount; } } public class FixedDiscountStrategy implements DiscountStrategy { private BigDecimal discountAmount; public FixedDiscountStrategy(BigDecimal discountAmount) { this.discountAmount = discountAmount; } @Override public BigDecimal applyDiscount(BigDecimal amount) { return amount.subtract(discountAmount); } } public class PercentageDiscountStrategy implements DiscountStrategy { private BigDecimal percentage; // 0.8 表示8折 public PercentageDiscountStrategy(BigDecimal percentage) { this.percentage = percentage; } @Override public BigDecimal applyDiscount(BigDecimal amount) { return amount.multiply(percentage); } } public class OverThresholdDiscountStrategy implements DiscountStrategy { private BigDecimal threshold; private BigDecimal discount; public OverThresholdDiscountStrategy(BigDecimal t, BigDecimal d) { this.threshold = t; this.discount = d; } @Override public BigDecimal applyDiscount(BigDecimal amount) { return (amount.compareTo(threshold) >= 0) ? amount.subtract(discount) : amount; } } // 3. 上下文 (Context) - 负责使用策略 public class DiscountContext { private DiscountStrategy strategy; // 设置策略 public void setStrategy(DiscountStrategy strategy) { this.strategy = strategy; } // 执行策略 public BigDecimal calculate(BigDecimal amount) { if (strategy == null) { throw new IllegalStateException("Discount strategy not set."); } return strategy.applyDiscount(amount); } } // 客户端代码 public class Client { public static void main(String[] args) { DiscountContext context = new DiscountContext(); BigDecimal orderAmount = new BigDecimal("150"); // 动态切换策略 context.setStrategy(new FixedDiscountStrategy(new BigDecimal("10"))); System.out.println("Fixed: " + context.calculate(orderAmount)); // 140 context.setStrategy(new PercentageDiscountStrategy(new BigDecimal("0.8"))); System.out.println("80% off: " + context.calculate(orderAmount)); // 120 context.setStrategy(new OverThresholdDiscountStrategy(new BigDecimal("100"), new BigDecimal("25"))); System.out.println("Over 100 minus 25: " + context.calculate(orderAmount)); // 125 } }6.3 策略模式与工厂模式的结合
在实际项目中,我们通常不会在客户端手动new具体的策略对象。而是结合工厂模式(或Spring的依赖注入)来管理策略的创建。
// 策略工厂 public class DiscountStrategyFactory { private static final Map<String, DiscountStrategy> strategies = new HashMap<>(); static { strategies.put("FIXED_10", new FixedDiscountStrategy(new BigDecimal("10"))); strategies.put("PERCENT_90", new PercentageDiscountStrategy(new BigDecimal("0.9"))); strategies.put("OVER_100_20", new OverThresholdDiscountStrategy(new BigDecimal("100"), new BigDecimal("20"))); // 可以从配置或数据库加载策略参数,动态创建 } public static DiscountStrategy getStrategy(String strategyCode) { DiscountStrategy strategy = strategies.get(strategyCode); if (strategy == null) { throw new IllegalArgumentException("Unknown strategy code: " + strategyCode); } return strategy; } } // 在Spring环境中,可以更优雅地使用 @Component 和 @Autowired @Service public class OrderService { @Autowired private Map<String, DiscountStrategy> strategyMap; // Spring会自动将所有DiscountStrategy实现注入为Map,key是bean name public BigDecimal calculateOrder(String strategyCode, BigDecimal amount) { DiscountStrategy strategy = strategyMap.get(strategyCode); if (strategy == null) { strategy = new NoDiscountStrategy(); } return strategy.applyDiscount(amount); } }核心优势:
- 消除条件判断:客户端代码(如
OrderService)中不再有冗长的if-else,只需通过一个标识符获取策略并执行。- 符合开闭原则:新增一种折扣策略,只需新增一个
DiscountStrategy的实现类,并在工厂或Spring容器中注册即可。OrderService的calculateOrder方法完全不需要修改。- 便于测试:每个策略都是独立的类,可以单独进行单元测试。
OrderService也可以轻松地用Mock策略进行测试。- 策略可共享:策略类通常是无状态的(只有行为,没有成员变量),可以被多个上下文共享实例,减少对象创建开销。
7. 模式应用中的共性陷阱与最佳实践
经过上面几个模式的代码演练,我们可以总结出一些跨模式的通用经验和容易踩的坑。
7.1 警惕过度设计和模式堆砌
这是新手最容易犯的错误。看到一点“变化”就想用模式,导致简单问题复杂化。
- 判断标准:问自己,这个变化发生的频率高吗?如果一年都改不了一次,用简单的
if-else可能更可读、更易维护。或者,未来扩展的可能性是否足够清晰?如果需求还很模糊,先使用简单实现,等模式真正需要时再重构(Refactoring to Patterns)。 - 反面案例:一个只有两种导出格式(Excel, CSV)的报告功能,非要用上抽象工厂、建造者、模板方法三个模式,创建了十几个类。这就是典型的过度设计。
7.2 理解模式的本质,而非死记结构
所有模式的根本目的都是解耦和应对变化。不要纠结于类图是否和《设计模式》书上一模一样。只要你的代码实现了“将变化的部分封装起来”、“针对接口编程”、“组合优于继承”这些原则,即使类名不同、结构略有差异,你也是在正确地使用模式思想。
- 例如,如果某个“策略”非常简单,只有一两行代码,使用Java 8以后的Lambda表达式或方法引用,可能比创建一个完整的策略类更简洁。
// 传统策略类 context.setStrategy(new DiscountStrategy() { @Override public BigDecimal applyDiscount(BigDecimal amount) { return amount.multiply(new BigDecimal("0.95")); } }); // Lambda简化 context.setStrategy(amount -> amount.multiply(new BigDecimal("0.95")));
7.3 结合现代框架和语言特性
在Spring Boot项目中,很多模式已经被框架以更优雅的方式实现了。
- 单例:Spring管理的Bean默认就是单例(
@Scope(“singleton”)),无需手写。 - 工厂:Spring的
ApplicationContext就是一个巨大的工厂,@Bean注解就是在定义产品,@Autowired就是在获取产品。 - 策略:如前所述,利用
Map<String, Strategy>的自动注入,可以极其方便地管理策略。 - 模板方法:Spring的
JdbcTemplate,RestTemplate等,都是模板方法模式的典范,它们定义了骨架,我们只需提供回调(如RowMapper)。
最佳实践是:优先使用框架提供的机制,当框架不满足或你在编写框架底层、工具类时,再考虑手写经典的设计模式实现。
7.4 命名至关重要
好的命名是“活文档”。使用模式时,在类名中体现其角色,能极大提升代码的可读性。
XxxFactory:一看就知道是创建Xxx的工厂。XxxAdapter:一看就知道是适配Xxx的适配器。XxxStrategy:一看就知道是Xxx的策略。XxxBuilder:一看就知道是构建Xxx的建造者。 避免使用XxxImpl,XxxHelper这种模糊的名字。
写代码时,多思考“这段代码未来最可能因为什么而修改?”。找到那个“变化点”,然后用合适的设计模式去封装它,这才是设计模式应用的真正起点。在接下来的系列文章中,我们会继续深入其他经典模式,比如观察者模式如何优雅处理事件驱动、责任链模式如何构建灵活的审批流程、代理模式如何实现AOP等,继续用扎实的代码和真实的场景,把每个模式讲透、用活。