当前位置 : 主页 > 网络编程 > 其它编程 >

阿里架构师,如何降低系统架构复杂度

来源:互联网 收集:自由互联 发布时间:2023-07-02
来源聂晓龙率鸽读APhilosophyofSoftwareDesign有感软件设计与架构复杂度你是战 来源聂晓龙率鸽 读 A Philosophy of Software Design 有感软件设计与架构复杂度你是战术龙卷风吗 01 前言 Aliware 有一天
来源聂晓龙率鸽读APhilosophyofSoftwareDesign有感软件设计与架构复杂度你是战

来源聂晓龙率鸽

读 A Philosophy of Software Design 有感软件设计与架构复杂度你是战术龙卷风吗

01

前言

Aliware

有一天一个医生和一个土木工程师在一起争论“谁是世界上最古老的职业”。医生说“上帝用亚当的肋骨造出了夏娃这是历史上第一次外科手术所以最古老的职业应该是医生”土木工程师说“在创世纪之前上帝从混沌中创造了天堂与人间这是更早之前的一次土木作业所以最古老的职业应该是土木工程”。这时软件工程师拖着键盘走出来说“那你认为是谁创造了那片混沌”

建筑师不会轻易给 100 层的高楼增加一个地下室但我们却经常在干这样的事并且总有人会对你说“这个需求很简单”。到土里埋个地雷这确实不复杂但我们往往面临的真实场景其实是“在这片雷区里加一个雷”而雷区里哪里有雷任何人都不知道。

02

什么是复杂性

Aliware

我们一直在说系统很复杂那到底什么是复杂性关于复杂的定义有很多种其中比较有代表的是 Thomas J. McCabe 在 1976 提出的理性派的复杂性度量与 John Ousterhout 教授提出的感性派的复杂性认知。

01

理性度量

a130510acfa8099976b6a6970280327c.png

复杂性并不是什么新概念早在上世纪 70 年代软件就已经极其复杂开发与维护的成本都非常高。1976 年 McCabe并提出了 McCabe Cyclomatic Complexity Metric我们也称之为 McCabe 圈复杂度。它通过多个维度来度量软件的复杂度从而判断软件当前的开发/维护成本。

圈复杂度

代码状况

测性成本

维护成本

圈复杂度1 - 10

清晰/结构化

可测性高

维护成本低

圈复杂度10 - 20

复杂

可测性中

维护成本中

圈复杂度20 - 30

非常复杂

可测性低

维护成本高

圈复杂度30

不可读

不可测

维护成本非常高

02

感性认知

复杂度高的代码一定不是好代码但复杂度低的也不一定就是好代码。John Ousterhout 教授认为软件的复杂性相对理性的分析可能更偏感性的认知。

Complexity is anything that makes software hard to understand or to modify译所谓复杂性就是任何使得软件难于理解和修改的因素。

John Ousterhout 《A Philosophy of Software Design》

50 年后的今天John Ousterhout 教授在《A Philosophy of Software Design》书中提到了一个非常主观的见解复杂性就是任何使得软件难于理解和修改的因素。

模糊性与依赖性是引起复杂性的2个主要因素模糊性产生了最直接的复杂度让我们很难读懂代码真正想表达的含义无法读懂这些代码也就意味着我们更难去改变它。而依赖性又导致了复杂性不断传递不断外溢的复杂性最终导致系统的无限腐化一旦代码变成意大利面条几乎不可能修复成本将成指数倍增长。

03

复杂性的表现形式

Aliware

复杂的系统往往也有一些非常明显的特征John 教授将它抽象为变更放大Change amplification、认知负荷Cognitive load与未知的未知Unknown unknowns这3类。当我们的系统出现这 3 个特征说明我们的系统已经开始逐渐变得复杂了。

01

症状 1-变更放大

Change amplification: a seemingly simple change requires code modifications in many different places.译看似简单的变更需要在许多不同地方进行代码修改。

John Ousterhout 《A Philosophy of Software Design》

变更放大Change amplification指得是看似简单的变更需要在许多不同地方进行代码修改。比较典型的代表是 Ctrl-CV 式代码开发领域模型缺少内聚与收拢当需要对某段业务进行调整时需要改动多个模块以适应业务的发展。

/*** 销售捡入客户*/public void pick(String salesId, String customerId) {// 查询客户总数long customerCnt customerDao.findCustomerCount(salesId);// 查询销售库容long capacity capacityDao.findSalesCapacity(salesId);// 判断是否超额if(customerCnt > capacity) {throws new BizException("capacity over limit");}// 代码省略 do customer pick}

在 CRM 领域销售捡入客户时需要进行库容判断这段代码也确实可以满足需求。但随着业务的发展签约的客户要调整为不占库容。而客户除了销售捡入还包括主管分发、leads 分发、手工录入、数据采买等多个场景如果没对库容域做模型的收拢一个简单的逻辑调整就需要我们在多个场景做适配才能满足诉求。

02

症状 2-认知负荷

Cognitive load: how much a developer needs to know in order to complete a task.译开发人员需要多少知识才能完成一项任务。

John Ousterhout 《A Philosophy of Software Design》

认知负荷Cognitive load是指开发人员需要多少知识才能完成一项任务。使用功能性框架时我们希望它操作简单部署复杂系统时我们希望它架构清晰其实都是降低一项任务所需的成本。盲目的追求高端技术设计复杂系统增加学习与理解成本都属于本末倒置的一种。

64eb4d2157c883eb7c4a880a7406564e.png

TMF 是整个星环的支柱也是业务中台面向可复用可扩展架构的核心。但 TMF 太过复杂认知与学习成本非常高我们日常中所面临的一些扩展诉求 99%或者应该说 100%都不适合 TMF可能通过一些设计模式或者就是一些 if else可能更适合解决我们的问题。

除此之外还包括一些简单搜索场景却用到了 blink 等流式引擎简单后台系统通过 DDD 进行构建几个商品发布的状态机转换用上了规则引擎等等都属于认知负荷复杂度的一种。

03

症状 3-未知的未知

Unknown unknowns: it is not obvious which pieces of code must be modified to complete a task.译必须修改哪些代码才能完成任务。

John Ousterhout 《A Philosophy of Software Design》

443328ba979ac27833151488190a3ad9.gif

未知的未知Unknown unknowns是指必须修改哪些代码才能完成任务或者说开发人员必须获得哪些信息才能成功地执行任务。这一项也是 John Ousterhout 教授认为复杂性中最糟糕的一个表现形式。

当你维护一个有 20 年历史的项目时这种问题的出来相对而言就没那么意外。由于代码的混乱与文档的缺失导致你无法掌控一个 500 万行代码的应用并且代码本身也没有明显表现出它们应该要阐述的内容。这时“未知的未知”出现了你不知道改动的这行代码是否能让程序正常运转也不知道这行代码的改动是否又会引发新的问题。这时候我们发现那些“上帝类”真的就只有上帝能拯救了。

04

为什么会产生复杂性

Aliware

那软件为什么越来越复杂是不是减少一些犯错就能避免一场浩劫呢回顾那些复杂的系统我们可以找到很多因素导致系统腐化。

  • 想简单图省事没有及时治理不合理的内容

  • 缺少匠心追求对肮脏代码视而不见

  • 技术能力不够无法应对复杂系统

  • 交接过渡缺失三无产品几乎无法维护

  • 除了上述内容外还可以想到很多理由。但我们发现他们好像有一个共同的指向点 - 软件工程师似乎所有复杂的源头就是软件工程师的不合格导致所以其实一些罪恶的根因是我们自己

    01

    统一的中国与分裂的欧洲

    欧洲大陆面积大体与中国相当但为什么欧洲是分裂的而中国是统一的。有人说他们文化不一样也有人说他们语言不通是主要原因也有人说他们缺一个秦始皇。其实我们回顾欧洲的历史欧洲还真不缺一个大一统的帝国。罗马帝国曾经让地中海成为自己的内海拿破仑鼎盛时期掌管着 1300 万平方公里的领地。欧洲也曾出现过伟大的帝国但都未走向统一。

    我们再观察地图其实除了中国、俄罗斯以外全世界 99%的国家都是小国。分裂才是常态统一才不正常。马老师也曾说过成功都有偶然性只有失败才存在必然。只有极少国家才实现了大一统所以我们不应该问为什么欧洲是分裂的而应该问为什么中国是统一的。类比到我们的软件也同样如此复杂才是常态不复杂才不正常。

    02

    软件固有的复杂性

    The Complexity of software is an essential property, not an accidental one.译软件的复杂性是一个基本特征而不是偶然如此。

    Grady Booch 《Object-Oriented Analysis and Design with Applications》

    Grady Booch 在《Object-Oriented Analysis and Design with Applications》中提出这样一个观念他认为软件的复杂性是固有的包括问题域的复杂性、管理开发过程的困难性、通过软件可能实现的灵活性与刻画离散系统行为的问题这 4 个方面来分析了软件的发展一定伴随着复杂这是软件工程这本科学所必然伴随的一个特性。

    Everything, without exception, requires additional energy and order to maintain itself. I knew this in the abstract as the famous second law of thermodynamics, which states that everything is falling apart slowly.译世间万物都需要额外的能量和秩序来维持自身无一例外。这就是著名的热力学第二定律即所有的事务都在缓慢地分崩离析。

    Kevin Kelly 《The Inevitable》

    Kevin Kelly 在《The Inevitable》也有提过类似的观点他认为世间万物都需要额外的能量和秩序来维持自身所有的事物都在缓慢地分崩离析。没有外部力量的注入事物就会逐渐崩溃这是世间万物的规律而非我们哪里做得不对。

    05

    软件架构治理复杂度

    Aliware

    为软件系统注入的外力就是我们的软件架构以及我们未来的每一行代码。软件架构有很多种从最早的单体架构到后面的分布式架构、SOA、微服务、FaaS、Service Mesh 等等。所有的软件架构万变不离其宗都在致力解决软件的复杂性。

    01

    架构的本质

    编程范式指的是程序的编写模式软件架构发展到今天只出现过 3 种编程范式( paradigm )分别是结构化编程面向对象编程与函数式编程。

    • 结构化编程取消 goto 移除跳转语句对程序控制权的直接转移进行了限制和规范

    • 面向对象编程限制指针的使用对程序控制权的间接转移进行了限制和规范

    • 函数式编程以 λ 演算法为核心思想对程序中的赋值进行了限制和规范

    面向对象的五大设计原则 S.O.L.I.D。依赖倒置限制了模块的依赖顺序、单一职责限制模块的职责范围、接口隔离限制接口的提供形式。

    软件的本质是约束。商品的代码不能写在订单域数据层的方法不能写在业务层。70 年的软件发展并没有告诉我们应该怎么做而是教会了我们不该做什么。

    02

    递增的复杂性

    软件的复杂性不会凭空消失并且会逐级递增。针对递增的复杂性有 3 个观点

  • 模糊性创造了复杂依赖性传播了复杂

  • 复杂性往往不是由单个灾难引起的

  • 我们可以容易地说服自己当前变更带来的一点点复杂性没什么大不了

  • 曾经小李跟我抱怨说这段代码实在是太恶心了花了很长时间才看懂并且代码非常僵硬而正好这个需求需要改动到这里代码真的就像一坨屎。我问他最后是怎么处理的他说我给它又加了一坨屎。

    03

    3

    编程思维论

    1、战术编程

    其实小李的这种做法并非是一个个体行为或许我们在遇到复杂代码时都曾这样苟且过John 教授这种编程方法称之为“战术编程”。战术编程最主要的特点是快同时具备如下几个特点

  • 当前一定是最快的

  • 不会花费太多时间来寻找最佳设计

  • 每个编程任务都会引入一些复杂度

  • 重构会减慢当前任务速度所以保持最快速度

  • HSFProvider(serviceInterface AgnDistributeRuleConfigQueryService.class)public class AgnDistributeRuleConfigQueryServiceImpl implements AgnDistributeRuleConfigQueryService {Overridepublic ResultModel queryAgnDistributeRuleConfigById(String id) {logger.info("queryAgnDistributeRuleConfigById id" id);ResultModel result new ResultModel();if(StringUtils.isBlank(id)){result.setSuccess(false);result.setErrorMsg("id cannot be blank");return result}try {AgnDistributeRuleConfigDto agnDistributeRuleConfigDto new AgnDistributeRuleConfigDto();AgnDistributeRuleConfig agnDistributeRuleConfig agnDistributeRuleConfigMapper.selectById(id);if(agnDistributeRuleConfig null){logger.error("agnDistributeRuleConfig is null");result.setSuccess(false);result.setErrorMsg("agnDistributeRuleConfig is null");return result}this.filterDynamicRule(agnDistributeRuleConfig);BeanUtils.copyProperties(agnDistributeRuleConfig, agnDistributeRuleConfigDto);result.setSuccess(true);result.setTotal(1);result.setValues(agnDistributeRuleConfigDto);} catch (Exception e) {logger.error("queryAgnDistributeRuleConfigById error,", e);result.setSuccess(false);result.setErrorMsg(e.getMessage());}return result;}}

    我们看上面这段代码是一段查询分发规则的业务逻辑。虽然功能能够 work但不规范的地方其实非常多

  • Facade 层定义全部逻辑 - 未做结构分层

  • 业务与技术未做分离 - 耦合接口信息与业务数据

  • Try catch 满天飞 - 缺少统一异常处理机制

  • 没有规范化的日志格式 - 日志格式混乱

  • 但不可否认他一定是当前最快的。这就是战术设计的特点之一永远按当前最快速交付的方案进行推进甚至很多组织鼓励这种工作方式为了使功能更快运作只注重短期收益而忽略长期价值。

    2、战术龙卷风

    631a7e970765cddd1475090d9ccf98d4.png

    Almost every software development organization has at least one developer who takes tactical programming to the extreme: a tactical tornado.译几乎每个软件开发组织都有至少一个将战术编程发挥到极致的开发人员战术龙卷风。

    John Ousterhout 《A Philosophy of Software Design》

    将战术编程发挥到极致的人叫战术龙卷风。战术龙卷风以腐化系统为代价换取当前最高效的解决方案或许他自己并未觉得。战术龙卷风也有如下几个特点

  • 是一位多产的程序员没人比龙卷风更快完成任务

  • 总能留下龙卷风后毁灭的痕迹留给后人去清理

  • 是真的很卷

  • 一些组织甚至会将战术龙卷风视为英雄为什么能干得又多又快因为他将成本放到了未来。软件工程最大的成本在于维护我们每一次代码的改动都应该是对历史代码的一次整理而非单一的功能堆积。龙卷风能赢得现在但终将失去未来而这个失败的未来或许需要全团队与他一起买单。

    3、战略编程

    ced198b63e76bec276718bc3044f9495.png

    John 教授提出与战术编程相对的是战略编程战略编程更注重长期价值不满足于功能 work致力于制作出色的设计以满足对未来扩展的诉求注意不要过度。战略设计有如下 4 个特点

  • 工作代码远远不够

  • 引入不必要的复杂度不可接受

  • 不断对系统设计进行小幅改进

  • 投资心态每位工程师都需要对良好的设计进行连续的少量投资 10~20%

  • John Ousterhout 教授在《A Philosophy of Software Design》书中提到了战略设计与战术设计的总成本投入。随着时间的流逝战略设计可以有效控制软件成本但战术设计会随着时间的推移线性递增。这与 Martin Fowler在《Patterns of Enterprise Application Architecture》这本书中所提的关于数据驱动与领域驱动关于复杂度的治理是同样的含义要致力于长期的价值投资。

    04

    系统的困境与演进

    efca98e081f5414fff3025f8bb9d8ac5.png

    没有系统是天然复杂的为了快速完成任务不断引入新的复杂度至系统逐渐腐化无限增长与无限传递的复杂度让软件需求越来越难“快速完成”。当有一天我们意识到系统的复杂性时再试图通过战略设计进行软件的迭代你会发现举步维艰一处很小的修改需要投入大量的基建修复最终我们不得不向成本低头不断再通过战术设计无限的苟且。

    A condition that is often incorrectly labeled software maintenance. To be more precise, it is maintenance when we correct errors; it is evolution when we respond to changing requirements.

    译我们总是说我们需要“维护”这些老系统。而准确的说在软件发展过程里只有我们修正错误时才是维护在我们应对改变的需求时这是演进。

    Grady Booch 《Object-Oriented Analysis and Design with Applications》

     it is preservation when we continue to use extraordinary means to keep an ancient and decaying piece of software in operation. Unfortunately, reality suggests that an inordinate percent- age of software development resources are spent on software preservation.

    译当我们使用一些极端的手段来保持古老而陈腐的软件继续工作时这是保护苟且。事实证明我们更多的时间是在应对最后一种状况。

    Grady Booch 《Object-Oriented Analysis and Design with Applications》

    如同 Grady Booch 在《Object-Oriented Analysis and Design with Applications》中所提到的观点当我们使用一些极端的手段来保持古老而陈腐的软件继续工作时这确实是一种苟且。我们小心翼翼、集成测试、灰度发布、及时回滚等等我们没有在“维护”他们而是以一种丑陋的方式让这些丑陋的代码继续能够成功苟且下去。当代码变成意大利面条时将几乎是不可能修复成本将成指数倍增长并且似乎我们的系统已经存在这样的代码并且可能还在持续增加中。

    a6658ad6217123e4346f3c5a75cd8342.png

    06

    架构伪论

    Aliware

    在架构设计中总有一些软件工程师所坚信的诗和远方但到不了的乌托邦不一定就是遥不可及的美好圣地实则也可能是对系统无益甚至有害的架构设计。这里列举其中 2 条可能存在的架构伪论。

    01

    好的代码自解释

    Comments do not make up for bad code译注释不是对劣质代码的补救

    Martin Fowler 《Clean Code》

    Martin Fowler 在《Clean Code》书中提到注释不是对劣质代码的补救以前我也一直坚信如果代码足够好是不需要注释的。但实则这是一个伪命题John 教授这么评价它‘good code is self-documenting’ is a delicious myth。

    /*** 批量查询客户信息*/public List queryCustomerList(){// 查询参数准备UserInfo userInfo context.getLoginContext().getUserInfo();if(userInfo null || StringUtils.isBlank(userInfo.getUserId())){return Collections.emptyList();}LoginDTO loginDTO userInfoConvertor.convert(userInfo);// 查询客户信息List customerSearchVOList customerRemoteQueryService.queryCustomerList(loginDTO);Iterator it customerSearchVOList.iterator();// 排除不合规客户while(it.hasNext()){CustomerSearchVO customerSearchVO it.next();if(isInBlackList(customerSearchVO) || isLowQuality(customerSearchVO)){it.remove();}}// 补充客户其他属性信息batchFillCustomerPositionInfo(customerSearchVOList);batchFillCustomerAddressInfo(customerSearchVOList);return customerSearchVOList;}

    这段代码我们可以很轻松的在 5 秒内看明白这个函数是做什么的并且知道它内部的一些业务规则。无限的私有方法封装会让代码链路过深无限类的拆解会造成更多网状依赖至少有 3 点内容让我们绝不能抛弃注释。

  • 无法精准命名命名的含义是抽象实体隐藏细节我们不能在一个名字上赋予它全部的信息而必要的注释可以完美的进行辅佐。

  • 设计思想的阐述代码只能实现设计不能阐述设计这也是为什么一些复杂的架构设计我们需要文档的支撑而非代码的‘自解释’在文档与代码之间的空隙由注释来填补。

  • 母语的力量这点尤其适合我们中国人有时并不是因为注释少代码多所以我们下意识会首先看代码。而是我们几十年感受的文化让我们对中文与ABC具有完全不一样的感观。

  • 02

    永远追求最优雅

    雷布斯曾自夸自己写的代码像诗一样优雅追求优雅的代码应该是每个软件工程师的心中的圣地。但有时存在一些不优雅存在一些‘看似不合理’并不代表就不对反而有时在追求更优雅的路上我们持续跑偏。

    The goal of software architecture is to minimize the human resources requiredto build and maintain the required system.译软件架构的终极目标是用最小的人力成本来满足构建和维护该系统的需求。

    Robert C.Martin 《Clean Architecture》

    Robert C.Martin 在《Clean Architecture》一书中提到了架构终极目标用最小的人力成本来满足构建和维护该系统的需求。架构始终是我们解决复杂度的一个工具如果当前系统并不复杂我们不需要为了所谓的优雅去过分改造与优化它持续将成本置在一个较低水位就是软件最好的解决办法。

    业务简单的系统不应用 DDD 架构弱交互场景也无需进行前后端分离哪怕是邓总设计师在规划新中国的发展上也是制定了一套‘中国特色社会主义’制度。不要盲从一些教条的观念选择适合自己的控制在可控制范围内既不过度也不缺失。毕竟没有绝对的优雅甚至没有绝对的正确。

    07

    写在最后

    Aliware

    很多人认为做业务开发显得没那么有挑战性但其实正好相反。最难解决的 bug 是无法重现的 bug最难处理的问题域是不确定性的问题域。业务往往是最复杂的面向不确定性设计才是最复杂的设计。软件工程学科最难的事情是抽象因为它没有标准、没有方法、甚至没有对错。如何在软件固有的复杂性上找到一条既不过度也不缺失的路是软件工程师的终身课题或许永远也无法达到或许我们已经在路上了。

     -END- 


    大家在看

    1.一位38岁被裁技术经理的忠告

    2.这家公司太恶心了预测离职倾向

    3.滴滴股票延期解禁我损失6000万

    4.如何用敏捷搞垮一个团队

    5.为什么CTO不写代码还这么牛逼

    6.如何快速降低一个员工的积极性

    上一篇:从Docker到Kubernetes
    下一篇:没有了
    网友评论