在实际编码过程中,我们经常会遇到各种容器类,它们有时被称为POJO,有时又被称作DTO、VO、DO等。这类类主要承担容器的角色,通常包含完整的getter和setter方法,作为信息载体在系统各层之间传输数据。

容器类的本质与分类

从更广义的角度看,许多编程元素都可以视为某种形式的"容器"。比如,仅起标记作用的接口,如Serializable、各种**Aware接口等;注解元信息,如@Order提供排序信息、@Value提供值注入、@Autowired提供属性注入等。

这类容器类通常很少包含面向对象的业务方法,又被称为贫血模型。与之相对的是充血模型,更适合复杂系统设计。Spring源码显然以充血模型为主,充分体现了面向对象的核心思想:对象 = 数据 + 行为。不过,Spring中也存在大量容器类,甚至某些类的部分功能也可以视为容器作用。

函数式编程视角下的数据载体

在函数式编程思想中,对象通常是不可变的。这种设计带来诸多优势:

  • 线程安全:便于支持并发编程

  • 无状态性:极大简化代码理解,所有状态变化都是显式的

  • 性能优化:便于编译器进行延迟计算、对象复用等优化

  • 可测试性:函数无副作用,相同输入总是产生相同输出

这种对象称为值对象。Lombok中的@Value注解、JDK14引入的record关键字都实现了类似功能。例如,Map容器对Key的要求就是值对象。

实战案例解析

案例一:MVC模型中的数据传递

在MVC架构中,Model、View、Controller之间传输的数据对象通常使用POJO。典型场景是:请求到达时,Spring MVC将JSON格式的字符串反序列化为POJO(通常命名为**Request);经过业务计算或数据库交互后,返回可序列化的POJO对象,再由Spring MVC序列化为JSON响应。

这种设计实现了数据与表现的分离,让业务逻辑专注于数据处理,而不必关心数据的具体传输格式。

案例二:配置属性容器

我们常用的@Value@ConfigurationProperties注解能够快速配置对象属性,结合@RefreshScope可以实现配置的动态刷新。@Value注解还支持EL表达式,为配置取值提供了极大的灵活性。

java

@ConfigurationProperties(prefix = "app.datasource")
@Data
public class DataSourceConfig {
    private String url;
    private String username;
    private String password;
    private int maxPoolSize;
}

这种配置类就是典型的数据容器,将分散的配置项聚合为有意义的业务对象。

案例三:Spring事件机制

观察者模式中的事件对象应该是值对象。当一个事件被多个监听器处理时,如果有监听器修改了事件属性,其他监听器无法确定事件的原始状态,会导致系统行为不可预测。

以Spring容器刷新过程为例,在refresh模板方法执行完成后,会调用finishRefresh方法发布ContextRefreshedEvent事件。查看该事件的源码可以发现,其所有属性都在构造函数中确定,不提供setter方法:

java

public class ContextRefreshedEvent extends ApplicationContextEvent {
    public ContextRefreshedEvent(ApplicationContext source) {
        super(source);
    }
}

// 继承链上的父类都保持了不可变性
public abstract class ApplicationEvent extends EventObject {
    private final long timestamp;
    
    public ApplicationEvent(Object source) {
        super(source);
        this.timestamp = System.currentTimeMillis();
    }
    
    public final long getTimestamp() {
        return this.timestamp;
    }
}

这种不可变设计确保了事件在传递过程中的一致性。

案例四:Monad模式的应用

前置知识:Java Stream的操作如filtermapflatMapcollectreduce等,其参数通常为函数。

Monad是一个复杂的概念,但从使用角度可以简化为:一个包装其他对象的容器,该容器封装了对象的行为,支持mapflatMap等方法

简单来说,Optional就是一个Monad(用于处理可能为null的情况),数据库查询的返回值应该是Optional类型。

Java8引入Stream后,集合类型也可以视为Monad:

java

int totalSalary = members.stream()
    .filter(validPredicate)
    .map(Member::getSalary)
    .mapToInt(x -> x)
    .sum();

Monad的flatMap方法可以避免"套娃"式嵌套。在Java中,典型的嵌套问题出现在Try-Catch场景中。使用vavr库的Try可以优雅解决:

java

// 查询职员A的主管
Try<Member> manager = Try.of(() -> repo.getByName("A"))
    .flatMap(member -> Try.of(() -> repo.getByName(member.getManager())));

案例五:配置类中的数据传递

使用@Order注解可以为一组类指定执行顺序。即使不了解具体的处理类,我们也能推断其处理过程必然包含:获取注解值、建立类与排序号的关联、执行通用排序算法。

SpringBoot自动配置了大量类,虽然我们不一定清楚配置如何映射到容器中的Bean,但可以确定数据传递经历了配置 → 解析 → 注册Bean的过程。从实用角度出发,多数情况下我们不需要了解Bean创建的具体逻辑,只需知道如何使用和配置,理解每个Bean的功能边界即可。

事实上,在Spring项目中,除了少数策略类(各种**Strategy),大多数类都可以视为某种形式的容器类。

设计启示与总结

  1. 关注行为而非数据位置:在阅读源码时,我们应该更关注类的行为和数据处理的逻辑,而将数据存储的具体位置放在次要地位。

  2. 选择合适的数据表示:数据的表示可以是POJO类、值类型、数组、基本数据类型或其组合。在编码过程中,我们应该特别注意数据结构的可读性,并优先考虑使用不可变对象来表示数据。

  3. 理解数据流设计:数据的传递路径反映了系统的架构设计。有时候虽然不清楚数据传递的具体过程,但我们更关心数据传递的最终结果和影响。

通过理解这些容器类和数据传递模式,我们能够更好地设计出清晰、可维护的系统架构,在复杂性和简洁性之间找到平衡点。

更多推荐