
送《技术方案设计参考模板》
1 引言
在现代软件开发中,系统的复杂性不断增加,这不仅体现在功能需求的多样性上,也反映在代码的组织和管理上。从系统工程的角度来看,软件系统由多个元素及其相互关系组成,这使得理解和管理其复杂性成为一项重要任务。复杂性可分为本质复杂性和偶然复杂性:本质复杂性源自问题本身的固有特性,而偶然复杂性则由实现技术和工具的选择引入。为了有效应对这些复杂性,开发者需要在架构设计中充分考虑结构、行为、认知和演化等多个维度的复杂性,采用合适的架构模式和设计原则来提升系统的可维护性和可扩展性。本文将探讨如何通过架构设计和代码优化来管理软件系统的复杂性,并提出编写“好”代码的建议。
2 软件系统的复杂性与代码
在系统工程的视角下,任何系统都由元素及其相互关系构成,软件系统也不例外。这种构成方式帮助我们理解软件系统的复杂性,从而更好地进行设计和管理。
复杂性可以分为两种类型:本质复杂性和偶然复杂性。前者是软件系统固有的,源自于要解决的问题本身的复杂性,与系统的功能需求紧密相连,因此不可避免。而偶然复杂性则是由实现系统时所使用的工具、技术和过程引入的。这种复杂性并不属于问题本身,而是由于不完善的工具、语言或开发方法导致的。例如,选择过于复杂的框架或不必要的技术栈可能会增加偶然复杂性。
软件系统的复杂度可以从以下几个维度来分析:
1.结构复杂性(Structural Complexity):与系统的架构和代码组织有关。模块化设计、组件间的耦合度、系统的层次结构等都会影响结构复杂性。通过良好的架构设计,可以有效减少这种复杂性。
2.行为复杂性(Behavioral Complexity):指系统在运行时的表现,包括状态管理、并发处理和事件驱动等特性。复杂的业务逻辑和状态转换往往会增加行为复杂性。
3.认知复杂性(Cognitive Complexity):涉及开发者在理解和修改软件系统时遇到的困难。清晰的代码、良好的文档和简单的接口设计等都可以降低认知复杂性。
4.复杂性(Evolutionary Complexity):随着软件系统的演化和扩展,复杂性往往增加。支持灵活的扩展和维护能力是减少演化复杂性的关键。
结构复杂性与代码的组织方式密切相关。良好的模块化设计、清晰的代码结构、适当的封装和接口定义都有助于降低结构复杂性。高耦合和低内聚的代码结构会增加结构复杂性,使得代码难以理解和维护。行为复杂性体现在代码实现的动态特性上,例如状态管理、并发处理和事件驱动机制。复杂的业务逻辑和状态转换会导致代码难以调试和测试。通过设计简单明了的状态机、使用合适的并发控制机制可以降低这种复杂性。认知复杂性直接影响开发者对代码的理解和修改能力。代码的可读性、命名规范、注释和文档等都影响认知复杂性。简洁明了的代码、清晰的命名和良好的文档可以帮助开发者更容易地理解和维护代码。
3 聊聊代码
"谁" 会关注代码?
这可能是一个“简单”的问题,作为研发、架构师、技术经理都会关注系统的代码。不论是进行实际的开发,还是进行代码评审,又或者是对系统进行代码质量、安全、稳定等相关治理工作,都会关注系统的代码实现。
代码不仅仅只有开发人员关注,团队内的其他角色也又可能关注代码,不同干系人关注点不同
满足这些架构属性是 “好” 的代码?
我们回到对代码的探讨,探索一下 “什么是 ‘好’ 的代码?”。对于应用系统本身而言,除了满足功能性需求之外,还要满足特定的架构属性(不同的文章对架构属性有不同的称呼,有些称之为“非功能性需求”,有些称为“质量属性”)。系统的架构属性非常多,涵盖了系统的方方面面,比如常见的架构属性如下图所示:
满足这些架构属性就是 “好” 的代码吗?不一定。本质上,同满足功能需求一样,满足系统架构属性不能完全表明代码是“好”的。
遵循这些设计原则是 “好” 的代码?
遵循设计原则和模式通常有助于创建更好的代码,但这并不保证代码一定是“好”的:
1.过度设计:过于严格地遵循设计模式可能导致过度设计,增加了不必要的复杂性。代码应该根据实际需要来设计,而不是为了遵循原则而设计。
2.上下文忽视:设计原则和模式需要根据具体的项目需求和上下文来应用。盲目地应用这些原则可能导致不适合的解决方案。
3.性能问题:有时,过于追求设计优雅可能会忽视性能需求。某些情况下,需要在设计和性能之间找到平衡。
4.可读性和简洁性:复杂的设计模式可能会降低代码的可读性。简单、直接的解决方案有时更适合项目需求。
5.团队技能和经验:设计模式和原则的有效应用需要团队成员具备相应的知识和经验。如果团队不熟悉这些概念,可能会导致误用或误解。
6.灵活性与稳定性:过于灵活的设计可能导致系统不够稳定,容易受到变化的影响。需要根据项目的稳定性需求进行适当的设计。
回归到代码本身
我们一直在关注系统的安全性、健壮性、扩展性、可维护性等诸多的架构属性,也一直在关注设计模式、SOLID原则、DDD、GRASP等设计原则,但我们有多少精力关注 “代码” 本身?让我们回归到代码和人两个基本要素。
对代码的不同视角
开发编写代码很“容易”,但开发出能够被人容易理解的代码并不容易。Martin Fowler在《重构》一书中强调了代码的“可理解性”。
Richard P. Gabriel 则强调了代码的 “Habitability”(宜居性):代码应该使人们能够理解其结构和意图,并能舒适、自信的对其进行修改。
两个视角都体现了对代码的“理解性”,提出了“Joyful Software Code”:
原则一:可组合(Composable):系统的各个组件可以独立开发、测试,并能够灵活组合以形成更复杂的功能。
原则二:遵循Unix哲学(Unix-Philosophy):Unix哲学强调构建简单的工具,每个工具只做一件事,并通过标准化接口进行组合。通过分而治之的方法,降低系统复杂性,提升系统的可靠性和可维护性。
原则三:可预测(Predictable):可预测性意味着系统行为一致且可预期,输入相同则输出相同。通过减少副作用和状态依赖性,可以提高代码的可预测性。
原则四:惯用性(Idiomatic):指的是使用编程语言或框架中推荐的惯用模式和最佳实践。这意味着开发者在编写代码时,应遵循该语言或框架的特定风格和习惯用法,以提高代码的可读性、一致性和可维护性。
原则五:基于领域(Domain-Based):调在软件设计和开发中紧密关注和反映特定业务领域的需求和特性。这个原则的核心是通过对业务领域的深入理解,将领域知识直接嵌入到软件系统中,以便更好地支持业务目标和流程。
4 应用架构模式
经典的单体分层架构
经典分层架构是一种常见的软件架构模式,通常用于组织和分离应用程序的不同关注点:
1.用户界面层:负责与用户进行交互,展示数据并接收用户输入。这一层通常与用户体验直接相关,需要处理输入验证和界面逻辑。
2.业务层:包含应用程序的核心业务逻辑。它负责处理用户界面层传递的数据,执行应用程序的业务规则和流程。
3.通用服务层:提供业务层和持久层之间的通用服务。它通常用于处理跨业务逻辑的功能,如日志记录、安全性、事务管理等。
4.持久层:负责与数据库进行交互,执行数据的存储和检索操作。它通常包括数据访问对象(DAO)或类似的模式来封装数据库操作。
5.数据存储层:实际存储数据的地方,通常是关系型数据库、NoSQL数据库或文件系统。
分层架构的本质是关注点分离(Separation of Concerns),通过将系统的不同功能模块化,降低了系统的复杂性,提高了系统的灵活性和可维护性。这种架构模式强调每一层只关注其特定的责任,并通过明确的接口与其他层进行交互,从而实现松耦合。
经典分层架构的潜在问题主要是:
•性能开销:由于层与层之间的调用,可能会引入额外的性能开销,尤其是在层次复杂的情况下。
•复杂性:对于简单的应用程序,使用分层架构可能引入不必要的复杂性。
•跨层依赖:如果设计不当,可能会出现跨层依赖,违背了分层架构的初衷。
模块化单体架构
•模块化单体架构将应用程序划分为多个独立的模块,每个模块都包含特定的功能和逻辑:
1.用户界面层:这是应用程序的最上层,负责与用户进行交互。用户通过此层与系统进行操作和数据输入,通常包括网页、移动应用界面或桌面应用界面。
2.模块化设计:图中显示了两个模块:模块A和模块B。每个模块都独立于其他模块,具有自己的功能和逻辑。模块化设计的优点是可以提高代码的可维护性和可扩展性。每个模块可以独立开发、测试和部署。
3.模块内部结构:业务层:负责处理具体的业务逻辑。它根据用户的输入或系统的状态执行相应的操作。通用服务层:提供模块内的公共服务和功能,如数据转换、通讯、日志记录等。这个层次可以被业务层调用以实现业务逻辑。持久层:负责与数据库或其他存储系统进行交互。它处理数据的存储、检索、更新和删除操作。
4.基础设施:这是支持整个应用程序运行的底层设施,包括服务器、网络、数据库、存储等。基础设施为上层的模块和用户界面提供必要的支持和服务。
这种架构的关键在于模块化和层次化的设计,使得应用程序可以更好地适应变化,便于开发团队并行工作,并且可以更容易地进行功能扩展和维护。每个模块的独立性也有助于隔离故障,确保系统的稳定性和可靠性。
模块化单体架构可能存在的问题是:复杂的依赖管理、部署复杂性、资源共享问题、升级和扩展的限制、潜在的模块间耦合
经典DDD四层架构
端口-适配器架构
这张图展示了经典的分层架构,具体是“六边形架构”(也称为“端口与适配器架构”)。这种架构模式旨在通过将应用程序的核心逻辑与外部系统的交互隔离开来,提高系统的可测试性和可维护性:
1.领域层(Domain Layer):这是架构的核心部分,包含了应用程序的业务逻辑和规则。领域层不依赖于任何外部系统,它只关注于业务本身。
2.应用服务层(Application Service Layer):这一层负责处理应用程序的用例。它协调领域对象以实现应用程序的功能,但不包含业务逻辑。
3.适配器(Adapters):适配器是用于与外部系统(如数据库、用户界面、外部服务等)进行交互的组件。适配器将外部请求转换为应用程序可以理解的格式,并将应用程序的响应转换为外部系统可以处理的格式。
4.端口(Ports):端口定义了应用程序与外部系统交互的接口。它们是领域层和适配器之间的桥梁,确保领域层不直接依赖于外部系统。
优点:
•高内聚低耦合:领域层与外部系统隔离,增强了系统的内聚性和模块化。
•易于测试:由于业务逻辑与外部系统的交互被隔离,领域逻辑可以在不依赖外部系统的情况下进行单元测试。
•灵活性和可扩展性:可以轻松替换或扩展外部系统(如数据库、UI等)而不影响核心业务逻辑。
•清晰的责任划分:每一层都有明确的职责,便于开发和维护。
缺点:
•复杂性:这种架构模式可能会增加系统的复杂性,特别是在小型项目中。
•初始设置成本高:需要额外的设计和开发工作来定义端口和适配器。
•学习曲线:对于不熟悉这种架构模式的开发人员来说,理解和实施可能需要一些时间。
六边形架构的本质在于解耦业务逻辑和技术实现,通过端口和适配器的设计,确保领域层的独立性和稳定性。这种架构模式强调业务逻辑的优先级,确保系统在技术栈变化时仍然能够保持核心功能的完整和一致。
整洁架构
这张图展示的是整洁架构(Clean Architecture),由Robert C. Martin(也称为“Uncle Bob”)提出。整洁架构是一种软件设计模式,旨在通过分离关注点来提高系统的可维护性和可测试性。它的核心思想是通过层次化设计,使得业务逻辑与外部的框架、数据库、UI等隔离开来。
1.Entities(实体):代表企业级业务规则。通常是应用程序中的核心业务对象,独立于应用的具体实现。
2.Use Cases(用例):代表应用级业务规则。用例层定义了应用程序的具体业务逻辑和交互流程。
3.Interface Adapters(接口适配器):负责将数据从外部格式转换为应用程序内部使用的格式,反之亦然。包含控制器、网关、演示者等适配器,使得外部系统可以与应用核心交互。
4.Frameworks & Drivers(框架和驱动):最外层,包含具体的框架和工具,如数据库、Web框架、UI框架等。这些组件是可替换的,应用的核心逻辑不依赖于它们。
优点
•关注点分离:通过层次化设计,业务逻辑与技术实现细节分离,使得系统更容易理解和维护。
•高可测试性:由于业务逻辑与外部依赖隔离,可以在不依赖外部系统的情况下进行单元测试。
•灵活性和可扩展性:系统的核心逻辑不依赖于具体的框架或工具,方便替换和升级。
•易于维护:清晰的层次结构使得开发人员可以更容易地定位和修复问题。
缺点
•初始复杂性:引入多层结构增加系统的初始复杂性,尤其是对于小型项目。
•过度设计风险:简单项目不需要如此复杂的架构,可能导致过度设计。
•学习曲线:理解并应用这种架构模式,可能需要一定学习成本。
整洁架构的本质在于通过层次化设计实现关注点分离,从而提高系统的灵活性和可维护性。它强调业务逻辑的重要性,并通过将其与外部技术实现隔离,确保业务逻辑的独立性和稳定性。通过这种方式,开发人员可以更专注于实现高质量的业务逻辑,同时保持对技术实现的灵活适应能力。
COLA架构
COLA(Clean Object-oriented & Layered Architecture)架构是一种分层架构模式,旨在通过分离关注点和职责,提升系统的可维护性和可扩展性。上图展示了COLA架构的经典分层结构,包括多个层次和适配器,具体如下:
1.Adapter层:负责处理外部请求和响应,通常包括控制器(controller)、调度器(scheduler)和消费者(consumer)等。它将外部输入转换为内部可处理的格式,并将处理结果返回给外部。
2.App层:应用层负责协调和处理业务逻辑,通常包括服务(service)和执行器(executor)。这一层使用DTO(Data Transfer Object)来传递数据。
3.Domain层:领域层是架构的核心,负责处理业务规则和逻辑。包括网关(gateway)、模型(model)和能力(ability)等组件。实体对象(Entity)在这一层定义,反映业务的核心概念和状态。
4.Infrastructure层:基础设施层负责与外部系统和资源的交互,如数据库、搜索引擎、RPC等。包括网关实现(gateway impl)、映射器(mapper)和配置(config)等。
5.Driving Adapter & Driven Adapter:分别代表输入适配器(如浏览器、定时器、消息等)和输出适配器(如数据库、搜索、RPC等),用于处理系统与外部环境的交互。
优势:
•清晰的职责分离:每一层有明确的职责,降低了模块间的耦合,提高了代码的可读性和可维护性。
•易于测试:通过分层结构,可以对每一层进行独立测试,特别是领域层的业务逻辑。
•灵活性和可扩展性:可以在不影响其他层的情况下更换或扩展某一层的实现。
•重用性:领域层的业务逻辑可以被不同的应用层复用,减少重复代码。
劣势:
•复杂性增加:引入多层次结构可能导致系统的复杂性增加,特别是对于简单应用来说,可能显得过于繁琐。
•性能开销:由于层与层之间的调用,可能会带来一定的性能开销。
COLA架构的本质是通过分层和分离关注点来提高系统的健壮性和灵活性。它强调领域驱动设计,将业务逻辑放在中心位置,确保系统能够更好地适应变化的业务需求。通过清晰的分层,开发者可以专注于每一层的具体实现,而不需要过多考虑其他层的细节。
应用架构的异同
5 编写“好”代码的建议
应用架构模式的选择
编写良好代码的建议
关于命名
命名本质上是一种抽象,可以传递大量隐喻信息,表达复杂的系统概念。
计算机科学只存在两个难题:缓存失效和命名。”
——PhilKarIton
命名,看上去是开发过程中“最简单”的事情,但往往是“最复杂”的事情。合理命名的挑战在于:
•命名规范及意识
•不同人对同一事物的认识角度
•中英文语言映射差异
对于具体的开发工作而言,一些好的实践是:
•提升团队对合理命名的重视程度
•明确团队的命名规范:团队层面对常见的命名定义规范
•基于领域统一语言:命名与业务域统一语言
•代码评审:人工评审 or AI自动评审
•自动化校验:通过类似ArchUnit的代码自动化规则校验工具对代码的命名规则进行约束行校验
模块化
基于领域
基于领域语言:统一语言,业务人员与技术人员在业务域名下统一术语认知,消除命名歧义。代码的实现反应业务域的统一语言。
基于领域的结构:工程结构层面基于领域视角划分,确保业务域实现的内聚性
基于领域的边界:清晰的业务域划分以及到系统域的映射,系统边界反应业务边界
关于结构化
结构化是屏蔽系统复杂度、降低系统认知负载的最直接有效的方式之一,有利于提升代码质量及可维护性。从“大”的方面看,子域的划分、系统模块化设计、分层设计都是结构化,从更细的代码维度看,代码的模块、类、方法也体现结构化思维。
适当抽象
抽象,是开发人员的必备技能。适当抽象有利于降低系统认知的复杂度,提升系统的可复用性。当然,抽象是一把双刃剑,抽象维度越高,则细节丢失越多。抽象越少,则复杂性越高、复用性及扩展性越差。
关于设计模式的过度使用
设计模式优势:
•为特定问题域提供了开箱即用的解决方案
•设计模式本身遵循了部分OOP原则,有助于开发更加面向对象的程序
•提供了技术性的统一语言,降低了成员间沟通的不一致性,提升了沟通效率
设计模式的问题在于:
•可能会引入中间类,提高了系统复杂性
•模式本身也有一定复杂度,同样也会导致认知成本的提升
•提高了代码的分析、测试复杂度
复杂的判断条件
条件判断在代码中非常常见,且有些判断逻辑涉及的条件较多。常见的坏味道是“把复杂的条件判断逻辑直接写到if/while中”,往往导致代码的可读性、可扩展性和维护性变差。解决方式是进行适当的抽象和结构化:
•将条件判断剥离,定义一个布尔型变量,且合理命名
•将条件判断玻璃,定义一个私有方法,且合理命名
•将条件判断玻璃,定义一个类或服务,且合理命名
合理注释
“代码应该自解释”,通过合理的命名代码应该完全准确、清晰、无歧义的表述其用途和逻辑。
“代码自解释”是是一个美丽的谎言。由于
不可否认,好的代码命名有助于提升系统的可读性。实际实践中,对“好的命名”无法进行统一,这与业务域上下文、团队认知、中英文语言差异有关,因此,在实践中完全基于“命名自解释”无法达到预期目的。通过合理的、有效的注释能够大大降低代码认知负载、提高代码维护效率,特别是对于遗留系统而言。
关于注释的常见反例:
•对非常明确表达语意的代码进行注释
•对代码的一比一重复
•对注释进行编号表达代码逻辑顺序
6 附录
六边形架构
https://alistair.cockburn.us/hexagonal-architecture/
https://swreflections.blogspot.com/2012/12/rule-of-30-when-is-method-class-or.html
https://dannorth.net/2022/02/10/cupid-for-joyful-coding/
https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/
https://zhuanlan.zhihu.com/p/420022531
软件系统复杂性: https://zhuanlan.zhihu.com/p/533396688