前言 上篇看了软件架构的演进,从两层架构到DDD的分层架构,并且使用结合代码理解了不同分层架构的思想。接下来我们看看DDD 的分层架构是如何知道我们落地微服务代码模型。
基于DDD分层架构的微服务代码模型 回顾一下DDD 的分层架构,包括:用户接口层、应用层、领域层和基础层
DDD 分层架构对应的微服务代码模型,其实DDD没有给出明确的定义 ,也就是说每个实践者都可能会有不一样。那么我们还是吸收前人的经验来确定我们的代码模型,具体如下:
整合Yii2框架 实现以上代码结构,我们基于熟悉的yii框架来搭建微服务代码模型。
一级目录的定义
为了方便devops部署和日志输出,我把一级目录结构划分为:devops,runtime,src
devops 用于存放运维相关的文件,包括云擎配置,用于部署类的文件。runtime 运行时的文件,包括日志,cache和debugsrc 存放代码目录,作为微服务代码结构的根目录
通过composer引入yii2
首先是引入yii2框架,放在哪里合适呢?基础层 可以存放第三方包,所以我们把vendor放到infrastructure目录下
1 2 3 cd infrastructurecomposer init composer require yiisoft/yii2
入口文件改造
入口文件index.php我们放在src下的public目录,注意修改vendor的autoload.php和yii2/Yii.php文件的位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php define('SRC_PATH' , dirname(__DIR__ )); define('ROOT_PATH' , dirname(SRC_PATH)); defined('YII_DEBUG' ) or define('YII_DEBUG' , true ); defined('YII_ENV' ) or define('YII_ENV' , 'dev' ); require __DIR__ . '/../infrastructure/vendor/autoload.php' ;require __DIR__ . '/../infrastructure/vendor/yiisoft/yii2/Yii.php' ;$config = require __DIR__ . '/../infrastructure/config/web.php' ;(new yii\web\Application($config ))->run();
此处定义了两个常量ROOT_PATH 和SRC_ROOT ,分别是站点的根目录和代码目录,方便后序配置
提醒一下:有了入口文件,nginx配置指定root的时候自然需要改到src/public下,nginx具体怎么配置此处就不多说了。
配置文件
根据基础层 的职责,配置文件同样是放在基础层,我们确定放在src/infrastructure/config下
对web.php配置文件进行改造,改造的目的主要由以下三个:
根据一级目录的定义,runtime目录需要改到相应的位置。
根据用户接口层 的职责,我们对外的接口放在用户接口层 interfaces 下,通过facade来提供较粗粒度的调用接口,将用户请求委派给一个或多个应用服务进行处理。也就是说,我们要把controller放在interfaces/facade下
我需要有一个显性的路由定义,只有定义了路由才能访问,有利于对接口的把握和提高灵活性。
于是,对于web.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 44 45 46 47 48 49 50 51 52 53 54 <?php $params = require __DIR__ . '/params.php' ;$db = require __DIR__ . '/db.php' ;$config = [ 'id' => 'basic' , 'basePath' => dirname(dirname(__DIR__ )), 'bootstrap' => ['log' ], 'aliases' => [ '@bower' => '@vendor/bower-asset' , '@npm' => '@vendor/npm-asset' , '@runtime' => ROOT_PATH . '/runtime' , '@interfaces' => SRC_PATH . '/interfaces' , ], 'controllerNamespace' => '@interfaces\facade\controllers' , 'controllerMap' => [ 'product' => 'interfaces\facade\ProductApi' ], 'components' => [ 'request' => [ 'cookieValidationKey' => 'e2ndO0Mv5t_-UUfou2od0uBRJHCfe4Qy' , ], 'db' => $db , 'log' => [ 'traceLevel' => YII_DEBUG ? 3 : 0 , 'targets' => [ [ 'class' => 'yii\log\FileTarget' , 'levels' => ['error' , 'warning' ], 'logFile' => ROOT_PATH . '/runtime/logs/app.log' ], ], ], 'urlManager' => [ 'enablePrettyUrl' => true , 'showScriptName' => false , 'rules' => [ ], ], ], 'params' => $params , ]; return $config ;
再看看ProductApi.php文件
1 2 3 4 5 6 7 8 9 10 11 <?php namespace interfaces \facade ;class ProductApi extends \yii \web \Controller { public function actionIndex ( ) { } }
脚本程序
除了接口外,还有脚本程序也是放到基础层 中,具体目录:src/interfaces/commands,并且把yii指令文件:yii和yii.bat放到commands下。
到此,我们完成yii2框架的整合,整体看一下代码结构:
领域对象的代码设计示例 上面我们根据DDD 的分层架构定义出具体的代码模型和目录结构,下面结合我对DDD思想的理解,给出一些领域对象的代码示例
值对象 值对象的特征包括:
度量或者描述了领域中的一件东西
可作为不变量
将不同的相关的属性组合成一个概念整体(Conceptual Whole)
当度量和描述改变时,可以用另一个值对象予以替换
可以和其他值对象进行相等性比较
不会对协作对象造成副作用
通过特征来辨别实体与对象, 值对象可以是数值,文本字符,日期,时间,一个人的全名,货币,颜色,电话号码,邮箱地址,收货地址
下面是收货地址示例
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 <?php namespace domain \product \entity \valueobejct ;class Address { protected $province ; protected $city ; protected $area ; protected $detail ; public function __construct ($province , $city , $area , $detail ) { $this ->province = $province ; $this ->city = $city ; $this ->area = $area ; $this ->detail = $detail ; } public function getProvince ( ) { return $this ->province; } public function setProvince ($province ) { $this ->province = $province ; } }
实体 实体对象应该不用多说了,有一个重要的特征就是具备唯一的ID,我们很容易想到商品这一个实体,因为每一个商品都应该有一个唯一的商品ID,这样一来有利于实体的持久化。
假设我们有这样一个表:
1 2 3 4 5 6 7 8 9 10 11 create table if not exists `product` ( `id` char (36 ) not null primary key comment '商品唯一ID' , `name` varchar (1000 ) not null default '' comment '商品名称' , `cate_id` char (36 ) not null default '' comment '商品分类ID' , `status` tinyint unsigned not null default 0 comment '商品状态:0下架,1上架' , `created_on` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间' , `created_by` char (36 ) DEFAULT NULL COMMENT '记录创建者' , `modified_on` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录修改时间' , `modified_by` char (36 ) DEFAULT NULL COMMENT '记录修改者' , `is_deleted` tinyint(1 ) DEFAULT '0' COMMENT '是否已删除' , );
通常我们设计完表结构后,我们的思维比较容易陷入数据建模的陷阱,而DDD 对于实体的设计是建议采用充血模型 。
充血模型 vs 贫血模型 我通过代码描述一下过往我们”贫血模型”的方式:
首先当我们设计完数据表后,接下来是为这个创建一个实体类:
商品实体类
1 2 3 4 5 6 7 8 9 <?php namespace app \models ;class Product extends \yii \db \ActiveRecord { public static function tableName ( ) { return "product" ; } }
假设我们需要修改商品的上下架状态,我们会这样:
1 2 3 4 5 6 7 <?php $product = $productRepository ->findOne($productId );if (!is_null($product )) { $product ->status = 1 ; $product ->save(); }
为了解决代码重用问题,按照三层架构模式,自然会设计一个服务层 来封装这段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php class ChangeProductStatusService { protected $productRepository ; public function __construct (ProductRepository $productRepository ) { $this ->productRepository = $productRepository ; } public function putOnSale ($productId ) { $product = $this ->productRepository->findOne($productId ); if (!is_null($product )) { $product ->status = 1 ; return $product ->save(); } return false ; } }
再来就是仓储层
1 2 3 4 5 6 7 8 9 use app \models \Product ;class ProductRepository { public function findOne ($productId ) { return Product::findOne($productId ); } }
这种三层架构的逻辑虽然做到了代码重用,但是有一个很严重的弊端:如果controller直接调用ProductRepository的代码,实现putOnSale通用的逻辑,那么ChangeProductStatusService的putOnSale方法根本不会被执行,甚至忽略。
充血模型 的思想是领域模型中的实体的数据应该与实体对应的行为绑定 ,这样我们得到一个更加丰富的实体类,当我们需要实体完成一些动作时,实体自身就带有相应的方法。
所以上面Product实体类,应该带有putOnSale的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 namespace app \models ;class Product extends \yii \db \ActiveRecord { public static function tableName ( ) { return "product" ; } public function putOnSale ( ) { } }
三种服务 虽然说实体和值对象,采用充血模型来设计,能够包含大部分应用程序的业务逻辑,但是你会发现,在领域里有一些重要过程或者转换不是实体或者值对象的职责范围 ,根据DDD的思想,我们应该把这些操作定义成服务。
这样一来,你将碰到一下三类服务:
应用服务 :位于应用层,用来表述应用和用户行为,负责服务的组合、编排和转发,负责处理业务用例的执行顺序以及结果拼装,对外提供粗粒度的服务
领域服务 :位于领域层,领域服务封装核心的业务逻辑,实现需要多个实体协作的核心领域逻辑
基础服务 :位于基础层,提供基础资源服务(比如数据库、缓存等),实现各层的解耦,降低外部资源变化对业务应用逻辑的影响,比如仓储服务
应用服务 根据分层架构的调用关系,应用服务应该会被用户接口层调用,比如下面代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php namespace interfaces \facade ;class ProductApi extends \yii \web \Controller { public function actionPutOnSale ( ) { $product = ProductAssembler.toDO(ProductDTO); $productService = new ChangeProductStatusService(new ProductService()); $productService ->putOnSale($product ); } }
与此同时,应用服务的内部可以调用领域层的领域服务,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php namespace application \service ;use domain \product \entity \Product ;use domain \product \service \ProductService ;class ChangeProductStatusService { protected $productService ; public function __construct (ProductService $productService ) { $this ->productService = $productService ; } public function putOnSale (Product $product ) { if ($this ->productService->putOnSale($product )) { } } }
领域服务 对多个实体或方法的业务逻辑进行组合或编排,或者在严格分层架构中对实体方法进行封装,以领域服务的方式供应用层调用
看看上面一个例子中的ProductService代码:
1 2 3 4 5 6 7 8 9 10 11 12 <?php namespace domain \product \service ;class ProductService { public function putOnSale (Product $product ) { return $product ->putOnSale(); } }
基础服务 以上例子中,基础服务主要是仓储服务,是通过依赖倒置的方式提供资源服务的,上篇文章已经展示过依赖倒置的实现,此处不再叙述。
总结一下 本篇开头回顾了一下DDD的分层架构,同时直接引入了与DDD的分层架构对应的代码模型,并且对各级代码目录的职责做了明确定义。考虑到目前团队对yii2框架的熟悉程度,我整合了yii2框架到代码模型中,最后通过包括值对象、实体、应用层服务、领域层服务等实例代码,展示了充血模型的意义,各层服务之间的调用关系。
以上是我学习ddd设计思想的总结,代码示例没有在完整的场景中实现,所以业务逻辑可能会不完整,但是不影响理解。希望对你有帮助,同时有理解不正确的,欢迎讨论。
下期预告:”【微服务】基于DDD的商城系统实战(五) – 实现商品上下文”
思考题 你能根据自己的理解,设计出你自己的微服务代码模型,并且与你最熟悉的框架做整合吗?