likes
comments
collection
share

如何识别、管理系统的复杂度

作者站长头像
站长
· 阅读数 44

好的设计不是一蹴而就的,大抵是在一次次的演进中慢慢形成的。

复杂性是什么?复杂的原因是什么?如何衡量一个软件是否复杂?

复杂性 是指在软件系统中,任何导致系统难以被理解、被变更的事物;(Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system.)

复杂性源于系统对模块间依赖性不恰当的处理(侧重难以变更)与模糊性(侧重难以理解); PS:模块可以指C语言中的函数,Java中的类,一个组件(or 插件 or 模块),一个微服务,模块指代在不同的视角与维度下,系统不同粒度的基本组织单位。

依赖性导致变更放大认知负荷

  • 变更放大:比如多个模块依赖了相同的一段业务逻辑,那么这个业务逻辑在发生变更时,我们需要将所有涉及到这个业务逻辑的模块进行修改;
  • 认知负荷:比如在使用多个模块时,模块间可能存在时序等方面的隐式依赖,那么要正确的使用这些模块需要我们认知到这部分依赖。

模糊性会导致未知的未知,即在不够了解这个系统的情况下,如果需要修改某个业务逻辑,不清楚需要修改哪些地方,经常会遗漏应该修改的地方。

在基于OOP构建的系统,当一个类调用了另一个类的接口时,模块间的依赖便产生了;依赖是无法被消除的,因此我们需要遵循OOP设计的原则更好的管理系统模块间的依赖关系;

依赖管理的方式影响了系统的复杂性,处理依赖的设计原则中两个方向为纵向分层横向分块,也对应了计算机领域的两个核心思想——分层与分治,而分层与分治本质上是SOC(关注点分离,Separation of concerns)与单一职责原则的体现。 PS:SOC的关键在于识别出关注点,这个后面会提到。

纵向分层

分层的一些实践

纵向分层中有很多常见的设计思路,比如:

  • 遵循依赖抽象而不是依赖实现的原则(分层):要用更简单、更稳定的依赖替换不够稳定的依赖;抽象一方面掩盖了实现的细节、精炼了语义,减少了上层模块的认知负担,另一个方面遵循间接原则,建立了抽象层(Layer),将功能与实现进行了分离,便于不同实现遵循单一职责原则,缓解变更放大。
  • 组件化(上层组装下层):组件化是纵向分层的常见实践;下层代码按照不同的机制实现功能组件,而上层代码对功能组件进行组合形成不同的策略。本质上是将系统分成了知识(机制)层与操作层,因为知识是相对稳定且可收敛的,而基于知识组合的上层功能是多变的;这也是Unix在设计自身Api时遵循的原则——系统提供的应该是机制而不是实现,比如Unix提供了管道这个机制,可以用管道来实现不同的功能组合。
  • IOC(控制反转,下层回调上层):控制反转是实现框架的重要思想,由框架代码来定义流程,并提供给用户代码扩展点(or 回调函数),与组件化上层组装下层不同,IOC则由下层代码控制上层代码;这样的好处在于框架设计的可重用性;比较常见的实践是Spring框架,控制了Bean的注入流程,以及Spring提供的基于Bean生命周期流程以及事件的可扩展点;IOC缺点在于上层代码的理解成本较高,比如代码中使用了@PostConstruct注解,不了解底层代码根本不知道它做了什么。

分层作为一个常见的分解复杂度的手段,最为有代表性的实践大抵是Tcp/Ip协议栈的设计了;

不过分层虽好,但是也不可避免的引入了一些需要面对的问题。

分层的问题(层间需要通信)

  • 变量跨层的Pass-through:如果一个变量只在Low-Level中使用,那么这个变量需要从High-Level透传到Low-Level中去,会导致入参的膨胀;比较常见的例子,加解密的方法封装到了基础的Sdk里面,并且上层代码并不关系,那么需要一路传下去,为了避免方法体的膨胀,目前主流的解决方案是全部层共享一个Context对象,但是需要考虑Context对象的线程安全。
  • 层与层功能划分、数据跨层流动的方式需要形成共识:如果分层的方式小组内没有形成共识,会导致数据层间流动非常频繁,最后导致每一层的逻辑都非常宽且浅,最终分层也就名存实亡了。因为有比较重的逻辑、比较多的细节才会抽象出层,所以每一层中自己能处理更好的逻辑就不要将问题抛给上层。在Java中比较常见的一类跨层抛问题的场景是Throw Exception,但是上层处理往往也只能try-catch-throw然后抛给更上层,并且try-catch对于代码结构/缩进有毁灭性的打击,所以如果一个异常交给上层处理时,上层也不能基于信息做出更好的决定时,不如内部自己引入机制进行处理;因此上层应该尽量控制下层的行为而不是决定下层的行为(control not determine),比如在Tcp的超时场景下,上层控制了超时时间的设定,而没有决定丢包重试时下层应该如何做,Tcp层自己实现了指数退避的重传方式,上层只关心在指定的时间内,包有没有发出去。但是有些场景,下层代码很难知道应用的最佳策略,而上层则对领域更加熟悉,比如上层可能知道哪些请求比其他请求更为紧迫,在这种场景下,配置参数可以在更广泛的领域中带来更好的性能。

横向分块

横向分块的一些实践

横向分块中亦有很多常见的设计思路,比如:

  • 模块化: 系统以业务功能的角度垂直划分成一个个存在透明(显式)依赖关系的模块;模块化与组件化是两个类似的概念,但是侧重的点不同;组件化侧重代码的可重用性,相同的功能可以通过组件进行代码复用,组件与组件使用者往往是上下层的关系,组件间不强调接口协议的统一;而模块化侧重代码的封装性以显式管理模块间的依赖,将不同的业务关注点抽象成接口协议相同的模块,而模块与模块的使用者往往是同级的关系,模块化后模块的使用者依赖的是模块的接口而不是依赖某个具体的模块。比较常见的模块化系统比如Nginx,所有的核心模块都实现了ngx_module_t结构体中定义的通用接口,该结构体定义了模块如何进行初始化、退出以及对配置项的处理,因为所有的核心模块都实现了接口,所以Nginx以该接口为基础是实现了对模块的统一管理,并实现了所有模块的配置热更的机制;更常见的一种方式为策略模式的运用,每一组策略类即为实现了相同接口协议的一组模块。

模块间如何通信

拆分出的模块最好具有相同的接口协议,如果模块的接口不尽相同,会导致模块间的依赖方式从依赖接口退化为依赖实现,因此可以使用适配器模式、门面模式进行统一接口的提取。

何时以及如何对系统进行分层、分块

分层与分块并不是正交的,这两种方式往往是相互交叉使用的,比如使用时序分解的方式划分模块时,按照业务的不同处理流程,拆分成一个个独立的模块,但是不同的模块可能会使用到相同的机制,而这个时候,需要进行代码分层,将相同的机制下沉,不同的模块依赖相同的机制层,使得在该机制被修改时,只需要修改一处。

如何识别层与模块?

我们的系统之所以复杂,往往是因为系统中存在一些隐式且重要的概念没有被定义、命名、抽离隐式的概念与被定义的概念以一对多、多对多的形式耦合在一起,使得程序的核心逻辑变得复杂。 因此需要对系统进行SOC以遵循单一职责原则,识别出概念,而相互平行的概念拆分为模块,上下依赖的拆分为层。

分层、分块实践

上层定义模块组装流程,下层定义模块的不同实现

  • 比较常见的实践是上层进行不同元素模块的组装,比如常见的插件化设计,上层主流程中定义了不同的模块在流程中的位置以及依赖关系,而下层则通过插件的方式按照不同的策略使用相同模块的不同实现方式。PS:可以类比为Pipeline模式+策略模式
  • 另一种比较常见的一种实践是访问者模式(Visitor Pattern,类似迭代器模式)的运用,主流程中定义了对于元素的访问方式,然后依据传入的实现相同接口的元素操作对象,将元素的访问方式与对元素的操作方式这两个关注点进行分离,这样将代码分为了两层,在元素的访问过程中,对元素进行操作时,依赖的是接口的操作方法,从而在操作方法改变时,元素访问的代码无需修改,只需要增加一个操作类即可,操作类(模块)与操作类是平行的。PS:其实可以类比为模板+策略。

本质上是将通用代码(组装插件的代码、元素访问方式的实现代码)与专用代码(插件代码、操作类)通过分层实现了分离,不同的实现按照不同的场景进行了模块划分。这类设计方式的共性在于主流程需要是稳定的,比如使用访问者模式的前提在于元素的结构是稳定的,使用流程组装的前提是业务流程是稳定的。

也可以看到,由设计原则衍生的设计模式,不仅可以指导类粒度的设计,也可以使用相同的思想指导系统的设计。

概念提取(基于抽象)、分层

  • 比较常见的实践是对正交概念的提取与分层,比如MVC、Restful的设计,视图(数据展现形式)与数据(Model对象)本就是正交的,并且会存在多对一的关系,一种数据可以有多种的展现形式,耦合在一起不利于代码的依赖复杂度管理,比如退回原生Servlet的编写方式,如果需要对所有网页页面的数字改成新罗马体,那么需要找到所有含有数字的Servlet对象,然后再修改;如果分层,只要在视图层引用相同字体组件就可以了。
  • 另一种比较好的实践是泛型编程,泛型编程将类型的概念从程序算法中分离出来(程序=算法+数据结构),使得在通过oop编写程序时无需再基于不同的数据结构写不同的类了。

总结

因此,对于SOC,概念抽象是前提,分解(分块、分层)是目的,而模块化与组件化是结果。分层侧重不同层概念代码的可重用、可复用,分块侧重对于同层概念代码的封装性使得依赖可管理。

主要参考:

关注点分离的艺术 ·有抱负的工匠 (aspiringcraftsman.com) GraphQL及元数据驱动架构在后端BFF中的实践 - 美团技术团队 (meituan.com) 组件化开发和模块化开发概念辨析 - Derek_Cheng - 博客园 (cnblogs.com) 前言 | 《软件设计的哲学》中文翻译 (cactus-proj.github.io)

转载自:https://juejin.cn/post/7214699255508435005
评论
请登录