Skip to main content
  1. internet/

以真实经历谈分层

·3550 words·8 mins·

笔者在工作过程中遇到了一些分层相关的问题,于是将问题和想法记录下来,以供未来回顾。

提出问题 #

  1. 什么是分层
  2. 为什么要分层
  3. 怎样做分层

什么是分层 #

这是一个很简单的问题。

这也是一个很复杂的问题。

简单之处在于每个人都能做出回答,复杂之处在于这其实是个通用问题

通用问题是啥?百度百科上是没有这个词条的,因为我不知道这类问题如何划分,所以随便造了个词,或者称为底层逻辑问题更好理解些?

程序员当然知道有哪些分层:网络有分层、操作系统有分层、项目有分层、代码有分层等等。

但生活中的分层要更多。

  • 每天早上吃的鸡蛋有分层:蛋壳、蛋白、蛋黄
  • 上班路上两边的树木有分层:树根、树干、树冠、树叶,或者将其拦腰斩断,能看到层次分明的年轮
  • 坐电梯时可能更能体会到分层——每层楼都是一层。
  • 进入公司,销售部、行政部、研发部等等也在分层
  • 连我们人体本身也满是分层:上半身、下半身、头、胳膊、脚,或者皮肤、脂肪、血液、骨骼、神经等等

分层是这个世界的基本规则之一。

思维的发散就到此为止吧,因为我已经发现没有办法直面我们最初的三个问题了。

所以让我们来简化下问题——将问题的讨论范围限制在代码内

对于什么是分层——我先给出我的答案——分层就是对代码按照某种规则进行切分

至于为什么是这个答案,下面会讲。

为什么要分层 #

我们先来回顾下分层的演进。

最早的分层是什么呢,那一定是没有分层。当我们打印出“hello world"时,我们是没有分层的。

让我们继续写代码。我可能要在前端展示一些文字,这些文字可能存储在数据库中。如果仅仅是这样的话,我们很可能还是没有分层——功能实在是太简单了。

直到有一天,我们写了上千行的代码,突然发现代码已经很难维护了,因为数据模型、业务逻辑、前端代码等都混在一起,于是我们本能的开始分层。于是一个伟大的概念产生了——MVC

MVC最早据说是起源于桌面端开发,M代表数据层,V代表UI层,C代码控制层,通过分离这三层,我们的代码已经是很清晰了。

但是该死的产品经理还在没完没了的增加那些不知道有什么用的功能。

于是代码开发者发现三层不够用,于是把前端和后端代码进行了隔离,也就是前后端分离。后端将已有的两层扩展为三层——控制层-逻辑层-数据层(controller-service-model)。那么前端呢?前端都分出去了,我们就不管了。

这里有个逻辑要叙述下。有些人认为是ajax这类技术的产生才导致了前后端分离。这种想法属实是本末倒置了,任何技术的产生都来源于需求!

我对于controller-service-model这种分层可谓是异常熟悉,因为就在我大学实习的时候,就用的这种分层。当时用得是java的SSM框架,三个框架正好对应这三层(java好像搞啥都是一整套?)。这几个框架让我深受贫血模型的影响,即使我后来不写Java了。

时代在发展,软件的用户越来越多,功能越来越复杂,开发人员越来越多。代码也越来越臃肿。

于是某个大佬发明了微服务的概念,再然后某个大佬发明了中台的概念。

于是我们不仅有前后端的分层,还有后端与后端的分层——前台、中台、支撑的分层。

回到我们的问题——为什么要分层——答案应该已经很明确了,就是为了解决代码的臃肿问题,让代码更清晰!

怎样做分层 #

服务分层 #

现状:目前公司内有很多中台仅仅是对数据库的CRUD的封装(看起来就像是封装了一个使用http做传输的ORM框架),业务逻辑仍集中在前台。这种中台没有任何意义,似乎只是为了分层而分层,或者说为了做中台而分层。进一步的原因就是设计者对中台缺乏认知。

目前我们的项目存在两种分层方式:按功能划分按业务划分

以报表功能举例:我们在多种场景中都需要报表功能,如人事报表、招聘报表。这些报表都有自己的业务逻辑,不能进行统一处理,但是都需要订阅功能,且都存在业务逻辑:当用户删除报表时,需要同时删除用户对该报表的订阅(该功能在下文用功能A标识)。

按功能划分 #

根据功能的性质划分,此时订阅功能和报表功能为同等级功能。

此时会存在:报表中台、报表前台、订阅中台、订阅前台。

功能A应在报表前台来实现。

按业务划分 #

按照业务来划分,此时订阅功能应被视为报表的附属功能。

此时会存在:招聘报表中台、招聘报表前台、人事报表中台、人事报表前台。

功能A应在招聘报表中台和人事报表中台分别实现。

划分手段 #

上边直接说了结论,那么这样划分的依据是什么?

首先必须要分为中台和前台:中台作为业务的聚合,而前台作为对前端的适配。这样能保证业务逻辑的内聚,使中台专注于自己的业务,避免易变的产品需求对业务核心代码的侵蚀。

其次一定要让服务有明确的边界。设计者不能凭感觉来划分服务,一定要有自己的方法论作为指导基础。如果只凭感觉来划分,最终的结果就是服务之间没有边界,导致中台服务冗余了大量不属于自己领域内的代码。

所以不管是按功能划分还是按业务划分又或者有其他划分方法,总之设计者一定要有自己的划分方法论。

代码分层 #

现状:目前公司内大量项目的代码结构为controller+business+service。business做业务逻辑,service做服务实现。换句话说,就是将以前的service层改名为business,以前的model改名为service。这种改变的逻辑是:微服务时代需要大量调用其他服务,model不具有此含义,因此需要将model改名为service,用service来处理调用其他服务的逻辑。

这种结构在实际开发中面临一个非常严重的问题——business和service的边界模糊——导致service层的代码和business层代码混在一起——导致本就复杂的业务层代码更加复杂且难以理解。

如何解决business和service的边界模糊问题 #

边界模糊的原因1:词汇描述能力不足。我们一般使用service来写业务逻辑,现在换用了business,但是仍保留service层来做服务调用,这增加了开发者对service和business语义上的模糊。另外,从读者的角度来看,这种命名会让人十分疑惑。

边界模糊的原因2:分层之间没有约束。目前在分层上business依赖并调用service层,没有约束business层对service层的访问限制,导致部分应属于service层的代码放到了business层,或者应属于business层的代码放到了service层。

解决手段1:依赖倒置。要限制business对service层的访问,很自然会想到依赖倒置——让原本business层依赖service层的情况改为service层依赖business层。

解决手段2:强化业务概念。为了避免service和business语义上的模糊,我们只保留了service,用来处理业务逻辑。那服务调用如何表示?为了突出业务逻辑层,我们弱化了服务调用层——将服务调用作为业务逻辑的辅助层。

解决手段实现:端口-适配器模式非常契合当前的解决手段——将服务调用层抽象为适配器(adaptor),辅助业务逻辑层完成功能。将service层需要的外部资源(数据库、缓存、外部服务调用)抽象为接口,在adaptor层进行实现。即service层所需要的接口为“端口”,在adaptor层实现接口的对象为“适配器”。同时我们借助接口,实现了service层与adaptor的松耦合。(理解上述描述需要对go中的接口有一定了解)

如何处理复杂的业务逻辑 #

解决了历史问题,我们再进一步思考一个问题:如何处理复杂的业务逻辑

写代码总是很容易的,让别人看懂则很难。

要解决这个问题,本质上还是要让代码保持清晰

我们还是本能的选择了进一步分层

如何进一步分层?业务逻辑的复杂会导致service层代码臃肿,因此一定是在这一层进行切割。

按照什么规则切割?service层中包含了业务规则对外部资源的调用,因此我们可以将业务规则抽离出来。我把这一新层命名为domain(致敬DDD)。

domain层的责任:如何在这一层中体现出业务规则来呢?业务逻辑的本质上就是对数据对象的转换,复杂的业务逻辑其转换规则也越复杂。因此合理的设计数据对象,并将这些转换规则封装为方法,将数据对象之间的转换对service层屏蔽是domain层存在的方式,也是其责任(如果熟悉DDD的话,可以理解为domain层就是编写对象的规则与对象之间的转换逻辑,这些对象包括entity与value object)。

总结 #

以上是为了解决现有问题而做出的一系列优化。

也许读者能够在其中看到类似于DDD的一些思想。在实际的工作中,我也碰到过一些同事遵守DDD提出的一套流程进行实践,但是在我看来,了解其思想然后解决现实问题才是我们学习这些思想的最大价值