一个遗留项目的结构引发的思考

3,157 阅读7分钟

原有项目结构

原有的项目结构是这样子的,web,rpc,mq schedule 拆到不同的 module,单独部署。除去主动适配器层之外其他的层都放到 service 里面,并通过共享主机模式被其他 module 依赖。当修改 service 逻辑的时候,如果上层的 module 都依赖到的话,则所有服务都需要更新。这种拆分的好处是可以做到弹性伸缩和资源隔离。缺点是开发效率变低,资源效率低。这个只是比较粗略的分析。我们继续探讨一下一般比较合理的拆分方法是怎样子的,再分析一下这种拆分方法是否合理(合理意味着利大于弊)。

image.png

系统划分方式

一个系统的划分可以有2种,一种是按功能拆分,一种是按分层拆分。本质上都是分类,分类是为了能更快的找到你要改的内容。两种分类方式都是合理的。按功能分可以让我们隔离其他功能的影响,而按分层分可以让我们隔离技术(包括通讯协议,中间件,数据库等概念)对我们的影响,都是为了专注于业务逻辑的理解和修改。分层基本上业界都达成了共识,常见的模型整洁架构,端口和适配器模式,几乎都是要进行分层的。而功能则不一定要分,一开始系统功能很简单,可以不用分了。但随着业务的发展,某一层会越来越庞大。很容易导致几百行代码或者一个分层有几十个类的出现。可能有人觉得我在分层基础上进行拆分就好了。例如一个 controller 包和 service 包,再分包就好了。这种就是常见的先分层,再分功能的方式。但这并不是一个好的方式。

先功能后分层

先功能后分层是一种更好的方式。有几个原因

  1. 需求几乎都是面向功能的。分层之间从需求的角度看耦合很大。要修个一个功能需要先找到功能的位置,然后往往要修改所有层。如果先分层再分功能,则要修改的内容会跨的有点远,不是一个层级。

既然要修改所有层,无法隔离变化,我们为什么还要分层呢?

  1. 用户的使用也是根据功能分的,同一个功能的qps,可用性等非功能性需求在一个功能内部的耦合也是很大的。意味着我们很可能会因为非功能性需求而对功能进行拆分。
  2. 分层之间的依赖是不可避免的,但功能的依赖则不是必须的。对功能进行依赖管理可以让功能间耦合度没那么大。

分类的实现方式

image.png

分类的实现方式有好几种,从左往右边界越来越弱,耦合越来越大,但开发也越来越方便。

不同分类方式有不同特点

  • 服务: 类似微服务,这种边界最清晰。想依赖只能依赖接口。适合不同团队协作,对单个服务进行弹性伸缩,隔离计算资源。但如果仅仅用于代码解耦,那成本太高了。微服务谁用谁知道,不是什么场景都适合拆的小小的,拆的不好会极大降低开发效率。
  • 模块: 可以通过 maven 强制指定模块间依赖关系。比较容易往服务拆分发展。缺点是只要依赖一个模块,就可以依赖它的所有东西。需要管理依赖关系.
  • 包: 几乎没什么边界,package之间可以随意依赖。

module 是比较适合分功能的,因为只有功能是需要指定依赖关系的。分层的依赖关系都是固定的,简单的,不是很有必要使用 module 来进行约束。而功能则是灵活的,功能在涉及之初我们就希望这个功能就尽可能少的依赖其他功能,让它更加稳定,避免被影响。

分层可以通过模块和包分都可以,但我更倾向于按包分.前面提到分层的依赖关系比较简单,可以不加编译约束。而且按模块还需要管理依赖关系。但如果团队对边界把握不好,使用模块去划分也是 ok 的,这样可以保证依赖关系不会错,特别对于依赖倒置这种比较难理解的。

基于此可划分为

image.png

ohs 还可以划分为 web,rpc,schedule 等。一个项目作为一个单体运行。如果像 dubbo 那样需要单独一个包作为 rpc 的 api,也没关系,把 dubbo 接口单独提取即可。

功能间的依赖

前面提到 module 的一个缺点是只要依赖了,就能访问所有的内容。不同功能之间合理的依赖关系是怎样子的呢? 如果按照往服务的方向发展,那 module 最多只能依赖另一个 module 的应用层,而不能依赖领域层。这是一种方式,如果按这种方式,最合理的方式是 module 的防腐层依赖另一个 module 的应用层。通过防腐层进行隔离。

另一种方式则是类似共享主机模式,一个 module 直接依赖另一个 module 的领域模型。优点是开发效率高,缺点是边界不够清晰,需要依赖整个模型。

两种方式有利有弊,需要看实际情况来衡量,例如模型间的耦合程度,是否有一方后续有计划转成微服务。如果是第一种方式,建议参考 dubbo rpc 的方式把依赖的方法单独一个包作为接口。

评价原有项目的设计

原有项目有几种分层

  1. 是先按分层分(主动适配器和其他)
  2. 在其他里面按照功能分(function1,function2)
  3. 再根据分层分(function1 还会包括领域层和被动适配器)

第一个分层是为了做资源隔离。但资源隔离其实有更好的方式去做,例如对线程池进行隔离,或通过配置的方式在不同的容器启动不同的协议(web,mq,rpc等)。从而保持最终打出来的包是同一个。在开发中发现使用 module 来隔离会有很多坑,例如 maven 依赖很乱,修改 maven 依赖可能会导致某个包无法启动,需要所有的包都试过运行没问题才行。改到 service 模块需要所有包都更新等等问题。有点得不偿失。 弹性就更说不上,如果说例如 mq cpu的占用很高,单独一个服务也可以接受。但一股脑全部都分 module 是完全没考虑成本的体现。

第二三层分层虽然也是根据功能分,再根据分层分。但第二层是通过 package 来分的,导致无法一目了然的梳理出模块间的关联关系。只能通过分析实现逻辑来看有没有依赖到别的模块,很不直观。如果要评估一个功能能不能单独有一个服务,非常困难。依赖也非常难以管理。

总结

  1. 遵循先分功能再分层,及时拆分模块。非必要不拆服务。
  2. 能通过技术实现的问题不要通过结构上解决。
  3. 多维护好依赖关系,便于后续的拓展。