By Chen.lin3 min read673 words

OOP 与 充血/贫血模型

Backend DevelopmentJAVA

背景:

公司的业务集中在电商领域,基于 shopify 进行电商网站的开发。于是,在空闲时间,我计划使用 java spring 去写自己的电商后台(因为一直以来我们都是依赖于shopify做一些扩展,类似于产品,订单等都是在shopify平台上,实际的后端业务很少)。希望借此提升电商领域的业务理解和开发技术上的设计能力。在实操过程中,我慢慢发现了以往的问题(实际上因为我使用的是所谓的贫血模型的方式,所有的业务逻辑都堆在service中,大量的重复性代码,导致service非常臃肿,我实在是看不下去了,也写不下去了;以前都是简单的业务,倒也不觉得,一旦有较为复杂的业务,代码量稍微多一点就很难维护和修改。因为之前都是调用shopify的接口,这些问题相当于都被别人处理了,把这些都堆在一个叫做 ShopifyService的服务中即可,这个服务就专门用来调用接口),再读了好几篇博文再加之ai的帮助,写下这篇blog记录下。

原来我之前一直用的是贫血模型,刚毕业就来了这里,在学校的时候一直听老教授们告诉我们 面向过程/面向对象 设计。 那时候我只是知道学C的时候是面向过程,学java的是面向对象,C++是都可以。 毕业了来到公司 以为前辈们写的方式(spring boot项目)就是真正的 OOP . 现在理解了 贫血模型根本就是违反了OOP的基本原则:对象应该同时有数据和行为和OOP背道而驰【但是贫血模型中只有数据,所有业务逻辑在service中,OOP变成了事务脚本】。这里不是完全否定贫血模型的意思。但是显然这是更简单的一种方式,当然不能只会简单的方式。

这里引用一句(也是我从其他博主那里看到的): 曾经Spring的作者Rod Johnson也承认,Spring 不过是在沿袭 EJB2 时代的“事务脚本”,也就是面向过程编程。

这里直接让AI给我总结下充血/贫血模型的区别:

比较维度贫血模型(Anemic Model)充血模型(Rich Model)
定义只有数据(字段),没有业务行为的方法同时包含数据和与之相关的业务行为
职责划分数据结构与业务逻辑分离,逻辑通常在 Service 层对象自身封装业务逻辑,职责内聚
面向原则更接近过程式编程,OOP 不彻底体现面向对象的封装、聚合、职责
代码结构Entity 只含字段和 getter/setterEntity 除字段外包含验证、状态变更等方法
测试方便性单元测试主要写在 service 层每个对象方法可单测,聚焦职责
演化灵活性修改业务逻辑需要修改 service 层对象演化更自然,改动集中
常见用途场景CRUD 型应用、事务脚本架构复杂业务、DDD(领域驱动设计)
开发初期门槛较低,容易理解高,需理解建模思想
维护成本随逻辑复杂增加,service 层容易变得臃肿随对象划分,逻辑分布清晰,长远更易维护
示例(加入购物车)Cart 类只有字段,加入逻辑写在 CartServiceCart.addItem(Product) 封装加入逻辑

可见 其实真正的面向对象设计起来的门槛更高,我自己实操下来也是深有体会。

直入主题:如何使用充血模型进行开发(使用购物车这个模型的例子解释)

在我实际的操作中,我发现。充血模型的设计过程大不相同。

过去,我拿到设计稿,理解了业务逻辑之后。先分析需要哪些接口,每个接口有哪些数据,然后可能会建表,表一建完,直接就是无脑一条龙, 从Controller层到Service层再到DAO(或是其他的什么 Repository),最后打到数据库。一条线下来,只要牵住一头一尾,头是controller的接口,尾巴是数据库的SQL, 中间是猪的肚子 service。 没错 这就是我们 crud coder, 有些博主说这是 面向SQL编程,反正要么是一个复杂的service,要么是一个复杂的SQL.

现在,为了设计充血模型,在定义 interface 就耗费了我大量的时间,

  1. 购物车的唯一id 应该是用户id ,每个用户有唯一的购物车对象,因此想到后续的实现应该有 customer id 数据
  2. 购物车中所有的商品行组成一个 List
  3. 为了安全 设计了购物车的签名和验签方法
  4. 为了前端的显示 应该避免一些不可暴露的字段给用户 设计了 view 方法给前端输出 VO
  5. 购物车中的商品应该有选中和取消选中的功能
  6. 购物车的商品应该可以删除和修改(修改包含数量和商品的型号(选择不同的变体))后来我直接把修改型号的改为删除了该商品再添加 就变成了 updateQuantity 和 delete
  7. 添加商品到购物车的方法 add
  8. 设计了缓存和持久化的方法 cache save
  9. 如果购物车中还支持不同的店铺进行分类的话 可能还要改 这里所有的商品行是直接铺下来
/** * 代表一个用户的购物车实体 */ public interface Cart { void setCustomerId(Long id); Long getCustomerId(); void setItems(List<CartItem> items); List<CartItem> getItems(); /** * validate cart data * @return true -> safe */ Boolean validate(); /** * sign * @return sign */ String sign(); /** * show cart to customer * @return CartVo */ CartVo view(); /** * check one item of cart * @param id cart item id * @param checked status: choose or not */ void check(String id, Boolean checked); /** * delete * @param id cart item id */ void delete(String id); /** * update quantity * @param id cart item id * @param quantity add or minus item quantity */ void updateQuantity(String id, Integer quantity); /** * add product to cart use variant id * @param id variant id * @param quantity quantity * @param product Product */ void add(Long id, Integer quantity, Product product); /** * refresh cache */ void cache(); /** * save */ void save(); }

这样应该算是一个充血模型了,这样服务层就可以直接调用这些方法来完成业务逻辑了。

如果能设计一个合理的充血模型,那么最后service将会变得很清晰,用别的博主的话来说  “服务层很薄——所有重要的业务逻辑都写在领域层” 这里的领域层就是指模型层,比如我们这里的购物车领域的业务实体。

下面是购物车服务的接口和实现,从实现中可以看到为什么服务层中变薄了很多。

这样服务层中就只剩下一组和业务相关的方法,非常清晰。

public interface CartService { /** * 查询用户的购物车 * @param userId 用户 id * @return CartVo 购物车整体描述(验签) */ Result<CartVo> getCartView(Long userId); /** * 加购单个商品一定数量 * @param userId 用户 id * @param productId 产品 id * @param variantId 产品变体 id * @param quantity 加购数量 * @return CartVo 购物车整体描述(验签) */ Result<CartVo> addToCart(Long userId, Long productId, Long variantId, Integer quantity); /** * 更新购物车项的数量 * @param userId 用户 id * @param cartItemId 购物车项 id * @param quantity 修改的目标数量 * @param variantId 修改的目标变体 * @return CartVo 购物车整体描述(验签) */ Result<CartVo> updateCart(Long userId, String cartItemId , Integer quantity, Long productId, Long variantId); /** * 删除购物车项 * @param userId 用户 id * @param cartItemId 购物车项 id * @return */ Result<CartVo> deleteCart(Long userId, String cartItemId); /** * 选中或者取消选中 CartItem * @param userId 用户 id * @param cartItemId 购物车项 id * @param checked true 选中,false 取消 * @return CartVo 购物车整体描述(验签) */ Result<CartVo> checkCartItem(Long userId, String cartItemId, Boolean checked ); /** * 根据购物车的选中状态去生成订单 * @param userId 用户 id * @param clientAmount 金额只做验证,防止计算最终价格与用户所见不一致 * @param sign 购物车签名 * @return OrderPreviewVo 订单预览 */ Result<OrderPreviewVo> genOrder(Long userId, BigDecimal clientAmount, String sign); }
@Service public class CartServiceImpl implements CartService { private final CartRepository cartRepository; private final ProductRepository productRepository; public CartServiceImpl( CartRepository cartRepository, ProductRepository productRepository) { this.cartRepository = cartRepository; this.productRepository = productRepository; } @Override public Result<CartVo> getCartView(Long userId) { Cart cart = cartRepository.findById(userId); CartVo cartVo = cart.view(); return new Result<CartVo>().success(cartVo); } @Override public Result<CartVo> addToCart(Long userId, Long productId, Long variantId, Integer quantity) { Cart cart = cartRepository.findById(userId); Product product = productRepository.findById(productId); if (!product.getSKU(variantId).is_available(quantity)){ return new Result<CartVo>().error("stock not enough"); } cart.add(variantId, quantity, product); CartVo cartVo = cart.view(); return new Result<CartVo>().success(cartVo); } @Override public Result<CartVo> updateCart(Long userId, String cartItemId, Integer quantity, Long productId, Long variantId) { Cart cart = cartRepository.findById(userId); if (variantId != null){ Product product = productRepository.findById(productId); if (!product.getSKU(variantId).is_available(1)){ return new Result<CartVo>().error("stock not enough"); } cart.delete(cartItemId); cart.add(variantId, 1, product); } if (quantity != null){ cart.updateQuantity(cartItemId, quantity); } CartVo cartVo = cart.view(); return new Result<CartVo>().success(cartVo); } @Override public Result<CartVo> deleteCart(Long userId, String cartItemId) { Cart cart = cartRepository.findById(userId); cart.delete(cartItemId); CartVo cartVo = cart.view(); return new Result<CartVo>().success(cartVo); } @Override public Result<CartVo> checkCartItem(Long userId, String cartItemId, Boolean checked) { Cart cart = cartRepository.findById(userId); cart.check(cartItemId, checked); CartVo cartVo = cart.view(); return new Result<CartVo>().success(cartVo); } @Override public Result<OrderPreviewVo> genOrder(Long userId, BigDecimal clientAmount, String sign) { return null; } }

可以看到 这样服务层根本就看不到任何的和数据库相关的逻辑,这些被封装进入各个模型中。

实际上,这个 service 的实现综合使用了多个模型的行为,不仅是 Cart 还有 Product 和 SKU 和 Inventory . 如果使用 充血模型的 OOP ,那么 service 层就应该是和一组具有行为的模型组合使用

为甚么充血模型更好

在开头的区别中,其实已经表述了。这里用我的理解

以我较为浅显的理解来说,不管是用贫血或者充血,实际上都做了一层映射,把数据库中的数据搬到模型中去。但是,贫血模型的作用就仅限于此了,存一下数据,完全丢失了OOP的优势,下面用AI总结复习下OOP的优势,大概是这几点:

✅ 1. 行为 + 状态聚合,保持业务不变性

OOP 把“状态”和“操作状态的方法”聚在一起,避免状态被胡乱篡改。

🔒 OOP 能封住内部状态,让所有变更走业务逻辑路径,避免非法状态。


✅ 2. 高内聚,低耦合

对象内部高度内聚,外部通过接口与之交互,减少模块间耦合。

减少上下游影响,提高模块独立性。


✅ 3. 更强的表达力和可读性(Ubiquitous Language)

真正的 OOP 类和方法贴近业务术语,代码即文档。

order.cancelByCustomer(reason); payment.retry();

相比那种冗长、低语义的 if/else 或 service 方法调用,更清晰、自然。


✅ 4. 更易测试和演进

行为封装在对象中,可直接单测对象逻辑,不必依赖复杂上下文。

模块化程度高时,未来调整逻辑/策略也更容易。


✅ 5. 符合人类思维建模方式

我们理解“订单”、“购物车”、“客户”这些概念都是具备状态和行为的实体,用对象建模贴近现实,更易于团队沟通和协作。


✅ 6. 支撑复杂业务建模(DDD)

OOP 是 DDD(领域驱动设计)的基础。没有真正的 OOP,谈不上战斗力强的领域模型。聚合根、实体、值对象、策略模式等,都是 OOP 的体现。

让我感到不适应的几个点:

再转变到使用充血模型的过程中,在几个点上我感到不适应。或者说感觉有点问题的地方,不知道有没有人有同感

第一,模型建立的时候

在service中,当我需要获取到模型实例的时候,我需要通过 Repository 去专门写一个创建模型,获取模型的方法,

有时候我会的模型类嵌套的过深,比如我会在 Product 中嵌套多个 Variant, 每个 Variant 中还有一个 Inventory 。这三个内容我写在三张表中,此时其实创建模型实例的方法就有一点点麻烦了。

我不知道这是不可避免的,还是说我的方法不对。以前我都是需要哪个,就直接在 service 里用 mapper 查对应的数据。

有可能是我抽象的太过了,组合 Product 的方式有问题,因为,有的时候,一些 service 层的方法是不需要用到那么多的数据的,那我就将一个产品相关的所有查询都打到数据库了,正是因为这一点,我想到了使用缓存去保存模型的数据。但是问题依然存在。

第二,模型持久化的时候

模型的持久化,真正的写入数据库的操作肯定还是在 Repository 中的。但是不是要从模型中发起呢?那么什么时候去进行持久化的写入数据库?持久化的时候,是不是又需要把数据从模型中解构出来,如果嵌套组合不合理,这一步也很麻烦。

对于这两个问题,我喂给AI, 他的回答我觉得还是有说法的,贴在下面:

✳️ 问题一:模型构建复杂(Product → Variant → Inventory)

✅ 你当前的困惑:

模型之间嵌套,Repository 获取领域对象成本变高;不像以前只查用到的字段那么轻便。

🎯 问题本质:

  1. 充血模型强调建模完整的领域行为语义,所以你要构造出一个“完整的聚合”,这确实更重了。
  2. 有时候,你不是真的需要完整聚合,而只是想查一小块数据,却为了行为封装“被迫”加载一堆子对象,这种做法成本高。

🔍 解决策略:

✅ 1. 明确:是否真的需要构建聚合根?

不是所有情况都要构建完整模型,按 DDD 的经验:

业务操作类型是否构建聚合对象
状态变更(如加购物车、下单)✅ 是
只读(如查询商品详情)❌ 否,用 DTO 即可

📌 换句话说,只在需要行为的时候才构建模型,其余直接查 DTO。构建模型是为行为服务的,不是强制标准。

✅ 2. 构造方法中使用按需加载(Lazy or Factory)

你可以为 Product 提供多种构造方式:

java 复制编辑 // 用于完整聚合建模(有行为需求) Product withVariantsAndInventory = productRepository.loadFull(productId); // 用于轻量只读用途 ProductSummary summary = productRepository.loadSummary(productId);

用“意图驱动的模型构造方法”去控制装载深度,避免无谓的数据装载。

✅ 3. 组合与聚合边界是否划分过深?

你也提到“是不是组合方式有问题”,这个观察是非常敏锐的。

如果你在大多数业务中只对 Variant 或 Inventory 操作,而不是通过 Product 来驱动行为,说明它们可能应该是独立聚合根,而不是 Product 的一部分。

📌 聚合边界的定义应该基于事务一致性与行为闭包性,不是“物理上属于谁”。


✳️ 问题二:模型的持久化时机不明确

✅ 你当前的困惑:

充血模型强调模型发起操作,那我在什么时候调用 Repository 保存到数据库?Service 层还是模型自己保存?

🎯 问题本质:

这是 OOP + DDD 中一个经典问题:谁负责持久化,领域对象 or 应用服务?

🔍 推荐实践(领域建模中的常见方式):

✅ 1. 持久化操作由 Service 驱动,模型本身不关心持久化

DDD 里通常建议:模型自己只关注业务规则、状态变更,不关心数据落地。Repository 把对象“还原”和“保存”到数据库,统一由 Application Service 控制生命周期。

// 业务动作在模型中 cart.add(variantId, quantity, product); // 数据持久化由 Service 明确调用 cartRepository.save(cart);

📌 这样做的好处是:

  • 保持模型纯粹(只关注业务)
  • 易于测试
  • 明确持久化时机

✅ 2. 用“变更记录”控制写入策略(Optional)

在某些系统中,模型中记录自身是否“dirty”,交给应用服务统一批量提交。

if (cart.isDirty()) { cartRepository.save(cart); }

甚至可以设计领域事件(如 CartChangedEvent)触发写入,这在 CQRS 或事件风格架构中常见。

当然,这两个问题都不是模型本身的问题,模型本身只要关注自身的业务逻辑和数据就好,AI提到的聚合根和聚合边界的问题,在设计之初是需要好好权衡的,如果组合在一起,就是不可避免的导致创建的时候较为复杂,所以要选择合适的程度去设计聚合边界。或者直接将读写的逻辑分离,避免一些不必要的操作打到数据库。