失血模型
简单来说,就是domain object只有属性的getter/setter方法,没有任何业务逻辑。
@Data
@ToString
public class User {
private Long id;
private String username;
private String password;
private Integer status;
private Date createdAt;
private Date updatedAt;
private Integer isDeleted;
}
-------------------------------------------------
public class UserService{
public boolean isActive(User user){
return user.getStatus().equals(StatusEnum.ACTIVE.getCode());
}
}
贫血模型
在失血模型基础之上聚合了业务领域行为,领域对象的状态变化停留在内存层面,不关心数据持久化。
@Data
@ToString
public class User {
private Long id;
private String username;
private String password;
private Integer status;
private Date createdAt;
private Date updatedAt;
private Integer isDeleted;
public boolean isActive(User user){
return user.getStatus().equals(StatusEnum.ACTIVE.getCode());
}
public void setUsername(String username){
return username.trim();
}
}
简单来说,就是domain ojbect包含了不依赖于持久化的领域逻辑,而那些依赖持久化的领域逻辑被分离到Service层。Service(业务逻辑,事务封装) --> DAO —> domain object
这种模型的优点:
1、各层单向依赖,结构清楚,易于实现和维护
2、设计简单易行,底层模型非常稳定
这种模型的缺点:
1、domain object的部分比较紧密依赖的持久化domain logic被分离到Service层,显得不够OO
2、Service层过于厚重
充血模型
在贫血模型基础上,负责数据的持久化。
@Data
@ToString
public class User {
private Long id;
private String username;
private String password;
private Integer status;
private Date createdAt;
private Date updatedAt;
private Integer isDeleted;
private UserRepository userRepository;
public boolean isActive(User user){
return user.getStatus().equals(StatusEnum.ACTIVE.getCode());
}
public void setUsername(String username){
this.username = username.trim();
userRepository.update(user);
}
}
充血模型和第二种模型差不多,所不同的就是如何划分业务逻辑,即认为,绝大多业务逻辑都应该被放在domain object里面(包括持久化逻辑),而Service层应该是很薄的一层,仅仅封装事务和少量逻辑,不和DAO层打交道。
Service(事务封装) —> domain object <—> DAO
这种模型的优点:
1、更加符合OO的原则
2、Service层很薄,只充当“表面”的角色,不和DAO打交道。
这种模型的缺点:
1、DAO和domain object形成了双向依赖,复杂的双向依赖会导致很多潜在的问题。
2、如何划分Service层逻辑和domain层逻辑是非常含混的,在实际项目中,由于设计和开发人员的水平差异,可能导致整个结构的混乱无序。
3、考虑到Service层的事务封装特性,Service层必须对所有的domain object的逻辑提供相应的事务封装方法,其结果就是Service完全重定义一遍所有的domain logic,非常烦琐,而且Service的事务化封装其意义就等于把OO的domain logic转换为过程的Service Transaction Script。该充血模型辛辛苦苦在domain层实现的OO在Service层又变成了过程式,对于Web层程序员的角度来看,和贫血模型没有什么区别了 。
胀血模型
基于充血模型的第三个缺点,有同学提出,干脆取消Service层,只剩下domain object和DAO两层,在domain object的domain logic上面封装事务。
domain object(事务封装,业务逻辑) <—> DAO
该模型优点:
1、简化了分层
2、也算符合OO
该模型缺点:
1、很多不是domain logic的service逻辑也被强行放入domain object ,引起了domain ojbect模型的不稳定
2、domain object暴露给web层过多的信息,可能引起意想不到的副作用。
在这四种模型当中,失血模型和胀血模型应该是不被提倡的。而贫血模型和充血模型从技术上来说,都已经是可行的了。但是我个人仍然主张使用贫血模型。
代码层面理解什么是贫血模型与充血模型?
回答这个问题,我们从《重构》一书中的一个订单的开发场景,分别使用贫血模型与充血模型来实现,大家可以从中感受其差别理解它们的不同。
订单的场景
需求描述
- 创建订单
- 设置订单优惠
订单场景(失)贫血模型实现
Order 类 , 只包含了属性的Getter,Setter方法
@Data
public class Order {
private long orderId;
private int buyerId;
private int sellerId;
private BigDecimal amount;
private BigDecimal shippingFee;
private BigDecimal discountAmount;
private BigDecimal payAmount;
private String address;
}
OrderService ,根据订单创建中的业务逻辑,组装order数据对象,最后进行持久化
/**
* 创建订单
* @param buyerId
* @param sellerId
* @param orderItems
*/
public void createOrder(int buyerId,int sellerId,List<OrderItem> orderItems){
//新建一个Order数据对象
Order order = new Order();
order.setOrderId(1L);
//算订单总金额
BigDecimal amount = orderItems.stream()
.map(OrderItem::getPrice)
.reduce(BigDecimal.ZERO,BigDecimal::add);
order.setAmount(amount);
//运费
order.setShippingFee(BigDecimal.TEN);
//优惠金额
order.setDiscountAmount(BigDecimal.ZERO);
//支付总额 = 订单总额 + 运费 - 优惠金额
BigDecimal payAmount = order.getAmount().add(order.getShippingFee()).subtract(order.getDiscountAmount());
order.setPayAmount(payAmount);
//设置买卖家
order.setBuyerId(buyerId);
order.setSellerId(sellerId);
//设置收获地址
order.setAddress(JSON.toJSONString(new Address()));
//写库
orderDao.insert(order);
orderItems.forEach(orderItemDao::insert);
}
在此种方式下,核心业务逻辑散落在OrderService中,比如获取订单总额与订单可支付金额是非常重要的业务逻辑,同时对象数据逻辑一同混编,在此种模式下,代码不能够直接反映业务,也违背了面向对象的SRP原则。
设置优惠
/**
* 设置优惠
* @param orderId
* @param discountAmount
*/
public void setDiscount(long orderId, BigDecimal discountAmount){
Order order = orderDao.find(orderId);
order.setDiscountAmount(discountAmount);
//从新计算支付金额
BigDecimal payAmount = order.getAmount().add(order.getShippingFee()).subtract(discountAmount);
order.setPayAmount(payAmount);
//orderDao => 通过主键更新订单信息
orderDao.updateByPrimaryKey(order);
}
贫血模型在设置折扣时因为需要考虑到折扣引发的支付总额的变化,因此还需要在从新的有意识的计算支付总额,因为面向数据开发需要时刻考虑数据的联动关系,在这种模式下忘记了修改某项关联数据的情况可能是时有发生的。
订单场景充血模型实现
Order 类,包含了业务关键属于以及行为,同时具有良好的封装性
/**
* @author zhengyin
* Created on 2021/10/18
*/
@Getter
public class Order {
private long orderId;
private int buyerId;
private int sellerId;
private BigDecimal shippingFee;
private BigDecimal discountAmount;
private Address address;
private Set<OrderItem> orderItems;
//空构造,只是为了方便演示
public Order(){}
public Order(long orderId,int buyerId ,int sellerId,Address address, Set<OrderItem> orderItems){
this.orderId = orderId;
this.buyerId = buyerId;
this.sellerId = sellerId;
this.address = address;
this.orderItems = orderItems;
}
/**
* 更新收货地址
* @param address
*/
public void updateAddress(Address address){
this.address = address;
}
/**
* 支付总额等于订单总额 + 运费 - 优惠金额
* @return
*/
public BigDecimal getPayAmount(){
BigDecimal amount = getAmount();
BigDecimal payAmount = amount.add(shippingFee);
if(Objects.nonNull(this.discountAmount)){
payAmount = payAmount.subtract(discountAmount);
}
return payAmount;
}
/**
* 订单总价 = 订单商品的价格之和
* amount 可否设置为一个实体属性?
*/
public BigDecimal getAmount(){
return orderItems.stream()
.map(OrderItem::getPrice)
.reduce(BigDecimal.ZERO,BigDecimal::add);
}
/**
* 运费不能为负
* @param shippingFee
*/
public void setShippingFee(BigDecimal shippingFee){
Preconditions.checkArgument(shippingFee.compareTo(BigDecimal.ZERO) >= 0, "运费不能为负");
this.shippingFee = shippingFee;
}
/**
* 设置优惠
* @param discountAmount
*/
public void setDiscount(BigDecimal discountAmount){
Preconditions.checkArgument(discountAmount.compareTo(BigDecimal.ZERO) >= 0, "折扣金额不能为负");
this.discountAmount = discountAmount;
}
/**
* 原则上,返回给外部的引用,都应该防止间接被修改
* @return
*/
public Set<OrderItem> getOrderItems() {
return Collections.unmodifiableSet(orderItems);
}
}
OrderService , 仅仅负责流程的调度
/**
* 创建订单
* @param buyerId
* @param sellerId
* @param orderItems
*/
public void createOrder(int buyerId, int sellerId, Set<OrderItem> orderItems){
Order order = new Order(1L,buyerId,sellerId,new Address(),orderItems);
//运费不随订单其它信息一同构造,因为运费可能在后期会进行修改,因此提供一个设置运费的方法
order.setShippingFee(BigDecimal.TEN);
orderRepository.save(order);
}
在此种模式下,Order类完成了业务逻辑的封装,OrderService仅负责业务逻辑与存储之间的流程编排,并不参与任何的业务逻辑,各模块间职责更明确。
设置优惠
/**
* 设置优惠
* @param orderId
* @param discountAmount
*/
public void setDiscount(long orderId, BigDecimal discountAmount){
Order order = orderRepository.find(orderId);
order.setDiscount(discountAmount);
orderRepository.save(order);
}
在充血模型的模式下,只需设置具体的优惠金额,因为在Order类中已经封装了相关的计算逻辑,比如获取支付总额时,是实时通过优惠金额来计算的。
/**
* 支付总额等于订单总额 + 运费 - 优惠金额
* @return
*/
public BigDecimal getPayAmount(){
BigDecimal amount = getAmount();
BigDecimal payAmount = amount.add(shippingFee);
if(Objects.nonNull(this.discountAmount)){
payAmount = payAmount.subtract(discountAmount);
}
return payAmount;
}
写到这里,可能大家会有疑问,文章都在讲充血模型的业务,那数据怎么进行持久化?
数据持久化时我们通过封装的 OrderRepository 来进行持久化操作,根据存储方式的不同提供不同的实现,以数据库举例,那么我们需要将Order转换为PO对象,也就是持久化对象,这时的持久化对象就是面向数据表的贫血模型对象。
比如下面的伪代码
public class OrderRepository {
private final OrderDao orderDao;
private final OrderItemDao orderItemDao;
public OrderRepository(OrderDao orderDao, OrderItemDao orderItemDao) {
this.orderDao = orderDao;
this.orderItemDao = orderItemDao;
}
public void save(Order order){
// 在此处通过Order实体,创建数据对象 new OrderPO() ; new OrderItemPO();
// orderDao => 存储订单数据
// orderItemDao => 存储订单商品数据
}
public Order find(long orderId){
//找到数据对象,OrderPO
//找到数据对象,OrderItemPO
//组合返回,order实体
return new Order();
}
}
通过上面两种实现方式的对比,相信读者对两种模型已经有了明确的认识了,在贫血模型中,数据和业务逻辑是割裂的,而在充血模型中数据和业务是内聚的。