解决软件复杂度之道

1,779 阅读12分钟

前言

         在软件行业,传统2B与新型互联网的2C业务目前是主流的软件业务领域,2B软件主要面向企业用户,2C软件主要面向消费用户。

       两种软件业务模式从行业、产品、商业模式等方面上都有诸多的不同的点,这也导致产生软件复杂度的因素与表现也是不同的。

       2C业务更多的是要处理高可用、高并发、安全与海量数据处理等带来的技术复杂度,2B业务主要是如组织架构、多角色参与、多场景的业务流程与规则带来的业务复杂度。最近经手了几个2B业务的软件系统,无论是项目大小还是产品、业务的复杂度都是非常大的,真可谓”一入2B深似海“呀,本篇就以近期参与的一个企业OA管理小应用来说明下2B软件复杂度的一些见解。

1.软件复杂度

大型系统的本质问题是如何解决复杂性的问题。一般的互联网软件,是典型的大型系统,如下图所示,数百个甚至更多的微服务相互调用/依赖,组成一个服务数量庞大、行为复杂、时刻在变动(发布、配置变更)当中的动态的、复杂的系统。

1.1 软件复杂度是演进的产物

      “冰冻三尺非一日之寒”,这句话用在软件系统演进上再合适不过。大家会觉得软件架构与建筑架构是相同,在很多架构书籍或知识分享上都拿建筑架构来做类比,个人认为有些方面是类似的,但是这里有个本质上的区别,建筑一旦开工,那么整体架构设计就是相对固定的,除非拆了重来,但是软件系统架构却会随着用户规模/产品需求/技术变更等等变化而不断快速调整与更迭,常说的“给飞机换轮子”就是这个道理。 例如一些电商系统,早期都是由一个单体应用,经过4、5代架构不断演进,才到今天服务十亿人规模的电商交易平台。Amazon交易平台、Google搜索、Netflix微服务等这些平台或系统都是有类似的历程。

1.2 软件的高复杂度带来的挑战

        高复杂度的软件系统一般都会给业务与团队带来以下认知负荷高/协同成本高两个突出的问题。这两个维度相互区别但却又相互关联。

第一,认知负荷: 理解软件的接口、设计或者实现所需要的心智成本。而认知负荷高的软件模块让开发童鞋的难以理解,从而产生两个后果:(1) 难以维护,无法预估改动影响范围,bug 率故障率高;(2)大部分情况下,都会选择整个软件系统推倒重来,这就造成了浪费了,更糟的情况的,代码被抛弃但是又无法下线,但用户产品侧又有新需求紧逼,最近就在经历这样的过程,在产研资源紧张的情况下,迭代管理会变得一塌糊涂。

第二,协同成本:团队维护软件时需要在协同上额外付出的成本。协同成本高,让软件系统演进速度变慢,效率变差,工作其中的开发童鞋感到压力增大,而长期难以取得进展,一般有大的用户量/请求流量/数据流量才能对系统进行优化重构。但大部分的童鞋会倾向于离开项目,最终造成质量进一步下滑的恶性循环。

1.3 影响软件复杂度的因素

1.3.1 认知负荷的因素

不恰当的实现逻辑

        我们经常在开发中遇到“成片”的if-else语句,有些深入到五六层之多,这给接手的人带来了巨大的认知负荷。如下所示左右两边的实现效果是等价的,但代码实现逻辑是不同的,右边使用了较多的卫语句,如果在没有相关业务与技术背景情况下,右边部分的实现所需要的认知、维护成本都要比左边的实现会低很多。

模型设计不恰当

       软件的模型设计需要符合现实物理世界的认知,否则会带来非常高的认知成本。 当时在做一个设备管控的项目,里面有个比较基础的设备管理模型,历史原因,整个设备管理模型是设计成设备基础信息与配置的一个大集合,但是这样的设计,完全不符合用户的认知,对于用户来说,感受到的应该是设备和配置的概念,而不是带着设备管控参数的配置,可见其维护成本非常之高。

_接口设计不当_

举一个缓冲区接口实现的例子,这个问题可以明显看到一个接口设计的不合理带来的维护成本提升:一个Buffer的设计暴露了内部内存管理的细节(slot维护),从而导致在调用最常用接口 “insert”时存在陷阱:如果不在insert前检查空余slot,这个接口就会有异常行为。但是从设计角度看,维护底层的Slot的逻辑,也外部可见的buffer的行为其实并没有关联,而只是一个底层的实现细节。 因此更好的设计应该可以简化接口。把Slot数量的维护改为内部的实现逻辑细节,不对外暴露。这样也完全消除了因为使用不当带来问题的场景。 同时也让接口更易于理解,降低了认知成本。

1.3.2 影响协同成本的因素

团队分工

在微服务化时代,合理的模块/服务的切分和团队分工会加快开发迭代效率。而模块拆分和边界的不合理,则会增加系统/代码维护的复杂度,这时新的特性需要在跨多个团队的情况下进行开发、测试和迭代。则是或者就是我们常说的“组织架构决定系统架构”,软件的架构最后会围绕组织的边界而变化,当组织分工不合理时,会产生重复的建设或者冲突。

服务依赖

如下图所示,是我们常见的系统模块拆分的方式。

  • 有四个团队,其中一个是框架团队负责框架实现,框架具有三个扩展点,这三个扩展点有三个不同的团队实现插件扩展,这些插件被调用,从架构上,这是一种类似于继承的模式。
  • 底层的系统以API服务的方式提供接口,而上层应用或者服务通过调用这些接口来实现业务功能。

这两种模式适用于不同的系统模型。当框架模式更偏向于底层、不涉及业务逻辑且相对非常稳定时,也即框架被集成到团队1,2,3的业务实现当中。例如RPC 框架开发就是这样的模型:RPC底层实现作为公共的基建代码/SDK提供给业务使用,业务实现自己的RPC 方法,被框架调用,业务无需关注底层RPC实现的细节。因为框架基建代码被业务所依赖,因此这时业务希望框架的代码非常稳定,而且尽量避免对框架层的感知,这时右边的通过使用是一种比较合适的模型。在领域驱动设计中,还有其他的几种团队合作模式。

命名

        软件中的API、方法、变量的命名,对于理解代码的逻辑、范围非常重要,也是设计者清晰传达意图的关键。但是在很多项目中,我们并没有给出统一的命名方式与逻辑,我曾经听一个哥们说他们的项目是全部用汉语拼音来做命名的,搞得他们痛不欲生。

      一个不好的例子是我们搞的一个项目API 被命名为Phoenix API(凤凰),设想一下代码中的对象叫Phoenix时,我们如何理解在这个对象应该具备的行为? 再对比一下K8s中的资源: Pod, ReplicaSet,Service, ClusterIP,我们会注意到都是清晰、简单、直接符合其对象特征的命名。名实相符可以很大程度上降低理解该对象的成本。

文档

       降低协同成本需要清晰的接口/API说明的文档,针对接口的场景、使用方式等给出清晰描述。这些工作需要投入,开发团队有时不愿意投入资源,毕竟在哪里开发资源都是很紧张的。特别是对一些有年代的,处于运维期的系统,但是对于每一个用户/使用方,协同成本太,大大增加了因使用不当而造成的故障概率。

整体可测试性

         测试可分为功能/接口/单元/性能等几大类测试,一般在交付项目或者产品版本时都需要做好充足的测试才能保证质量,但经历过几个团队都没有充分的单元测试,在juejin.cn/post/695464… 关于单测的一些实践经验中,好的单测是可以很好的做集成测试的,但往往会因为单测不足/模块测试不足而带来联调/集成阶段的复杂度升高,最终可能导致失败或返工,极大增加了协同的成本。因此做好代码的充分单元测试,并提供良好的集成测试支持,是降低协同成本提升迭代效率的关键。

2.软件复杂度解决之道

2.1 解决复杂度的心法

        解决软件复杂度需要很大的耐心与决心,如下图所示,还需要一个开发童鞋具备一些底层思维和能力要求,不然很难看到问题,同样很难解决问题。

2.2 解决复杂度的相关实践

理解业务

理解业务是所有工作的起点。要找到业务的核心要素,理解核心概念,梳理业务流程。完整的方法论应该是“业务理解-->领域建模-->流程分解-->多维分析”。从业务产品出发才是王道。业务理解应该是从产品层面开始,以这个简单的OKR工具应用为例。

                                              

    从OKR管理的基本流程可以梳理一个主要业务流程与相关的功能点。

         通过上面的产品的主要业务架构,我们可以根据目前的信息来梳理出相关的领域设计,其中领域图是非常关键的,可以指导后面的技术架构与技术选型。

在领域驱动设计中,建立一套领域通用语言(术语)是非常重要的一步,可以提升团队整体对业务的理解,慢慢发现大家都在同一个单词来描述, 不要放过任何一个模糊的业务概念,一定要透彻的理解它,并给与合理的命名。唯有如此,我们才能更加清晰的理解业务,才能更好的开展后续的工作。

领域建模

        在软件设计中,模型是指实体,以及实体之间的联系,理解这些模型还是需要对业务流程有清晰的认识,通过拆解主要业务流程来分析出主要的实体模型是常见的一种做法。

再复杂的业务领域,其核心概念都不应该太复杂,抓住了核心,我们就抓住了主线,业务往往都是围绕着这些核心实体模型展开的。

合理的技术架构

        通过业务领域分析,可以得出我们现在的技术架构,一个合理的技术架构是需要有完整的需求,主要针对于功能、约束、质量等方面维度的思考,结合组织、开发、用户等不同视角去分析。至于具体的架构风格,都有比较成熟的参考,比如三层架构/微服务架构/事件响应服务架构等风格,这里就不赘述了。

业务流程分解

**流程分解就是对业务过程进行详细的分解,使用结构化的方法论(先演绎、后归纳)。**比如,在目标管理领域,有创建目标、增加关键结果、增加动态、检查用户权限的等一些列动作(流程),每个动作的背后都有非常复杂的业务逻辑。我们需要对这些流程进行详细的梳理,然后按步骤进行分解。 

多维分析

        业务的复杂性主要体现在流程的复杂性和多维度要素相互关联、依赖关系上,结构化思维可以快速梳理流程与上下文关系,而矩阵思维可以梳理业务实现上的多维度关联、依赖关系。二者结合,可以更加全面的展现复杂业务的全貌。 这里推荐一个分析方法,该原理刚才也用于架构需求分析。

因此,我们在做矩阵分析的时候,纵轴可以选择使用业务场景,横轴是备选维度,可以是受场景影响的业务流程(如文章中的商品流程矩阵图),也可以是受场景影响的业务属性(如文章中的订单组成要素矩阵图),或者任何其它不同性质的“东西”。通过矩阵图,可以清晰的展现不同场景下,业务的差异性。基于此,我们可以定制满足差异性的最佳实现策略,可能是多态扩展,可能是分离的代码,也可能是其它。

3.总结

        本文针对软件复杂度做的一些见解分析,主要说明了目前软件复杂度产生的一些因素与表现形式,同时结合实际业务开发给出了一些解决软件复杂度的经验和方法。

参考文献

www.zhihu.com/question/30… 2B与2C业务区别 

www.cnblogs.com/evan-liang/… 领域驱动设计笔记