Skip to content

几种不常见但有意思的设计模式

最近在整理重构公司的部分代码,看了不少以前的遗留代码,想分享一下一些不太常见(或较少讨论)的设计模式,它们在正确的上下文中仍然非常有用。当然,这些模式并不像 "Gang of Four "广为人知的模式(单例、工厂、策略等)那样受到关注。

Null Object Pattern

空对象模式提供了一个非操作或 "无操作 "对象,可以安全地代替空引用。与其在调用方法前不断检查是否为空,不如提供一个 "空对象 "来实现相同的接口,但执行中性或无操作的操作。

优点

  • 消除重复的无效检查。
  • 降低出现 NullPointerException 的风险。
  • 避免在方法调用周围使用条件句(例如 if (obj != null) todo)来保持代码的简洁。

缺点

  • 需要额外创建一个空对象,这可能会增加代码量。

使用场景

  • 经常遇到共享接口的空检查。
  • 在大型项目中,可实现行为可合并为中性的 "空 "实现。
  • 当您想让代码更健壮,并删除不必要的 if-else 分支时

代码例子

java
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 个或更多参数)时。
  • 当参数在逻辑上属于同一个参数时(如搜索过滤器、配置选项)。
  • 在不同方法或类中重复传递同一组参数时。

代码

java
// 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 背后的理念有关。

优点

  • 集中资源获取和发布逻辑。
  • 始终妥善处理拆卸工作,防止资源泄漏。
  • 当多个操作共享相同的前置/后置逻辑时,可减少重复操作。

使用场景

  • 处理常见资源管理任务(文件、数据库连接等)时。
  • 当您想在一个地方执行标准使用模式(打开/使用/关闭)时。
  • 只要能将 "什么"(业务逻辑)与 "如何"(获取、释放资源)分开

代码

java
// 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 上)。
  • 方便需要自动发现服务的框架使用。

使用场景

  • 当您需要一个灵活的插件系统时。
  • 在模块化或可扩展的应用程序中,运行时可以发现不同的实现方式。
  • 当您想避免代码与特定实现紧密耦合时

代码

java
// 定义服务接口:
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 或视图模型分开的应用程序中。
  • 无论何时您需要进行复杂的数据转换,这些转换都必须是一致的、可测试的。
  • 当你希望不同层(表现层、域层、持久层)之间有清晰的界限时。

代码

java
// 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)、微服务架构等,它们并非单纯的设计模式,而是更高层次的软件设计思想,往往结合了多种模式的优点。

在实践中,我们可能会遇到代码臃肿、扩展性差、维护成本高等问题,而设计模式的合理应用可以帮助我们降低耦合、提高代码复用性,使系统更加稳定和灵活。但设计模式并非银弹,滥用模式可能会增加代码复杂度,使其难以理解和维护。因此,在设计软件时,我们需要权衡利弊,找到最适合当前场景的解决方案。

总的来说,设计模式是软件开发中的一种工具,而不是目标。真正优秀的架构师或开发者,能够灵活运用设计模式,并结合实际需求,构建出既符合业务需求,又具备良好可维护性的系统。

Released under the MIT License.