更多优质内容
请关注公众号

面向对象和设计模式(五) 贫血模型和充血模型-张柏沛IT博客

正文内容

面向对象和设计模式(五) 贫血模型和充血模型

栏目:其他内容 系列:面向对象与设计模式 发布时间:2023-01-06 00:27 浏览量:1172
本系列文章目录
展开/收起

在日常开发中,我们经常的做法是将代码分为 控制器层 Controller、服务层 Service 和 数据存储层 Repository。

其中控制器层负责暴露接口,服务层负责业务逻辑的实现,存储层负责数据的读写。

控制器层中的一个类代表一个独立模块的所有api接口,其下的每一个方法代表一个api接口;控制器层并不负责业务逻辑本身,而是负责调用业务逻辑方法,因此会简短明了。

服务层中的类又可以包含 Service类 和 Domain类(领域对象)两部分,Service类负责实现一些较为复杂 或者 多个业务对象耦合的业务逻辑,一个Domain类则对应一个具体的实体对象。

例如电商业务中订单、产品、报价单等对象就可以对应 Order、Product、Quotation 类这样的Domain对象。OrderService类会放订单相关的处理逻辑,如果某个需求涉及到订单和产品的相关联的逻辑,也可以放到OrderService类中。

数据存储层负责数据读写,一个Repository对象可以对应一个数据表,并提供增删改查等操作。


了解上面的内容后,就可以说说本节的主角 充血模型 和 贫血模型 了。


贫血模型是指,业务对象(即Domain对象)中只包含数据(即只包含属性和属性的getter、setter方法),但不包含业务逻辑(不包含业务逻辑相关的方法)。

例如:

<?php
class Student {
    private $age ;
    private $name ;

    public function getAge() {
        return $this->age;
    }

    public function setAge($age) {
      $this->age = $age;
    }

    public function getName() {
        return $this->name;
    }

    public function setName($name) {
      $this->name = $name;
    }
}


Student实体类中只包含了对象的属性以及属性的getter、setter方法,不包括该对象的具体行为和业务逻辑,业务逻辑以及调用Repository访问数据库的行为全都放在service层。


贫血模型的缺点

1. 不够面向对象,原因很简单,只有数据没有行为的对象不是真正的对象。

2. 业务逻辑没有经过任何拆分全部怼到Service类中,使得Service类臃肿厚重可读性差扩展性差,未经收归的业务逻辑缺少复用性。


贫血模型的优点

设计简单,开发方便,对于仅需SQL的CURL操作就能满足的简单需求而言再适合不过。


充血模型是指,Domain对象既包含对象的属性,也包含业务逻辑行为的方法,能够用Repository存取数据。而Service类则负责调用Domain对象的业务逻辑,并进行如事务控制、权限控制、幂等、记录日志、发送消息、调用其他系统的RPC接口等与业务无关的工作,以及跨领域模型的业务聚合功能(比如有个需求设计订单和产品两个业务对象的交互,这样的逻辑不适合放到Order或Product的Domain类中,只能放到Service类)。

如此一来,Domain和Service类都包含业务逻辑,但区别是Domain类中放的是和该业务对象相关性强且粒度较小不可分割的逻辑,Service类则负责调用Domain对象的逻辑以及一些非业务的与三方系统的交互工作。因此Service类的内容就会薄很多,逻辑更加清晰。

但这么一来,如何拆分业务逻辑到Domain和Service类就是关键,这需要视具体业务场景和需求而定。


下面我们举个充血模型的例子,假如有个需求,允许用户将阿里巴巴平台上的产品同步到自己公司的erp系统(你可以理解为是公司内部的后台系统)后,生成erp内的本地产品。

此时涉及到四个对象:平台产品SPU(对应的Domain类是PlatformSpu类)、平台产品SKU(对应PlatformSku类)、本地产品SPU(对应LocalSpu类)和本地产品SKU(对应LocalSku类)。

Service类是LocalProductService类。


那么我们对业务逻辑的拆分应该是这样:

PlatformSku类定义获取平台SKU信息的方法;

PlatformSpu类定义一个获取平台产品(既包含SPU,又包含SKU信息)的方法;

<?php

class PlatformSku{
  // 根据spu id获取这些spu对应sku的信息
  public function getSkuByPlatformSpuIds($platformSpuIds){
    // 调用三方平台的接口获取平台产品sku的信息
    return $skuInfos;
  }
}

class PlatformSpu{
  public function getSpuByPlatformIds($platformIds){
    // 调用三方平台的接口获取平台产品spu的信息
    return $spuInfos;
  }

  public function getPlatformPoductInfos($platformIds){
      $spuInfos = $this->getSpuByPlatformIds($platformIds);
      $platformSku = new PlatformSku();
  
      $spuIds = array_column($spuInfos, "spu_id");    // 收集$spuInfos列表中的平台spu id
      $skuInfos = $platformSku->getSkuByPlatformSpuIds($spuIds);
  
      // 组装spu和sku信息,形成["spu 1的id"=>[sku 1的信息, sku 2的信息, ...], ...]的格式
      $platformProductInfos = ...;
      return $platformProductInfos;
  }
}


LocalSku类定义保存本地SKU信息的方法;

LocalSpu定义保存本地产品的方法(既保存本地SPU,也保存本地SKU,这是个原子操作);

class LocalSku{
  public function batchSave($data){/* ...省略*/}
}

class LocalSpu{
  // $data包含spu和sku的信息
  public function batchSave($data){//...保存本地SPU,也保存本地SKU}
}


LocalProductService类的生成本地产品方法如下:

class LocalProductService{
  public function generateLocalProduct($platformSpuIds){
    $platformSpu = new PlatformSpu();
    $platformProductInfos = $platformSpu->getPlatformPoductInfos($platformSpuIds);

    // 伪代码:将平台产品 $platformProductInfos 格式化为本地产品信息
    $localProductInfos = format($platformProductInfos);
    $db = getDb();
    $db->startTransaction();    // 开启事务
    
    try{
      $localSpu = new LocalSpu();
      $localSpu->batchSave($localProductInfos);
      $db->commit();
    }catch(\Throwable $e){
      $db->rollback();    // 回滚
      // ... 错误处理
    }
  }
}

保存SPU和SPU下的SKU这两个行为是一个关联性极强的操作,因此需要将这个这两个行为共同放到 LocalSpu 这个Domain类下。

而 获取平台产品信息 和 保存本地产品信息是两个Domain对象的交互逻辑,无论放到哪个Domain类下都不合适,因此放到Service类中。


业界内更推荐使用充血模型,因为其更符合面向对象,并且可扩展性高,能应对复杂度较高的系统模块和需求。




更多内容请关注微信公众号
zbpblog微信公众号

如果您需要转载,可以点击下方按钮可以进行复制粘贴;本站博客文章为原创,请转载时注明以下信息

张柏沛IT技术博客 > 面向对象和设计模式(五) 贫血模型和充血模型

热门推荐
推荐新闻