为什么开发软件装起来那么复杂,开发一个软件有那么容易吗

首页 > 实用技巧 > 作者:YD1662023-04-22 16:22:53

计算方法

计算公式1:V(G)=e-n 2。其中,e表示控制流图中边的数量,n表示控制流图中节点的数量。

计算公式2:V(G)=区域数=判定节点数 1。圈复杂度所反映的是“判定条件”的数量,所以圈复杂度实际上就是等于判定节点的数量再加上1,也即控制流图的区域数。

计算公式3:V(G)=R。其中R代表平面被控制流图划分成的区域数。

举个例子,以前面AB实验的代码片段为例子,画出流程图如下,通过计算得出其圈复杂度为4:

为什么开发软件装起来那么复杂,开发一个软件有那么容易吗(5)

流程图

2.3 John Ousterhout的复杂度定义

John Ousterhout(约翰欧斯特霍特),在他的著作《A Philosophy of Software Design》中提出,软件设计的核心在于降低复杂性。他选择从认知的负担和开发工作量的角度来定义软件的复杂性,并且给出了一个复杂度量公式:

子模块的复杂度乘以该模块对应的开发时间权重值,累加后得到系统的整体复杂度C。系统整体的复杂度并不简单等于所有子模块复杂度的累加,还要考虑开发维护该模块所花费的时间在整体时间中的占比(对应权重值)。也就是说,即使某个模块非常复杂,如果很少使用或修改,也不会对系统的整体复杂度造成大的影响。

三、如何避免复杂度问题

软件复杂度问题可以完全避免么?我觉得不可能,但是这并不能成为我们忽视软件复杂度的理由,有很多措施可以帮助我们尽量避免自身的需求开发或工作中引入问题代码而导致软件复杂。这里结合日常的开发理解谈一下自己的认知:

1)开发前:我们可以通过需求梳理沉淀需求分析、架构设计等文档作为知识传递的载体。

2)开发中:我们需要强化系统架构理解,战略优先于战术,系统分层架构清晰统一,开发中接口设计要做到高内聚和低耦合同时保持良好代码注释的习惯。

3)维护阶段:我们可以进行代码重构,针对之前存在设计问题的代码,以新的思维和架构实现方案进行重构使得代码越来越清晰。

3.1 战略先于战术

在战术编程中,开发者主要关注点是能够work,比如修复一个bug或者增加一段兼容逻辑。乍一看,代码能够work,功能也得到了修复,然而,战术编程已经为系统设计埋下了坏的味道,只是还没人察觉,当相同的代码交接给后人的时候,经常会听到一句“屎山一样的代码”,这就是以战术编程长期累积的结果,是短视的,缺乏宏观设计导致系统不断的引入复杂性问题以至于代码很容易变得隐晦。

成为一名优秀的软件设计师的第一步是认识到仅仅为了完成工作编写代码是不够的。为了更快地完成当前的任务而引入不必要的复杂性是不可接受的。最重要的是这个系统的长期结构。

--John Ousterhout(约翰欧斯特霍特),《A Philosophy of Software Design》

目前我们所维护的系统往往都是在前人代码的基础上进行升级和扩展,日常需求开发工作中,一个重要的工作是借助需求开发的契机,推动需求所涉及到坏味道的设计能够面向未来扩展,而非仅仅着眼于完成当前的需求,这就是我理解的战略编程

举一个例子,有一个消息监听的处理逻辑,根据不同的业务执行对应的业务处理,其中一部分关键代码如下,可以猜想按照战术编程的思路以后会还会有无数的else if在后面进行拼接实现,而这里完全可以通过策略模式的方式进行简单的重构,使得后续业务接入时更加清晰和简单。

public void receiveMessage(Message message, MessageStatus status) { // ..... if(StringUtils.equals(authType, OnetouchChangeTypeParam.IC_INFO_CHANGE.getType()) || StringUtils.equals(authType, OnetouchChangeTypeParam.SUB_COMPANY_CHANGE.getType())){ if(StringUtils.equals("success", authStatus)){ oneTouchDomainContext.getOneTouchDomain().getOnetouchEnableChangeDomainService().notifySuccess(userId.toString(), authRequestId); } } else if(StringUtils.equals(authType,AUTH_TYPE_INTL_CGS_ONSITE)){ // XXXXXX } else if(StringUtils.equals(authType,AUTH_TYPE_INTL_CGS_ONSITE_CHANGE)) { // XXXXXX } else if (AUTH_TYPE_VIDEO_SHOOTING.equals(authType)) { if (AUTH_STATUS_SUCCESS.equals(authStatus)) { // XXXXXX } else if (AUTH_STATUS_PASS.equals(authStatus)) { // XXXXXX } else if (AUTH_STATUS_SUBMIT.equals(authStatus)) { // XXXXXX } } // ..... }

短期来看战略编程的成本会高于战术编程,但是从上面的案例长期来看,这样的成本是值得的,他能够有效的降低系统的复杂度,从而长期来看最终能降低后续投入的成本。开发同学在需求迭代的过程中应该先通过战略编程的思维进行设计和思考,然后再进行战术实现,所以我的观点是战略设计要优先于战术实现。

为什么开发软件装起来那么复杂,开发一个软件有那么容易吗(6)

3.2 高内聚低耦合设计

高内聚低耦合,是判断软件设计好坏的标准,主要用于程序的面向对象的设计,主要看类的内聚性是否高,耦合度是否低。目的是使程序模块的可重用性、移植性大大增强。通常程序结构中各模块的内聚程度越高,模块间的耦合程度就越低,当模块内聚高耦合低的情况下,其内部的腐化问题不容易扩散,从而带给系统本身的好处就是复杂度的降低。

内聚是从功能角度来度量模块内的联系,好的内聚模块应当做好一件事情,它描述了模块内部的功能联系;而耦合是软件结构中各模块之间相互连接的一种度量,耦合强弱取决于模块间接口的依赖程度,如调用一个模块的点以及通过接口的数据等。那么如何实现一个高内聚低耦合的接口呢?

3.2.1 简化接口设计

简单的接口往往意味着调用者使用更加方便,如果我们为了实现简单,提供一个复杂的接口给外部使用者,此时往往带来的是耦合度增大,内聚降低,继而当该接口出现升级等场景时会产生修改扩散的问题,进而影响面发生扩散,带来一定的隐患。

因此,在模块设计的时候,要尽量遵守把简单留给别人,把复杂留给自己的原则。

比如这样一个例子,下面两段代码实现的是同样的逻辑,方法一的设计明显要由于方法二,为什么?方法一更简单,而方法二明显违背了把简单留给别人,把复杂留给自己的原则。如果你是接口的使用者当你使用方法二的时候,你一定会遇到的两个问题:第一,需要传递哪些参数要靠问方法的提供方,或者要看方法的内部实现;第二,你需要在取到返回值后从返回值中解析自己想要的结果。这些问题无疑会让系统复杂度提升。

所以,我们要简化接口设计,把简单留给别人,把复杂留给自己,从而保证接口的高内聚和低耦合,进而降低系统的复杂度。

@Override public boolean createProcess(StartProcessDto startProcessDto) { // XXXXXXX } ​ @Override public HashMap createProcess(HashMap dataMap) { // XXXXXXX }3.2.2 隐藏实现细节

隐藏细节指的就是只给调用者暴露重要的信息,把不重要的细节隐藏起来。接口设计时,我们要通过接口告诉使用者我们需要哪些信息,同时也要通过接口告诉使用者我会给到你哪些信息,至于内部如何实现是使用者不需要关心的。

还是以上面的接口的实现为例,方法一对内部实现细节达到了屏蔽,使得当前接口具备更好的内聚性,当内部实现的服务需要调整时只需要修改内部的实现即可,而方法二则不然。通过这个案例也能够实际体会到,把内部的实现细节隐藏在实现方的内部能够有效的提升接口的内聚性降低系统耦合,随之带来的是系统复杂度的降低。

@Override public boolean createProcess(StartProcessDto startProcessDto) { Validate.notNull(startProcessDto); try { HashMap<String, Object> dataMap = new HashMap<>(8); dataMap.put(MEMBER_ID, startProcessDto.getMemberId()); dataMap.put(CUSTOMER_NAME, startProcessDto.getCustomerName()); dataMap.put(GLOBAL_ID, startProcessDto.getGlobalId()); dataMap.put(REQUEST_ID, startProcessDto.getAvRequestId()); String authType = startProcessDto.getAuthType(); String taskCode = getTaskCode(authType); ​ HashMap resultMap = esbCommonTaskService.createProcess(AV_ORIGIN_AV, taskCode, dataMap); return (MapUtils.isNotEmpty(resultMap) && TRUE.equals(resultMap.get(IS_SUCCESSED))); } catch (Exception e) { LOGGER.error("createProcess error. startProcessDto:{}", JSON.toJSONString(startProcessDto), e); throw e; } } ​ @Override public HashMap createProcess(HashMap dataMap) { Validate.notNull(dataMap); try { HashMap process = esbCommonTaskService.createProcess(ORIGIN_AV, TASK_CODE, dataMap); return process; } catch (Exception e) { LOGGER.error("createProcess error. dataMap:{}", JSON.toJSONString(dataMap), e); throw e; } }3.2.3 通用接口设计

通用接口设计并不是说所有的场景都为了通用而设计,而是针对具有同样能力的多套实现代码而言,我们可以抽取成通用的接口设计,通过业务类型等标识区分实现一个接口完成。

举一个例子,有一个需求是同时实现多种会员的权益列表功能,由于不同会员的权益并不完全相同,所以刚开始的想法是分开设计不同的接口来承接不同会员的权益内容的获取,但是本质上实现的是同样的内容:查询会员权益,所以最终通过对领域模型的重构抽取了统一的模型从而实现了通用的权益查询的接口。

public List<RightE> getRights(RightQueryParam rightQueryParam) { // 参数校验 checkParam(rightQueryParam); Locale locale = LocaleUtil.getLocale(rightQueryParam.getLocale()); // 查询商家权益 RightHandler rightHandler = rightHandlerConfig.getRightHandler(rightQueryParam.getMemberType()); if (rightHandler == null) { log.error("getRightHandler error, not found handler, rightQueryParam:{}", rightQueryParam); throw new BizException(ErrorCode.NOT_EXIST); } List<RightE> rightEList = rightHandler.getRights(rightQueryParam.getAliId(), locale); return rightEList; }3.3 分层架构

从经典的三层架构到领域驱动设计都有涉及到分层架构,分层架构的核心其实我的理解是隔离,将不同职责的对象划分到不同的层中实现,良好的分层能够实现软件内部复杂度问题的隔离,降低“洪泛”效应。

端口适配器架构将系统划分为内部(业务逻辑)和外部(客户请求/基础设施层/外部系统)。主动适配器(Driving adapters)承接了外部请求,系统内部业务逻辑能对其进行主动适配,独立于不同的调用方式提供通用的接口。被动适配器(Driven adapters)承接了内部业务逻辑调用外部系统的诉求,为了避免外部系统污染内部业务逻辑,通过适配屏蔽外部系统的底层细节,有利于内部业务逻辑的独立性。

在复杂软件的开发过程中,很容易出现分层的混淆,逐渐出现分层不清晰,系统业务逻辑和交互UI/基础设施等代码逻辑逐渐耦合,导致业务逻辑被污染的问题,而端口适配器正是要解决该类问题。

为什么开发软件装起来那么复杂,开发一个软件有那么容易吗(7)

六边形架构

Onion Architecture(洋葱架构,于2008年)由杰弗里 · 帕勒莫提出,洋葱架构是建立在端口适配器架构的基础上,将领域层放在应用的中心,外部化UI和基础设施层(ORM,消息服务,搜索引擎)等,更进一步增加内部层次划分。洋葱模型将应用分层细化,抽取了应用服务层、领域服务层、领域模型层等,并且也明确了应用调用依赖的方向:

为什么开发软件装起来那么复杂,开发一个软件有那么容易吗(8)

上一页123下一页

栏目热文

文档排行

本站推荐

Copyright © 2018 - 2021 www.yd166.com., All Rights Reserved.