Ddd实践总结

实践总结,总会有着反面的案例或者良好的设计案例作为背景的。这篇文章也会遵循这样的规律,会有场景案例说明,然后才是总结内容。

大背景

这次DDD的实践大背景是开发一个电商平台。这个电商平台与一般电商平台不同的是该电商平台以店铺作为租户向第三方商家提供店铺管理功能,但同时自身平台也有官网的店铺,无论是第三方店铺还是官方店铺,概念是一样的,两者没有任务特殊之处。为了开发这样的电商平台,在后台系统中自然划分了 平台级后台管理系统(针对全平台基础数据建设的管理系统),以及第三方店铺管理系统(为第三方店铺商家提供一站式商品/订单等的后台管理系统),这两套系统分别简称为PMS/TMS。由于自身公司的用户需要同时登录到PMS/TMS上操作,为了简化用户信息,PMS/TMS上采用相同的用户体系结构,即同一用户可在PMS上登录,也可以在TMS上登录,只是在不同平台上登录所具有的权限不同而已。

一开始,我们的技术架构是这样子的:

  • 客户端
    • Web : 浏览器的web页面
    • App : 手机app应用
  • 界面层
    • 微服务接口的门面:有两个作用,一是暴露内部微服务接口,二是承接前端请求的分发透传。
    • 前端页面模板:类似于jsp页面模板,生成html页面。
  • 端口层 : 用于使用不同协议来暴露应用服务
  • 应用服务层 :每个用例对应着一个应用服务接口
  • 领域层:领域内聚实现
  • 基础设施模块:基础设施实现

反面案例一

由于PMS/TMS上存在大量相似用例,比如说,在两个平台上都需要访问订单详情,也需要查询订单列表。一开始,我们设计了一个两个平台通用的查询接口:

// 查询订单详情:order/id?orderId=1

// 查询订单列表:order/page?pageIndex=1&pageSize=10&shopId=1

这些接口看起来很简洁,能满足我们的用例要求。

但这样存在着好几个问题:

  1. PMS平台是可以访问所有订单数据的,而TMS平台只能访问店铺下产生的订单数据,因此两者数据访问权限是不同的。这就引入一个问题——如何能够使用通用的接口却有着不同的数据访问权限呢?或者说如何防止跨数据访问问题呢?(如防止用户访问到其他用户的信息)
  2. PMS平台和TMS平台需要查看订单详情的数据可能是不一样的,如PMS平台可能需要展示订单与平台相关的信息,而TMS则不能展示这部分的信息,因此从用例的角度来看,PMS查询订单详情,TMS查询订单详情其实属于两个不同的用例,不同的用例使用相同接口将使接口变得混淆不清,或者使得接口实现内部需要对环境进行判断而返回不同的展现数据,这又将增加了接口的复杂度。这又引入另一个问题:接口的粒度要如何划分呢?
  3. 在TMS平台上,需要限制访问数据范围是在某个指定的shopId下的。如果依赖前端传递shopId,但又无法保证前端的请求不被篡改。在微服务接口中,如何保证传参进来的shopId是受信的呢?

以上三个问题可以总结为两个问题:微服务接口的粒度问题和用户权限问题。

用例粒度决定了微服务接口的粒度。 在PMS和TMS都需要查看订单详情,粗看之下,这里只有一个用例,但其实不然,用例本身包含了几个概念:用例主角、用例以及用例的边界。对查看订单详情用例来说,PMS和TMS的用例主角很明显不一样,PMS的用例主角是平台用户,而TMS的用例主角则是第三方用户;PMS用例的边界是PMS系统边界,而TMS的用例边界是TMS系统边界。除此之外,PMS和TMS从用例本身来说,也是不一样的,PMS用户和TMS用户需要访问的订单数据很可能是不一样的,虽然用例的名称是一样的。因此,查看订单详情这个接口不能简单地只提供一个订单查询接口,而是需要按用例边界划分成两个接口:PMS查看订单详情以及TMS查看订单详情。另外,值得一提的是,用例粒度可能会再细化,如查看订单详情是一个完整的用例,它可以再细化为查看订单基本信息和查看订单明细两个用例,是否需要再细化用例完全视实际页面需求而定了。如果页面是在一个页面中即时加载订单基本信息和订单明细,则不应该再细分了,但如果页面是分tag显示订单的,则需要将用例细分了。

权限问题可以细分为另两个权限问题:访问权限和数据权限。 访问权限可以理解为用户能不能访问某个接口,而数据权限则理解为能访问的数据范围。以上面查看订单详情为例,如果PMS和TMS复用同一个查看订单详情接口,那么PMS和TMS用户都应该具有该接口的访问权限,但数据权限如何控制呢?PMS用户能访问所有的订单,但TMS用户则只能访问其所在店铺的订单,两者在访问的订单数据范围上是不一致的,这样也会致使接口的入参是不确定或模糊不清的,对于在PMS上查订单,只需要提供订单ID作为接口入参即可,而对于TMS,则需要有订单ID和店铺ID作为接口入参了。有同学这时可能会反驳“店铺ID可以通过上下文传参的啊”,如果真要这样做(复用查看订单详情接口而且又要保证访问权限和数据权限,通过上下文来传递数据访问限制参数店铺ID),势必要在接口内部对用户进行分类处理,甚至要对返回结果分类处理(用例不同),另外接口的声明上也是含糊不清的,因为从接口声明上根本看不出店铺ID在起作用。由此得出结论, 在同一个接口中提供不同数据权限是不可取的。 这也再一次印证了 用例粒度决定了微服务接口的粒度。 我们再回过头看TMS查看订单详情接口,它需要两个接口入参:订单ID和店铺ID。店铺ID必须是不能被伪造的,如果被伪造将会跨越数据权限了,这是不允许的。如何保证店铺ID是可信的呢?这就只能够从会话上下文中获取店铺ID作为接口入参了,而至于如何从会话上下文中取出店铺ID并可传递到接口入参上,这在技术上就有多种可行的方式了,如通过http头部传递这些参数,在接口中使用@RequestHeader接收,又或者通过AOP的手段,在调用接口前传递上下文参数。

最后,我们结合CQRS思想,把分层架构设计成这样了: