搜索 | 会员  
基于DDD的商城系统实战(三)--分层架构
来源: CIO之家的朋友们   作者:CIO之家的朋友  日期:2023/3/20  类别:架构设计  主题:综合  编辑:Joanna
对于构架一个复杂的应用,设计一个适合应用需求的,同时具备”高内聚,低耦合”理念的分层架构,能够是的各层的边界清晰而职责分明。而DDD的分层架构又是怎么样的呢?

前言

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

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

从两层架构到DDD分层架构

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

image.png

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

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

image.png

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


<?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)。
服务层 用于对应用业务逻辑处理;
数据访问层 用于定义数据访问接口,实现对真实数据库的访问服务;

image.png

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

image.png

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

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

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

DDD 的分层架构

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

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

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

image.png

一层对应的职责:

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指导我们的分层架构落地后的代码模型。


德仔网尊重行业规范,每篇文章都注明有明确的作者和来源;德仔网的原创文章,请转载时务必注明文章作者和来源:德仔网;
头条那些事
大家在关注
广告那些事
我们的推荐
也许感兴趣的
干货