充血模型编码实践

失血模型

简单来说,就是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层过多的信息,可能引起意想不到的副作用。

在这四种模型当中,失血模型和胀血模型应该是不被提倡的。而贫血模型和充血模型从技术上来说,都已经是可行的了。但是我个人仍然主张使用贫血模型。

代码层面理解什么是贫血模型与充血模型?

回答这个问题,我们从《重构》一书中的一个订单的开发场景,分别使用贫血模型与充血模型来实现,大家可以从中感受其差别理解它们的不同。

订单的场景

需求描述
  1. 创建订单
  2. 设置订单优惠
订单场景(失)贫血模型实现

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();}
}

通过上面两种实现方式的对比,相信读者对两种模型已经有了明确的认识了,在贫血模型中,数据和业务逻辑是割裂的,而在充血模型中数据和业务是内聚的。


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部