【微服务】基于DDD的商城系统实战(三)-- 分层架构

Posted by jintang on 2021-03-11

前言

上一篇我们聚焦商品子域,讲解了如何使用事件风暴做领域建模。接下来,我们聊一聊DDD的分层架构。

对于构架一个复杂的应用,设计一个适合应用需求的,同时具备”高内聚,低耦合”理念的分层架构,能够是的各层的边界清晰而职责分明。而DDD 的分层架构又是怎么样的呢?

从两层架构到DDD分层架构

软件架构发展大体可以分为三个阶段:单体两层架构 -> 集中式三层架构 -> 分布式微服务架构

软件架构的演进

第一阶段的单体两层架构,分层方式是表现层和数据库两层,常常采用的是面向过程的设计方法。

两层架构 – 旧日的大泥球时代

单体架构

早期的PHPer,在PHP4发布之前,还没使用面向对象模式,表现层、业务逻辑和数据库访问都是交织在一起的。我们来看看下面的例子1-1

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
<?php
$conn = mysql_connect('localhost', 'username', 'password');
if (!$conn) {
die('Could not connect: ' . mysql_error());
}
mysql_set_charset('utf8', $conn);
mysql_select_db('databaseName', $conn);
$result = mysql_query('SELECT id, name, img_url FROM product', $conn);
?>
<html>
<head></head>
<body>
<table>
<thead>
<tr>
<th>商品ID</th>
<th>商品名称</th>
<th>商品图片</th>
</tr>
</thead>
<tbody>
<?php while ($product = mysql_fetch_assoc($result)) : ?>
<tr>
<td><?php echo $product['id']; ?></td>
<td><?php echo $product['name']; ?></td>
<td>
<img src="<?php echo $product['img_url']; ?>" />
</td>
</tr>
<?php endwhile; ?>
</tbody>
</table>
</body>
</html>
<?php mysql_close($conn); ?>

稍微好一点的开源项目(比如大名鼎鼎的ECShop),也只是单纯地利用include引入一些公共的基础设施文件做模块化复用。这种两层的分层架构风格,就是大泥球风格,这样分层方式的应用的会造成维护和开发成本持续增长。

后来随着Web应用的发展,PHP也因为有类似Zend Framework、Yii、ThinkPHP等框架的兴起流行,大家渐渐熟悉了MVC(模型-视图-控制器)模式,开发效率显著提升,维护成本也随之下降。

再来看看下面这个例子1-2

1
2
3
4
5
6
7
class ProductModel extends \yii\db\ActiveRecord 
{
public static function tableName()
{
return 'product';
}
}
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
class ProductController extends \yii\web\Controller
{
public function actionSaveProduct()
{
// 业务接入
$id = \Yii::$app->request->post('id');
$title = \Yii::$app->request->post('title');

if (empty($title)) {
throw new Exception('标题不能为空');
}

\Yii::$app->response->format = Response::FORMAT_JSON;

$product = new ProductModel();

// 业务逻辑
if (strlen($title) > 10) {
$product->title = substr($title, 0, 10);
}

// 数据库访问
if ($product->save()) {
return $this->render('save-product-success');
}
return $this->render('save-product-fail');
}
}

虽然有了MVC模式,上面代码依然是两层架构,因为业务接入、业务逻辑处理和数据库访问让仍然交织在一个controller的action方法中,久而久之也会发展成一个大泥球controller。

集中式的三层架构

早期的MVC思想只能解决表现层与业务逻辑耦合的问题,仍然无法解决两层模式导致软件发展成大泥球。于是,渐渐有了集中式的三层架构模式

我们来看看下面例子2-1

1
2
3
4
5
6
7
class ProductModel extends \yii\db\ActiveRecord 
{
public static function tableName()
{
return 'product';
}
}
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
class ProductRepository
{
public function add($title, $imgUrl)
{
$product = new ProductModel();
$product->title = $title;
$product->img_url = $imgUrl;
return $product->save();
}

public function update($id, $title, $imgUrl)
{
$product = $this->getById($id);
$product->title = $title;
$product->img_url = $imgUrl;
return $product->update();
}

public function getById($id)
{
return ProductModel::findOne($id);
}

// ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ProductService
{
/**
* @ProductRepository $productRepository
*/
protected $productRepository;

public function __construct(ProductRepository $productRepository)
{
$this->productRepository = $productRepository;
}

public function saveProduct($id, $title, $imgUrl)
{
if (strlen($title)) {
$title = substr($title, 0, 10);
}
$product = $this->productRepository->getById($id);
if ($product == null) {
return $this->productRepository->add($title, $imgUrl);
}
return $this->productRepository->update($id, $title, $imgUrl);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ProductController extends \yii\web\Controller
{
public function actionSaveProduct()
{
$id = \Yii::$app->request->post('id');
$title = \Yii::$app->request->post('title');
$imgUrl = \Yii::$app->request->post('img_url');

$productService = new ProductService();
if ($productService->saveProduct($id, $title, $imgUrl)) {
return $this->render('ok');
}
return $this->render('false');
}

}

集中式的三层架构,分别为:业务接入层、业务逻辑层和数据库访问层。

业务接入层用于处理异常、逻辑跳转控制、页面渲染模型等,又被称为mvc层(Model View Controller)。
服务层 用于对应用业务逻辑处理;
数据访问层 用于定义数据访问接口,实现对真实数据库的访问服务;

单体架构

目前大多数流行的PHP框架都采用这种集中式的三层架构进行开发,也就是controller/service/repository

csr

三者的调用关系为controller调用service完成业务逻辑处理,service调用repository完成数据访问,是不是似曾相识,我想你一定很熟悉了。

但是这种三层架构弊端在于,随着业务发展,Service层会越来越臃肿且业务耦合严重,在仓储模式下,Repository层会实现大量的各业务耦合一起的数据库访问的方法,拆分难,扩展性差,同时会导致实体的失血模型

因为一旦更换数据库,就可能需要重写大部分的代码,这对应用来说是致命的

DDD 的分层架构

在单机和集中式架构这两种模式下,软件无法快速响应需求和业务的迅速变化,最终错失发展良机,这时候分布式微服务架构登场。

分布式微服务架构也有很多种,包括命令查询职责分离(CQRS)、六边形架构、洋葱架构、整洁架构以及DDD 分层架构等,各种架构的理念都是为了设计出”高内聚、低耦合”的架构。

DDD的分层架构包括:用户接口层、应用层、领域层和基础层

csr

每一层对应的职责:

1.用户接口层

用户接口层负责向用户显示信息和解释用户指令。这里的用户可能是:用户、程序、自动化测试和批处理脚本等等,有点类似Controller的作用。

2.应用层

应用层是很薄的一层,理论上不应该有业务规则或逻辑,主要面向用例和流程相关的操作。在领域层智商,协调聚合服务组合和编排。

3.领域层

实现领域的核心业务逻辑。这一层聚集了领域模型的聚合、聚合根、实体、值对象、领域服务和事件等领域对象,以及它们组合所形成的业务能力。

4.基础层

贯穿所有层,为各层提供基础资源服务。这一层聚集了各种底层资源相关的服务和能力,包括第三方工具、驱动、消息中间件、网关、文件、缓存以及数据库等。基础层包含基础服务,它采用依赖倒置设计,封装基础资源服务,实现应用层、领域层与基础层的解耦,降低外部资源变化对应用的影响。

DDD 分层架构有一个重要的原则:每层只能与位于其下方的层发生耦合。

相比较三层架构模式,DDD 分层架构对三层架构的业务逻辑层进行了更清晰的划分,改善了三层架构核心业务逻辑混乱,代码改动相互影响大的情况。

基础层的依赖倒置实现

传统的软件分层思想中,上层代码依赖于下层代码,当下层出现变动时,上层代码也要相应变化,维护成本较高。而依赖倒置的核心思想是上层定义接口,下层实现这个接口,从而使得下层依赖于上层,降低耦合度,提高整个系统的弹性。这是一种经实践证明的有效策略。

依赖倒置的原则

高层次模型不应该依赖于低层次模型。它们都应该依赖于抽象。
抽象不应该依赖于细节,细节应该依赖于抽象。
– Robert C.Martin

继续例子2-1的示例代码,我们把ProductRepository改造成接口,按照”依赖于抽象”的原则,接口本身就是一种抽象。

1
2
3
4
5
6
interface ProductRepository 
{
public function getById($id);
public function add($title, $imgUrl);
public function udpate($id, $title, $imgUrl);
}

这个ProductRepository接口,暴露了有关商品的一些方法。现在来为它添加适配器,用于实现商品的一下具体操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class PDOProductRepository implements ProductRepository
{
public function add($title, $imgUrl)
{
$product = new ProductModel();
$product->title = $title;
$product->img_url = $imgUrl;
return $product->save();
}

public function update($id, $title, $imgUrl)
{
$product = $this->getById($id);
$product->title = $title;
$product->img_url = $imgUrl;
return $product->update();
}

public function getById($id)
{
return ProductModel::findOne($id);
}
}

只要我们定义了接口和实现,对于ProductService,通过依赖注入(Dependency Injection)来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
class ProductService
{
/**
* @ProductRepository $productRepository
*/
protected $productRepository;

public function __construct(ProductRepository $productRepository)
{
$this->productRepository = $productRepository;
}
// ...
}

从上面代码可以看出,如果当商品有必要更换存储方式,或者其他场景的仓储,只需要在初始化ProductService时(一般地在controller的action中)构造函数传入特定的ProductRepository接口实现,即可更换切换。

可见,通过使用依赖倒置原则,基础层 现在用户接口层,应用层和领域层这些高层次。于是依赖被倒置了,这样做可以降低类之间的耦合性,提高系统的稳定性,降低修改程序造成的风险。

总结一下

本篇介绍了从两层架构到DDD 分层架构的软件分层架构演进,利用代码体验了不同分层架构的优点,最后利用PHP代码实现了DDD分层架构的基础层依赖导致的原理。下一期将进入DDD的战术设计,看看DDD指导我们的分层架构落地后的代码模型。

下期预告:【微服务】基于DDD的商城系统实战(四)– 微服务代码模型

本篇是我对分层架构的理解,欢迎探讨指正。

思考题

结合你实际的业务场景,如果让你重构某一个业务模块,对照DDD的分层架构,哪些代码属于应用层,那些代码应该设计在领域层?