容器类与数据传递:软件设计中的数据载体模式
在实际编码过程中,我们经常会遇到各种容器类,它们有时被称为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的操作如filter、map、flatMap、collect、reduce等,其参数通常为函数。
Monad是一个复杂的概念,但从使用角度可以简化为:一个包装其他对象的容器,该容器封装了对象的行为,支持map、flatMap等方法。
简单来说,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),大多数类都可以视为某种形式的容器类。
设计启示与总结
-
关注行为而非数据位置:在阅读源码时,我们应该更关注类的行为和数据处理的逻辑,而将数据存储的具体位置放在次要地位。
-
选择合适的数据表示:数据的表示可以是POJO类、值类型、数组、基本数据类型或其组合。在编码过程中,我们应该特别注意数据结构的可读性,并优先考虑使用不可变对象来表示数据。
-
理解数据流设计:数据的传递路径反映了系统的架构设计。有时候虽然不清楚数据传递的具体过程,但我们更关心数据传递的最终结果和影响。
通过理解这些容器类和数据传递模式,我们能够更好地设计出清晰、可维护的系统架构,在复杂性和简洁性之间找到平衡点。
更多推荐
所有评论(0)