软件系统复杂性治理方法
本文讨论了软件复杂性及其产生原因,介绍了如何度量软件复杂性,及 SOLID 软件设计原则,并探讨管理复杂性的方法,包括使用代码重构、设计模式、领域驱动设计等。通过遵循这些原则和方法,开发人员可以降低软件复杂性,提高代码质量和可维护性。这篇文章内容涵盖了软件开发的道与术,希望能对你所有帮助,欢迎评论交流~
- 什么是软件复杂性
- 软件复杂性产生原因
- 如何度量软件复杂性
- SOLID 软件设计原则
- 管理复杂性的方法
什么是软件复杂性
软件系统复杂性指的是系统内部组件、模块、包、类、方法之间的交互关系以及整体设计的复杂程度。这种复杂度可能源自于多方面因素,包括但不限于代码规模、结构的混乱程度、各个模块之间的耦合度、算法的复杂性以及系统中存在的条件分支和循环等。
系统复杂度的高低直接影响着软件的可理解性、可维护性和可扩展性。高复杂度的系统通常意味着更难以理解和修改,也更容易引入错误。此外,复杂度过高还会增加软件开发和维护的成本。
所以说理解和管理软件系统复杂度至关重要。通过采用适当的设计原则、模式和工程实践,以及持续的重构和优化,可以有效地控制和降低软件系统的复杂度,从而提高系统的可维护性、可理解性和可靠性。
以下是两个相同功能的示例代码段,通过对比可以观察下复杂性差异:
多层嵌套条件语句
1 | public void processOrder(Order order) { |
通过早期返回重构
1 | public void processOrder(Order order) { |
上面两段代码,通过对比可以发现第一段代码展示了典型的深度嵌套条件语句,可读性差、扩展性差。第二段重构后的代码使用了早期返回的方式,将每个条件检查分开处理,遇到不满足条件的情况就提前返回错误消息。这样可以减少嵌套的条件语句,提高代码的可读性和可维护性。
第二段代码是对第一段代码的改进,但也是存在一定的可读性和扩展性差的问题的,提前返回是一种断路思考方式,不利于记忆,如果方法比较长,或者后期叠代码使方法变得很长,是不太容易梳理出”什么情况下会执行订单处理操作”,你需要记住各种断路情况。
系统复杂性产生的原因
复杂性是系统的固有属性,它来源于系统的规模、结构、功能、行为等多个方面,有外在和内在两方面原因,下面列举几点:
需求变更
随着时间的推移,产品需求会不断变化。这些变化可能需要对现有系统进行修改或添加新功能,从而增加了软件系统的复杂性。
技术选型
选择不合适的技术栈或架构模式可能会导致系统的过度复杂化。有时为了解决一个小问题可能会引入大量不必要的技术组件,使系统变得更加复杂。
规模扩大
随着业务的发展,软件系统可能需要处理更多种类的数据和用户,这会导致系统规模的扩大,系统的元素和关系会随着规模的增大而增多。
不完善的设计
缺乏清晰的系统设计和架构规划可能导致系统出现混乱和复杂性。如果最初的设计没有考虑到系统的未来发展,系统将很快变得难以管理和理解。
巨著《人月神话》中提出了两个重要概念:
- 本质复杂度:是指由软件系统所需解决的问题本身所固有的复杂性。它是由问题的本质属性和要求所决定的,与软件实现的细节无关。
- 偶然复杂度:是指由软件实现过程中引入的额外复杂性。它是由设计决策、技术选择、代码结构等因素所导致的。
偶然复杂度不是待求解问题的本质,相对而言, 本质复杂度和待求解问题的本质有关,是无法避免的。偶然复杂度一般是在选用求解问题的方法时所引入的。上面列举的四点,其中技术选型不当和不完善的设计都是因为开发人员经验和预判不足而产生的,属于偶然复杂度;而需求变更和规模扩大则是待求解问题逐渐变多变复杂而产生的,属于本质复杂度。
如何度量软件复杂性
之前写过一篇简单介绍过 软件架构与系统复杂性,下面主要介绍软件系统复杂度度量方式。
圈复杂度(Cyclomatic Complexity)
圈复杂度是一种用来衡量代码复杂性的指标,它通过计算代码中独立路径的数量来评估代码的复杂程度。通俗地说,圈复杂度越高,代码的可读性和维护性就越差。
时间空间复杂度
时间复杂度是用于衡量程序在执行过程中所需的时间资源的多少,而空间复杂度则衡量程序在执行过程中所需的内存资源的多少。
代码行数
代码行数是衡量软件规模和复杂度的一种指标。通常情况下,代码行数越多,系统的复杂度也越高。然而,这并不是绝对的,因为有时候简洁的代码可能实现了复杂的功能。
嵌套层数
嵌套层数指的是代码中条件语句、循环语句和函数调用的嵌套深度。如果嵌套层数过多,会导致代码逻辑混乱,增加代码的理解和维护难度。
组件的相互依赖关系
软件系统中各个组件之间的相互依赖关系也是衡量复杂度的重要标准。如果组件之间的依赖关系错综复杂,那么系统的修改和扩展将变得困难。
SOLID 软件设计原则
通常来说,要想构建一个好的软件系统,应该从写整洁的代码开始做起。毕竟,如果建筑所使用的砖头质量不佳,那么架构所能起到的作用也会很有限。反之亦然,如果建筑的架构设计不佳,那么其所用的砖头质量再好也没有用。SOLID 是一组软件设计原则,旨在帮助开发人员设计可维护、可扩展和易于理解的软件架构。下面简要介绍每个原则:
单一职责原则(Single Responsibility Principle,SRP):一个类应该只有一个引起它变化的原因。这意味着一个类应该只负责一项明确定义的职责或功能,这样可以使类更加内聚,易于理解和修改。
开放封闭原则(Open-Closed Principle,OCP):软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。这意味着在修改现有代码时,应该通过扩展现有代码的行为来实现变化,而不是直接修改已有的代码。
里氏替换原则(Liskov Substitution Principle,LSP):子类应该能够替换其父类并且不会破坏系统的正确性。这意味着子类应该能够在不改变程序正确性的前提下,替代父类的行为。这样可以确保代码的可靠性和可扩展性。
接口隔离原则(Interface Segregation Principle,ISP):客户端不应该依赖于它不需要的接口。这意味着接口应该尽量小而专注,而不是大而笼统。通过定义精确的接口,可以避免客户端依赖无关的接口,提高系统的灵活性和可维护性。
依赖倒置原则(Dependency Inversion Principle,DIP):高层模块不应该依赖于低层模块,而是应该依赖于抽象。这意味着应该通过抽象来解耦模块之间的依赖关系,使得系统更加灵活和可扩展。
这些原则共同强调了代码的高内聚性、低耦合性和可扩展性。遵循这些原则可以提高代码的可维护性、可测试性和可重用性,从而使软件架构更加健壮和可靠。你可能会说道理我都懂但是做不到,王阳明在《传习录》中说,“未有知而不行者,知而不行,只是未知”,如果不能在开发中深切体会这些原则的精髓那便是不懂,是需要在日常开发中不断思考和体会的。
控制和管理复杂性的方法
如果你读到这里,说明看过了很多 “大泥球” 代码,想找到其中的破解之道,下面会抽丝剥茧,介绍一些方式方法。
“大泥球”(Big Ball of Mud)是指一种缺乏清晰结构和良好架构的代码,通常随着时间推移不断添加功能和修复问题而产生。还是以上面的代码为例,展示一个可能被称为”大泥球”代码的案例:
1 | public void processOrder(Order order) { |
如果要增加一个判断订单是否为礼品订单的处理逻辑,最直接的方式是在其基础上继续嵌套更多的条件判断。久而久之这样的“大泥球”代码就存在多层嵌套的条件判断,逻辑复杂、难以理解和维护。
小重构
对于超大型的方法和类,最简单的、较低风险的方式是拆分方法和类,之前写过一篇小文章 从小重构说起。对于方法和类的拆分,可以借助 IDE 来实现,这样可以进一步降低风险;对于静态变量需要提取到公共配置类;通常来说业务方法大都是无状态的,对于有状态方法需要谨慎操作。
要提高上面方法的可读性和可维护性,可以将其拆分成更小的方法。下面是重构后的代码:
1 | public void processOrder(Order order) { |
重构后的代码中,我们将原来的单个方法拆分成了多个方法。processOrder
方法负责处理整个订单流程的控制,进行基本的前置条件检查,然后根据订单类型(礼品订单或非礼品订单)调用相应的处理方法。
processStandardOrder
方法用于处理非礼品订单,而 processGiftOrder
方法用于处理礼品订单。这样,我们可以在具体的处理方法中添加更多的逻辑,而不会让整个代码过于复杂。同时,通过使用早期返回,遇到不满足条件的情况就会提前返回错误消息,避免了过多的嵌套条件。
另外,我们还引入了一个辅助方法 sendErrorMessage
,用于发送错误消息,避免了重复的代码。
这样的重构使得代码结构更加清晰,逻辑更易于理解和维护。每个方法负责一个具体的任务,代码的可读性和可维护性都得到了提升。
设计模式
随着需求发生变化,不同状态的订单需要有不同的处理方式,当当前的方法不再满足现状时,就需要进行扩充,复杂度也会相应提升。为了进一步应对复杂度的提升,可以考虑适合的设计模式。
设计模式通过提供可重用的解决方案,帮助我们管理软件复杂度。它们提供了一种通用的、经过验证的方法来解决常见的软件设计问题,使得系统更易于理解、扩展和修改。同时,设计模式也促进了代码的重用和降低了系统的耦合度,从而提高了软件的质量和可维护性。
以上面的例子,要根据不同的订单状态生成不同的后续处理行为,可以使用策略模式来表达这一设计。策略模式允许定义一系列算法(策略),将它们封装在独立的策略类中,并使得它们可以互相替换。
下面是使用策略模式来重构的代码示例:
首先,定义一个接口 OrderProcessingStrategy
,表示订单处理策略,其中包含一个 processOrder()
方法来执行订单处理操作:
1 | public interface OrderProcessingStrategy { |
然后,实现不同的订单处理策略,每个策略都实现 OrderProcessingStrategy
接口,并根据订单状态执行相应的处理操作:
1 | public class PaidShippedOrderStrategy implements OrderProcessingStrategy { |
接下来,在 Order
类中添加一个 process()
方法,用来触发订单的处理操作。在该方法中,根据订单状态选择相应的策略,并调用策略的 processOrder()
方法来执行处理操作:
1 | public class Order { |
最后,在客户端代码中,我们可以创建订单对象,并根据订单状态设置相应的处理策略。通过调用订单对象的 process()
方法来触发订单的处理操作:
1 | public class Client { |
通过使用策略模式,我们可以将不同状态的订单处理逻辑解耦,使得每个策略类负责自己的处理操作。这样,可以更灵活地扩展和修改不同订单状态的处理行为,同时避免了原始代码的大泥球结构。
领域驱动设计
从电商交易流程上来说有以下简单几步:
- 用户在商品详情页面下单。
- 下单后用户进行支付。
- 支付后商家发货给用户。
- 用户收货后确认完成订单。
- 订单详情页展示订单、支付、物流信息。
这个处理流程需要一整个系统的支持才能实现。其中每一步执行后都需要有后继行为和通知,这些通知可能是以站内的、短信的方式触达用户,或者通过一些机制发送给下游系统。这时候就涉及到一个决策,哪些是我交易系统的核心能力,哪些是外围能力。如果前期缺乏良好的架构设计,有可能演变成一个”大泥球”系统。
为了避免系统的无序性演变,可以通过领域驱动设计的思想,识别交易领域核心行为,保护领域内部行为不被侵蚀,及领域内部行为是不变或者少变的。
领域驱动设计提倡将软件系统划分为不同的层次,以便更好地组织和解耦系统的各个部分。在DDD中,常用的四层架构和对应职责如下:
用户界面层(User Interface Layer):
- 负责展示商品详情页面,并接收用户的下单请求。
- 在下单后展示订单、支付和物流信息。
应用层(Application Layer):
- 接收用户界面层的请求,并进行必要的参数验证。
- 调用领域层的服务来处理下单、支付、发货和确认收货等操作。
- 提供查询服务,以获取订单、支付和物流信息。
领域层(Domain Layer):
- 定义订单(Order)实体,包含订单号、商品信息、支付信息、物流信息等属性,并处理与订单相关的业务逻辑。
- 实现下单、支付、发货和确认收货等操作的领域服务(OrderService)。
- 使用领域事件(Domain Event)来处理订单状态的变化,例如支付成功、发货操作等。
基础设施层(Infrastructure Layer):
- 实现与外部系统的交互,如支付服务、物流服务等。这些可以使用外部API或模拟实现。
- 提供持久化机制,用于存储订单、支付和物流信息。可以使用数据库或其他适合的持久化方式。
下面是一个示意性的代码结构,用于展示不同层级及其职责:
1 | - xxx-order-app |
在这个设计中,每个层级都有不同的职责和角色,以实现更好的代码结构和可维护性。用户界面层负责展示页面和处理用户输入,应用层负责协调各个领域服务的调用,领域层负责处理业务逻辑,基础设施层负责与外部系统的交互和数据持久化。
这样的设计可以更好地组织代码,使不同的职责分离,减少了耦合性,并且便于扩展和修改。同时,通过领域驱动设计,我们能够更好地表达业务领域的概念和规则,使代码更加贴近业务需求。
**用户界面层 (Presentation Layer)**:
1 | public class OrderDetailPage { |
**应用层 (Application Layer)**:
1 | public class OrderApplicationService { |
**领域层 (Domain Layer)**:
1 | public class Order { |
**基础设施层 (Infrastructure Layer)**:
1 | public interface OrderRepository { |
从上述代码中我们可以看到,基础设施层和领域层设计符合依赖倒置原则,从调用关系看领域层调用基础设施层进行数据的交互,而从依赖关系来看,领域层依赖于领域抽象,不依赖于具体实现,DDD 的精髓在于保护核心领域的自治性,降低层间的偶合度,同时有助于遵循单一职责原则,关注领域核心行为的管理和维护。
微服务架构
这篇文章在这里第一次直面架构,讨论的问题是软件架构(architecture)究竟是什么?从我的经验总结,是站在更高的层次去整体分析软件系统,抓大放小,把握重点,重点是组织结构,而不论是子系统、模块、包,还是分层、服务等,都可以看作为一个”构件”,需要关注的是如何组织使整体高效有序。
如果你要想理解它,可以从设计者的角度去审视,上面的方法从小到大逐层递进地讲了代码的组织形式,后面还要面临更多的复杂性问题,如当用户达到千万级规模,程序如何高效部署和管理,多人协作开发时如何做到高效。
微服务架构是一种软件架构风格,它将一个大型应用程序拆分为一组小型、独立的服务,每个服务都有自己的业务功能,并通过轻量级的通信机制进行交互。每个服务都可以独立开发、部署和扩展,从而提供了灵活性、可伸缩性和可维护性。有下面几个特点:
拆分与自治性:应用程序被拆分为多个小型服务,每个服务关注于特定的业务功能。每个服务都是自治的,可以独立开发、部署和运行,使团队可以并行开发和部署不同的服务。
独立部署和扩展:由于每个服务都是独立的,可以根据需求独立部署和扩展。这种灵活性使得系统能够更好地应对高负载和变化的需求,同时减少了对整个应用的影响。
技术多样性:微服务架构允许使用不同的技术栈和编程语言来实现不同的服务。这使得团队可以选择最适合其需求的技术,提高开发效率和灵活性。
弹性和容错性:由于每个服务都是独立的,当一个服务出现故障时,其他服务仍然可以正常运行,从而提高系统的弹性和容错性。
松耦合和可维护性:微服务通过轻量级的通信机制(如RESTful API或消息队列)进行交互,服务之间的耦合度较低。这使得系统更易于理解、修改和维护。
团队自治和快速交付:每个服务都可以由独立的团队负责开发和维护,团队可以根据自己的需求和进度进行快速交付。这种团队自治的方式促进了敏捷开发和持续交付的实践。
然而,微服务架构也带来了一些挑战,如服务间通信的复杂性、分布式事务管理、服务发现和监控等。在采用微服务架构时,需要仔细权衡利弊,并根据具体的业务需求和团队能力做出决策。微服务更多是关于组织和团队,而不是技术。
这里必须要谈一下康威定律:Conway’s law: Organizations which design systems[…] are constrained to produce designs which are copies of the communication structures of these organizations.
(设计系统的组织,其产生的设计和架构等价于组织间的沟通结构。)
简单来说,这意味着一个组织的沟通和组织结构会直接影响到所开发的软件系统的结构。
组织结构:组织内部的团队结构、沟通渠道和决策层级等因素会直接影响到软件系统的设计。
沟通结构:组织内部团队之间的沟通方式和频率会反映在系统设计中。如果团队之间的沟通不畅或存在壁垒,那么系统的设计可能会反映出这种分隔和隔离。
系统结构:根据康威定律,软件系统的结构往往会与组织结构相似。如果组织结构是分散的,那么系统的结构可能会呈现出分散的特征;如果组织结构是集中的,那么系统的结构可能会呈现出集中的特征。
康威定律的应用意义在于,通过理解组织结构和沟通结构对系统设计的影响,可以更好地规划和调整组织结构,以促进系统的设计和开发。例如,如果希望实现松耦合和模块化的系统,可以通过优化团队之间的沟通和协作方式来达到这个目标。
总结
这篇文章整理了近几年的关于治理系统复杂性的一些经验,主要包括概念介绍、度量方式、设计原则、治理方法几个方面去介绍。其中治理方法部分由浅入深的介绍了几个方式,不同方式面临的问题复杂程度也是不同的。这些方式通过不同的角度帮助我们管理软件复杂度,提高代码的可维护性和可扩展性,确保软件系统与业务需求紧密结合。管理软件复杂性是软件开发过程中非常重要的一环,不论面对何种问题,“简单,易于理解”都应该是我们要坚持的方向。