前言
领域驱动模型设计在业界也喊了几年口号了,但是对于很多“务实”的程序员来说,纸上谈“术”远比敲代码难得太多太多。本人能力有限,在拜读相关作品时既要隐忍书中晦涩难懂的专业名词,又要去迎合西方大牛在撰写的过程中融入的西式故事。我想总会有一部分水平和我类似的码农们,需要一份对系统阐述DDD小白文化的文本。因此,本人便自不量力地结合一些简单的项目经验,将领域驱动模型设计思想从理解到落地的实施和总结分享给诸位。当然,如果是某些行业先锋不幸看到本人稚嫩的文字时,就当作是马戏团中的小丑,一笑了之翻页便可。
思维的入门
在学习架构思想的初期,特别时面对架构模型时(例如六边形架构、领域服务拆分),我总会不自然地在对号入座,思考在模型中的这一块放到代码实现上是Controller层还是Dao层,是采用消息中间件还是NoSQL缓存。这种自动联想的“被动技能”在学习微服务设计架构思想的过程中是致命的,过于专注业务的技术实现而脱离架构思想本身的大局观,就容易陷入用“具体”无法概括“抽象”的处境。
对此,让我们先忘了平日在开发中使用的各种被分类到细致入微的技术,带着一种阅读“无用”文学而非可以模仿实操的工具书的心态一起来对领域驱动设计做一个基础性的理解。
1 常用的服务拆分方法
1.1 根据业务能力进行服务拆分
创建微服务架构的策略之一就是采用业务能力进行服务拆分,这也是目前市面上大部分产品设计初期采用的方式。主要原因是这种方法对于架构师来说是比较容易实施的,就好像在做一个分类游戏,关于用户的注册登录以及信息管理可以划分为一个业务,关于文章的发布下架以及内容更迭可以划分为一个业务。
这样的分类手段核心是以业务活动进行划分,也保证了大部分面向对象的程序员在代码实现时可以更快地、更明确地创建实体类,从而开辟出一个个针对不同业务功能的微服务。
倘若我们以程序员盘踞的技术论坛/博客为例,那么它们必须要实现最基础的三大业务:1、用户的注册登录以及用户对个人信息的操作;2、用户可以发布/修改/删除自己的文章;3、网站的后台管理员可以对前面两者进行更高权限的操作。至于更多的用户评论、用户私聊、vip充值等一些功能则是拓展,毕竟没有它们也不会影响到整个网站的正常运作,因此暂不加入讨论。
所以根据上述最基础的三大业务,如果要进行微服务架构设计(当然现实中不会有为了如此简陋的网站采用微服务,不然会被从业的程序员背地挖苦痛骂),我们可以画出相应的映射图【图1-1】:
采用业务能力进行拆分固然方便了架构师和程序员,在应对一般的企业项目时,这种方式是非常稳妥的方案,架构中每个划分出的服务内部后期可以根随功能的需求增加而逐渐迭代(例如,我现在需要文章发布时可以附带图片,那么可以在实现文章业务的服务中添加相关的接口与具体实现代码),但是整体的项目架构是保持不变的。
话虽如此,但读者们要知道上面仅仅是为了实现技术博客网站最基本业务而定义的“初代”架构。一种情况是随着整个技术博客网站的日益庞大,为了迎合用户的需要,我们不得不扩展网站的功能性,例如增添用户对文章点赞/收藏/留言,后台对文章内容是否健康的审核等等,这时候初代被划分好的服务内部就会日益“庞大”,需要我们重新操刀对其进行分解切割。
另外一种情况是由于用户量的提升,致使服务之间的远程调用、进程间的通信次数骤增而导致请求响应效率逐步低下,例如拥有庞大数量的读者用户在访问文章时不仅要通过文章服务获取文章内容,还要通过文章的作者每次调用用户服务来获取作者个人信息简介(在没有缓存机制,每个服务根据业务主体分割明确的假设情况),我们又得考虑把一些服务组合在一起:
例如当我们现在常用的注册/登录手段——利用第三方服务进行短信验证,如果只是为了实现用户注册/登录功能,完全可以并入到用户的服务中【图1-2】:
一旦不单用户登录/注册情况用到短信验证,例如要充值时候也要用到短信验证支付,那么就得把它独立出来【图1-3】:
上面的问题正是以业务进行服务拆分时难以避免的,因为业务必然是不断发展的(参考市面上众多臃肿的软件),被繁杂的业务牵着鼻子走的架构模式必然会陷入到不停拆分或重组的境地,那么服务内部以及服务之间的关系也将逐步模糊紊乱。
1.2 根据子域进行服务拆分
即使这是一段很枯燥的历史,但为了感谢曾在这个领域躬耕的技术先驱们我还是要带上这段文字:Eric Evans在他的经典著作中(Addison-Wesley Professional)提出的领域驱动设计是构建复杂软件的方法论,这些软件通常都是以面向对象和领域模型为核心。
既然是方法论,它提供的是“怎么办”的理论体系。类比蛋炒韭菜,我所能提供的是做这道菜需要什么工具和佐料,放油放蛋放盐放韭菜的先后顺序和比例,至于真正做起来火候的大小,炒菜的姿势,蛋菜的克量等都是视情况而定,并没有固定的要求。这又回到我在“思维入门”中提到的,不要用机械的“具体”去反推“抽象”理论,因为炒菜的姿势最多能影响的是菜的口味,并非是完成这道菜的必要条件。
下面让我们来理解DDD中重要的两个概念:子域和界限上下文。我先以好理解的方式在各位脑海中勾勒出这两者的基本认识,然后各位再去看专业解释会好接受的多。
如果把一个项目业务比作一个国家整体,而子域相当于国家内部的各个省份,这些省份细致地划分了国家每个地域面积大小和居住人群,而所有省份的聚合又构成了整个国家。作为限界上下文,其英文为bounded context,可以直译成“有界的环境”,那么套入前文各位可能举一反三理解成“省界”。但事实并非如此,以“省份”类比子域的话,省界并非是限制人口流动的主要原因,限制人流的本质在于一个省份有一个省份的风土人情和文化语言,例如广东省用粤语交流,福建省用闽南语交流,这些语言在省份内部是大家达成共识都能理解的,但跑到广东说福建话只会让本地人摸不着头脑。
因此,每个省份(子域)之间产生的交流障碍归根于它们各自内部通用语言环境不同(限界上下文)。这个举例或许不符合社会学对于人口流动的分析,但拿来解释子域和限界上下文还是很贴切的。
那么让我们引入教科书对二者的解释来巩固诸位对它们的记忆:子域是领域的一部分,领域是DDD中用来描述应用程序问题域的一个术语。识别子域的方法和识别业务能力一样:分析业务并识别业务的不同专业领域,分析产生的子域定义结果也会和识别业务能力得到的结果非常相近。DDD把领域设计模型的边界成为界限上下文,当使用微服务架构时,每一个界限上下文对应一个或则一组服务,我们可以通过DDD方式定义子域,并把子域对应为每一个服务【图1-4】。
1.3 拆分单体应用的难点
1.网络延时
网络延时是分布式系统中一直存在的问题。不论是以业务进行拆分还是子域进行拆分,对服务不断细化分解会导致各个服务之间的大量往返调用。即使可以通过批量处理API在一次往返中获取多个对象,从而减少延时。但是在其他情况下,解决方案是把多个服务整合到一起,用变成语言中的函数调用替换昂贵的进程通信。
2.同步进程间通信导致可用性降低
举一个最简单的例子,当我们在某东下单一件商品时创建了订单,创建订单的过程中需要获取商品的详细信息和购买者的详细信息,而这其中有一个服务出现了不可用的状态就会导致整个业务创建失败,这种同步进程通信带来的可用性降低让我们不得不折中采用异步消息进行处理。
3.服务之间维持数据的一致性
当我们对服务进行拆分后,服务之间如何保持数据一致性成为重点和难点。还是以某东为例,在活动期间大量用户可能在同一刻参与了秒杀活动,而对于仓存服务与商品服务之间如何保证它们在数量的一致性(比如前台有100个用户下单,那么对于商品来说只需要在原有的数量上减去100然后返回给前端页面作为商品信息的一部分及时展示给用户还有多少存货便可,但是实际上真正需要扣减的应该发生在仓库服务中,因为仓库服务内存储的才是实际库存),怎么让一波狂欢后实际库存与前端保持一致是业务中必须攻克的难题。
4.上帝类阻碍了服务的拆分
分解的一部分障碍就是所谓的上帝类,即全局类或则是“公用”类。上帝类通常为应用程序不同方面实现业务逻辑。
以美团外卖业务举例,一个不经思考设计的订单类中会将以下所有信息属性直接构建成一个类:商家信息属性(商家名称、商家地址等)、用户信息属性(账户、昵称、地址等)、外卖商品属性(外卖商品名称、价格、配料等)、配送方属性(配送员身份、配送员电话号码、配送起始时间、配送截止时间等)。不过这些属性涉及了不同服务中的应用程序,导致了商品系统中必然存在的订单所包含的信息变得特别庞大,并且与其他服务之间因为共同的属性保持着“暧昧”的联系。
一种解决方法是在数据库中创立一个公用的订单数据库,处理订单的所有服务使用此数据库,但这就出现了“紧耦合”的情况。
对此应用DDD将每个服务视为“孤岛”般的子域(尽量不与其他服务发生在属性的上纠葛,形成一座具有特色便于识别的“孤岛”)。所以让我们看看自己手机上美团APP中客户订单,然后再看看拿到外卖时钉在外卖上的商家收到的订单,幸运的话再看看外卖小哥送外卖时接收的订单,它们所包含的信息内容和信息数量绝对是不同。
这代表着在商家子域、用户子域、配送子域中我们根据子域的不同定义了不同侧重点的“订单”,而不是一股脑地都塞进全局订单类里让不同服务进行共享。也正是不同侧重点的“订单”只有在自己子域中才是“有效的”,“能被读懂的”(总不可能让商家去看外卖小哥的订单信息,用户去看商家的订单信息),子域便有了与之对应的界限上下文。
这一章的基础内容便到此结束,下一章可能会讲一下服务的进程通信或则是Saga管理事务,尽情期待。