【微服务】基于DDD的商城系统实战(五) -- 实现商品上下文

Posted by jintang on 2021-03-19

作者:刘浩

前言

大家好,我是刘浩。上一篇我们基于DDD的分层架构,直接引入了与DDD的分层架构对应的代码模型,并且对各级代码目录的职责做了明确定义。到此为止,本系列文章已经从领域划分,领域建模,分层架构到代码模型,一路走来,我们已经到达可落地实现阶段,那么接下来看看商品上下文怎么实现。

回顾

我们要实现的商品发布场景如下图:

商品发布系统

我们通过事件风暴构建商品子域的领域模型如下图:

领域对象

我们在商品这个子域里面,我们定义了两个限界上下文:商品和库存,其中包含三个聚合:商品发布、审批和库存,对应的代码结构如下:

代码目录

三个聚合中发现以下实体:商品、分类、库存单、采购单和审批记录,下面看看如何实现。

商品发布阶段实现

商品发布

上图的商品发布阶段,设计到两个实体:商品和采购单,商品的发布首先需要准备好商品信息,包括商品名称,分类,商品描述和商品图片,调用创建商品接口,完成商品的发布。完成发布后得到sku(本篇不讲述SKU生成逻辑,由于比较复杂,不在主题讨论范围,只用sku定义一个库存单位),调用创建采购单接口,完成采购单的填写。

SPU与SKU概念

spu (Standard Product Unit 标准化产品单元)

SPU = Standard Product Unit (标准化产品单元),SPU是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。通俗点讲,属性值、特性相同的商品就可以称为一个SPU。

sku (stock keeping unit 库存量单位)

SKU即库存进出计量的单位, 可以是以件、盒、托盘等为单位。SKU是物理上不可分割的最小存货单元。在使用时要根据不同业态,不同管理模式来处理。)

商品发布接口实现

创建商品接口: interfaces/facade/GoodsApi.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?php
namespace interfaces\facade;

use app\service\GoodsService;
use interfaces\assembler\GoodsAssembler;
use interfaces\request\GoodsRequest;
use yii\base\Exception;
use yii\web\Response;

class GoodsApi extends \yii\web\Controller
{
/**
* @var GoodsService
*/
public $goodsService;
/**
* ProductApi constructor.
* @param $id
* @param $module
* @param array $config
* @throws \yii\base\InvalidConfigException
*/
public function __construct($id, $module, $config = [])
{
$this->goodsService = \Yii::$app->get('GoodsService');
parent::__construct($id, $module, $config);
}

public function actionCreateGoods()
{
\Yii::$app->response->format = Response::FORMAT_JSON;
try {
$request = new GoodsRequest();
$goodsDTO = $request->getGoodsDTO();
$goodsDO = GoodsAssembler::toDO($goodsDTO);
$goods = $this->goodsService->createGoods($goodsDO);
} catch (Exception $e) {
return Reponse::json($e->getCode(), $e->getMessage());
}
return Response::json(200, 'ok', ['sku' => $goods->getSku()]);
}
}

创建商品接口,通过自定义请求类:GoodsRequest,完成参数校验后,得到GoodsDTO(商品传输对象),然后交给GoodsService(商品应用服务类)的createGoods方法创建应用,其中createGoods方法接收的是DO对象(也就是Goods实体对象),需要使用GoodsAssembler::toDO把DTO做转换成DO

商品应用服务实现

商品应用服务类:application/service/GoodsService.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

namespace app\service;

use domain\goods\entity\Goods;
use domain\goods\event\GoodsEvent;
use domain\goods\service\GoodsDomainService;
use infrastructure\component\EventPublisher;

class GoodsService
{
/**
* @var GoodsDomainService
*/
protected $goodsDomainService;
/**
* @var EventPublisher
*/
protected $eventPublisher;

public function __construct(GoodsDomainService $goodsDomainService, EventPublisher $eventPublisher)
{
$this->goodsDomainService = $goodsDomainService;
$this->eventPublisher = $eventPublisher;
}

/**
* @param Goods $goods
* @return mixed
* @throws \yii\base\Exception
*/
public function createGoods(Goods $goods)
{
if ($this->goodsDomainService->create($goods)) {
// 发送领域事件:商品发布事件
$this->eventPublisher->send(GoodsEvent::generate(GoodsEvent::EVENT_CREATE, $goods));
return true;
}
}
}

商品应用服务的createdGoods方法中,调用了商品领域服务中的create()犯法,创建商品后创建商品发布事件,并且通过事件发布者发布。

商品领域服务实现

商品领域服务:domain/goods/service/GoodsDomainService.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
namespace domain\goods\service;

use domain\goods\entity\Goods;
use domain\goods\GoodsFactory;
use domain\goods\repository\facade\GoodsRepositoryInterface;
use yii\base\Exception;

class GoodsDomainService
{
/**
* @var GoodsRepositoryInterface $goodsRepository
*/
protected $goodsRepository;
/**
* @var GoodsFactory $goodsFactory
*/
protected $goodsFactory;

public function __construct(
GoodsFactory $factory,
GoodsRepositoryInterface $goodsRepository
) {
$this->goodsFactory = $factory;
$this->goodsRepository = $goodsRepository;
}
/**
* 创建商品
*
* @param Goods $goods
* @return mixed
* @throws Exception
*/
public function create(Goods $goods)
{
$goodsPO = $this->goodsRepository->findGoodsById($goods->getGoodsId());
if (!empty($goodsPO)) {
throw new Exception("没找到对应的商品");
}
$goods->create();
$goodsPO = $this->goodsFactory->createGoodsPO($goods);
return $this->goodsRepository->createGoods($goodsPO);
}
}

商品领域服务: GoodsDomainService的create()方法,接受Goods的实体对象,通过实体自身的$goods->create()方法,完成创建商品的额外初始化动作后,使用goodsFactory工厂类,将Goods的DO对象转换成PO对象(数据持久化对象),交给商品仓储接口。商品仓储接口的createGoods()方法接收的是PO对象。

商品实体

商品实体类:domain/goods/entity/Goods.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
namespace domain\goods\entity;
use yii\helpers\StringHelper;
/**
* Class Goods
* @package domain\goods\entity
*/
class Goods
{
const STATUS_OFF = 0;
const STATUS_ON = 1;

private $goodsId;
private $categoryId;
private $title;
private $description;
private $skuObj;
private $status;

/**
* 创建商品
* @return $this
*/
public function create()
{
$this->id = StringHelper::uuid();
$this->skuObj = new SkuObject(); // 此处生成SKU
$this->status = self::STATUS_OFF;
return $this;
}
}

实体中的create()方法,用于处理创建商品时的业务逻辑,充血模型的思想为领域模型中的实体的数据应该与实体对应的行为绑定

商品仓储的实现

商品仓储接口:domain/goods/repository/facade/GoodsRepositoryInterface.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
namespace domain\goods\repository\facade;
use domain\goods\repository\po\GoodsPO;

interface GoodsRepositoryInterface
{
/**
* 创建商品
*
* @param GoodsPO $goodsPO
* @return mixed
*/
public function createGoods(GoodsPO $goodsPO);
/**
* 修改商品
*
* @param GoodsPO $goodsPO
* @return mixed
*/
public function updateGoods(GoodsPO $goodsPO);
/**
* 查询商品
*
* @param $goodId
* @return GoodsPO
*/
public function findGoodsById($goodId) : GoodsPO;
}

商品仓储的实现:domain/goods/repository/impl/GoodsRepositoryImpl.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
namespace domain\goods\repository\impl;

use domain\goods\repository\facade\GoodsRepositoryInterface;
use domain\goods\repository\model\GoodsModel;
use domain\goods\repository\po\GoodsPO;

class GoodsRepositoryImpl implements GoodsRepositoryInterface
{
/**
* 创建商品
* @param GoodsPO $goodsPO
* @return mixed
*/
public function createGoods(GoodsPO $goodsPO)
{
$goodsDAO = new GoodsDAO();
$goodsDAO->setAttributes($goodsPO->toArray());
return $goodsDAO->save();
}
}

在商品仓储的实现中,createGoods()方法接收的是PO对象,通过PO对象把数据传递给GoodsDAO,实现商品的持久化。Yii2的DAO对应的就是\yii\db\ActiveRecord。

总结一下

本篇使用代码实现商品发布接口,体验了DDD 分层架构下微服务的服务和数据的协作关系。为了实现聚合之间以及微服务各层之间的解耦,我们在每层定义了不同职责的服务和数据对象。

微服务内的数据对象:

数据持久化对象 PO(Persistent Object),与数据库结构一一映射,是数据持久化过程中的数据载体。
领域对象 DO(Domain Object),微服务运行时的实体,是核心业务的载体。
goods DTO(Data Transfer Object),用于前端与应用层或者微服务之间的数据组装和传输,是应用之间数据传输的载体

微服务各层的数据对象的职责和转换过程:
对象转换

以上属于个人对DDD领域在商品上下文的理解,如有不同的意见,欢迎提出来,大家一起来讨论