几种不常见但有意思的设计模式
最近在整理重构公司的部分代码,看了不少以前的遗留代码,想分享一下一些不太常见(或较少讨论)的设计模式,它们在正确的上下文中仍然非常有用。当然,这些模式并不像 "Gang of Four "广为人知的模式(单例、工厂、策略等)那样受到关注。
Null Object Pattern
空对象模式提供了一个非操作或 "无操作 "对象,可以安全地代替空引用。与其在调用方法前不断检查是否为空,不如提供一个 "空对象 "来实现相同的接口,但执行中性或无操作的操作。
优点
- 消除重复的无效检查。
- 降低出现 NullPointerException 的风险。
- 避免在方法调用周围使用条件句(例如 if (obj != null) todo)来保持代码的简洁。
缺点
- 需要额外创建一个空对象,这可能会增加代码量。
使用场景
- 经常遇到共享接口的空检查。
- 在大型项目中,可实现行为可合并为中性的 "空 "实现。
- 当您想让代码更健壮,并删除不必要的 if-else 分支时
代码例子
public interface Payment {
void pay();
}
public class CreditCardPayment implements Payment {
@Override
public void pay() {
System.out.println("Processing credit card payment...");
}
}
public class NullPayment implements Payment {
@Override
public void pay() {
// Do nothing, this is the "null" operation
}
}
// Usage
public class PaymentProcessor {
private Payment payment;
public PaymentProcessor(Payment payment) {
// If payment is null, assign a NullPayment
this.payment = (payment != null) ? payment : new NullPayment();
}
public void processPayment() {
payment.pay(); // Safe call, never throws NullPointerException
}
}
Parameter Object Pattern
参数对象模式将一个方法(或构造函数)的多个参数组合成一个对象。您无需单独传递多个参数,只需传递一个包含所有必要数据的 "参数对象 "即可。
优点
- 减少了冗长的参数列表,而冗长的参数列表可能会成为代码中的异味。
- 提高了可读性,使重构更容易(参数的添加/删除集中化)。
- 您可以在不同层之间传递参数对象,以确保一致性。
使用场景
- 当一个方法(或构造函数)有很多参数(通常为 4 个或更多参数)时。
- 当参数在逻辑上属于同一个参数时(如搜索过滤器、配置选项)。
- 在不同方法或类中重复传递同一组参数时。
代码
// Parameter object holding various parameters
public class SearchCriteria {
private String query;
private int pageNumber;
private int pageSize;
private boolean caseSensitive;
// Constructor and getters omitted for brevity
public SearchCriteria(String query, int pageNumber, int pageSize, boolean caseSensitive) {
this.query = query;
this.pageNumber = pageNumber;
this.pageSize = pageSize;
this.caseSensitive = caseSensitive;
}
// getters...
}
public class SearchEngine {
public void search(SearchCriteria criteria) {
// Use the parameter object instead of multiple parameters
System.out.println("Search: " + criteria.getQuery());
System.out.println("Page Number: " + criteria.getPageNumber());
System.out.println("Page Size: " + criteria.getPageSize());
System.out.println("Case Sensitive: " + criteria.isCaseSensitive());
// Implementation ...
}
}
// Usage
public class App {
public static void main(String[] args) {
SearchCriteria criteria = new SearchCriteria("Java patterns", 1, 20, false);
new SearchEngine().search(criteria);
}
}
Execute Around Method Pattern
Execute Around Method 模式封装了一个存储过程(通常涉及资源处理),因此调用者只需提供 "业务逻辑",而模板则在内部处理。这与 Java 的 try-with-resources 背后的理念有关。
优点
- 集中资源获取和发布逻辑。
- 始终妥善处理拆卸工作,防止资源泄漏。
- 当多个操作共享相同的前置/后置逻辑时,可减少重复操作。
使用场景
- 处理常见资源管理任务(文件、数据库连接等)时。
- 当您想在一个地方执行标准使用模式(打开/使用/关闭)时。
- 只要能将 "什么"(业务逻辑)与 "如何"(获取、释放资源)分开
代码
// Define a functional interface for your around method
@FunctionalInterface
public interface FileProcessor {
void process(BufferedReader reader) throws IOException;
}
public class FileService {
public static void executeAroundFile(String filePath, FileProcessor processor) {
try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {
// Acquire resource
processor.process(br);
// Automatic close because of try-with-resources
} catch (IOException e) {
e.printStackTrace();
}
}
}
// Usage
public class App {
public static void main(String[] args) {
FileService.executeAroundFile("data.txt", reader -> {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("Read line: " + line);
}
});
}
}
Service Loader Pattern
利用 Java Service Provider Interface (SPI) 机制(通过 java.util.ServiceLoader)在运行时动态加载给定接口或抽象类的实现。这通常被称为插件机制。
优点
- 将接口与具体实现解耦。
- 允许在运行时添加新的实现(插件)(只需将它们放在 classpath 上)。
- 方便需要自动发现服务的框架使用。
使用场景
- 当您需要一个灵活的插件系统时。
- 在模块化或可扩展的应用程序中,运行时可以发现不同的实现方式。
- 当您想避免代码与特定实现紧密耦合时
代码
// 定义服务接口:
public interface GreetingService {
String greet(String name);
}
// 实现服务:
public class EnglishGreetingService implements GreetingService {
@Override
public String greet(String name) {
return "Hello, " + name;
}
}
// 在 META-INF/services 中创建名为 com.example.GreetingService 的文件,文件内容为
com.example.EnglishGreetingService
// 使用 ServiceLoader:
public class GreetingApp {
public static void main(String[] args) {
ServiceLoader<GreetingService> loader = ServiceLoader.load(GreetingService.class);
for (GreetingService service : loader) {
System.out.println(service.greet("Alice"));
}
}
}
转换器或映射器模式
这是一种模式,其中的类(有时称为映射器、转换器或转换器)专门用于在不同的数据表示之间进行转换(例如,域对象和 DTO 之间的转换)。
优点
- 将转换逻辑集中在一处,而不是将转换分散在整个代码中。
- 通过隔离转换提高可测试性(可以只对映射器进行单元测试)。
- 适用于对象在层与层之间存在差异的分层架构(如实体到 DTO 的映射)
使用场景
- 在将域对象与 DTO 或视图模型分开的应用程序中。
- 无论何时您需要进行复杂的数据转换,这些转换都必须是一致的、可测试的。
- 当你希望不同层(表现层、域层、持久层)之间有清晰的界限时。
代码
// Domain object
public class User {
private String username;
private String email;
// constructors, getters, setters
}
// DTO
public class UserDTO {
private String userName;
private String emailAddress;
// constructors, getters, setters
}
// Converter
public class UserConverter {
public UserDTO toDto(User user) {
UserDTO dto = new UserDTO();
dto.setUserName(user.getUsername());
dto.setEmailAddress(user.getEmail());
return dto;
}
public User toEntity(UserDTO dto) {
User user = new User();
user.setUsername(dto.getUserName());
user.setEmail(dto.getEmailAddress());
return user;
}
}
// Usage
public class App {
public static void main(String[] args) {
User user = new User("alice", "alice@example.com");
UserConverter converter = new UserConverter();
UserDTO dto = converter.toDto(user); // Domain -> DTO
User user2 = converter.toEntity(dto); // DTO -> Domain
}
}
小结
上面列举了一些Java中不像经典的 "23种设计模式 "那样常被引用的模式,但它们确实也解决了 Java 开发中反复出现的一些实际问题。当然,实际使用场景中,可能需要结合使用多种模式,以解决更复杂的问题。亦或者有着其他更合适的模式。关于设计模式、架构设计这些,个人认为没有标准答案,上面只是一些标准的例子罢了。在实际的开发过程中,更重要的是理解设计模式背后的思想,而不是生搬硬套某种模式。模式的选择应当基于具体的业务需求、代码的可读性、可维护性以及团队的开发习惯。
此外,随着技术的发展和业务需求的变化,一些新的编程范式和架构思想也在不断涌现,比如领域驱动设计(DDD)、事件驱动架构(EDA)、微服务架构等,它们并非单纯的设计模式,而是更高层次的软件设计思想,往往结合了多种模式的优点。
在实践中,我们可能会遇到代码臃肿、扩展性差、维护成本高等问题,而设计模式的合理应用可以帮助我们降低耦合、提高代码复用性,使系统更加稳定和灵活。但设计模式并非银弹,滥用模式可能会增加代码复杂度,使其难以理解和维护。因此,在设计软件时,我们需要权衡利弊,找到最适合当前场景的解决方案。
总的来说,设计模式是软件开发中的一种工具,而不是目标。真正优秀的架构师或开发者,能够灵活运用设计模式,并结合实际需求,构建出既符合业务需求,又具备良好可维护性的系统。