充血模型编码实践

失血模型

简单来说,就是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();
    }
}

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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值