夜雨聆风学习资料网

ARTICLE · 1076500

第15讲:软件设计概述

第15讲:软件设计概述
软件设计(Software Design)是软件工程中承上启下的核心活动。
在需求分析阶段,我们主要回答:系统需要解决什么问题?而在软件设计阶段,我们进一步回答:系统应该如何组织,才能可靠、可维护、可扩展地解决这些问题?最终在实现阶段,则进一步回答:这些设计如何被转换为可执行的软件?
因此,可以将软件工程中的基本逻辑过程抽象为:业务问题→需求→设计→实现→验证(测试)→运行与反馈→持续演进。
软件设计并不是简单地把需求“翻译成代码”,也不是在编码之前画几张 UML 图。更准确地说:软件设计是在需求、约束和质量属性的共同作用下,通过划分职责、建立边界、定义协作关系并进行技术权衡,形成一个能够被实现、验证和持续演进的软件解决方案。
例如,一个需求可能是:用户可以在线提交订单。这个需求只描述了系统的外部行为,但没有回答大量设计问题:订单由哪个模块负责?用户、订单、商品之间如何划分职责?订单数据如何存储?创建订单是否需要事务?库存扣减与订单创建是否必须保持强一致?支付应该同步调用还是异步处理?系统是否允许重复提交?如何防止重复扣库存?API 应该如何定义?如果支付服务不可用怎么办?如何记录一次订单请求经过了哪些服务?系统达到每秒数万次订单请求时如何扩展?如果未来需要拆分微服务,现在的模块边界是否能够支撑这种变化?这些问题都属于软件设计需要解决的问题。
因此,软件设计的对象并不仅仅是“代码结构”,而是一个更大的结构空间:       
1  软件设计的相关概念
1.1 软件设计与需求、架构、实现的关系
软件设计经常与“需求分析”、“软件架构”、“编码实现”等概念混淆。实际上,它们虽然密切相关,但关注层次不同。
需求:定义问题空间
需求主要描述:系统要提供什么能力;谁使用系统;系统需要满足什么业务规则;系统需要满足什么质量属性;系统受到哪些约束。例如:用户可以在30 秒内完成订单提交。这里既包含功能需求,也可能隐含性能需求。
架构:定义系统的主要结构
架构主要关注:系统由哪些主要部分构成;这些部分之间如何协作;哪些边界需要长期稳定;数据和控制流如何组织;哪些技术决策会对整个系统产生长期影响。例如:层次架构如下。
详细设计:定义局部结构
详细设计进一步回答:Order Service 内部有哪些模块?Order 如何表示?OrderRepository 如何设计?OrderValidator 做什么?如何处理订单状态转换?哪些异常需要捕获?数据库事务边界在哪里?实现:将设计转化为可执行系统
实现最终把这些设计转化为:类、函数、数据库表、API、配置、容器、基础设施、测试代码、部署制品。因此可以建立如下对应关系:

层次

核心问题

需求

为什么做、做什么

架构

系统如何组织

详细设计

模块、服务如何协作

实现

如何写成可执行代码

测试

如何验证是否满足预期

运维

如何持续运行和反馈

需要特别强调的是:架构设计与软件设计不是完全等价的概念。架构是软件设计的重要组成部分,但软件设计的范围通常更加广泛。架构关注系统整体结构及其关键决策,而设计还包括模块、接口、数据、算法、状态、异常处理等更细粒度的问题。
1.2 软件设计的层次:架构设计、子系统和模块设计与详细设计
软件设计具有明显的层次性。一种常见的划分方式是:系统级→架构级→子系统级→模块/组件级→类/函数级→算法与数据结构级。不同层次的设计,其关注点不同。
第一层:架构设计
架构设计关注系统的整体结构。例如:单体还是分布式;模块化单体还是微服务;同步调用还是事件驱动;数据库如何组织;服务边界如何划分;如何实现高可用;如何实现弹性扩展。
第二层:子系统和模块设计
这一层关注职责划分。例如,订单系统可以分为订单、库存、支付、客户管理和通知管理。这里需要解决的是:哪些职责应该放在一起?哪些职责应该隔离?
第三层:组件和接口设计
这层进一步定义:API;Interface;DTO;Event;Repository;Adapter;Plugin 等。
第四层:详细设计
最后这一层涉及:类;方法;数据结构;状态机;算法;异常处理;并发控制。
因此,所谓“设计到什么程度”并没有统一答案。现代软件工程更倾向于:根据风险、复杂度和变化成本决定设计深度。对于一个简单 CRUD (增查改删) 功能,可能不需要几十页设计文档。而对于金融交易、分布式一致性、高并发系统、安全关键系统、大规模数据平台,则需要更深入的架构和设计分析。
1.3  软件设计过程:计划式、渐进式与演进式设计
软件设计并不存在唯一的过程。传统软件工程强调较完整的前期设计,即在编码之前尽可能确定系统结构。这种方法在需求稳定、约束明确、变更成本高的系统中仍然具有价值。但现代软件开发越来越强调:设计不是一次性活动,而是持续发生的工程活动。可以区分三种典型方式。
(1) 计划式设计
先进行较完整的设计,再进入大规模实现。其过程是:Requirements→Architecture→ Detailed Design→Implementation。其优点:整体结构清晰;对高风险系统更容易进行前期验证;有利于团队协作和治理。缺点:需求变化时容易产生设计返工;前期设计可能建立在错误假设之上。
(2) 渐进式设计
先建立基本结构,再随着需求逐步完善设计。其过程是:Initial Design→Implementation→Feedback→Refinement→More Implementation。这种方式强调:不追求一开始就知道所有细节。
(3) 演进式设计
演进式设计进一步强调:软件结构本身应该能够随着需求、技术和运行环境变化而持续演进。
例如:Version 1,模块化单体→Version 2,内部模块边界稳定→Version 3,部分模块独立部署→Version 4,形成服务化架构。
这并不意味着“所有系统最终都应该变成微服务”。真正重要的是:设计应该降低未来因变化而带来的成本。
1.4 为什么需要软件设计:复杂性、变化与技术债务
软件系统最大的敌人之一并不是代码数量,而是复杂性(Complexity)。当系统规模增加时,组件之间的关系数量可能快速增长。
假设系统有 n 个组件,如果任意两个组件都存在直接依赖,潜在关系数量可以达到:n(n−1)/2个。因此,软件设计的重要目标之一就是:控制复杂性,而不是消灭复杂性。复杂性主要来自:业务规则;数据关系;状态变化;并发;分布式环境;外部依赖;技术约束;团队协作;历史兼容性。
如果没有良好的边界,系统可能逐渐形成所谓的Big Ball of Mud(大泥球),任何一个局部修改都可能影响其他区域。这会形成:修改一个功能→ 发现依赖 → 修改另一个模块 → 测试失败 → 修复第三处 → 引发更多回归。最终产生技术债务(Technical Debt)。
因此,软件设计的价值并不是“让代码看起来漂亮”,而是降低未来变化带来的成本和风险。
2 软件设计的核心原则
2.1 关注点分离
关注点分离(Separation of Concerns,SoC)是软件设计最基础的原则之一。所谓关注点,是指系统中具有相对独立意义的一类问题。
例如,一个电商系统可能同时包含:用户认证、商品目录、订单、支付、库存、消息通知、日志、权限、监控。如果这些问题全部混杂在一起,系统将很难理解和修改。因此可以通过不同维度进行分离,按业务关注点分解:
按技术关注点分解:
SoC 的核心并不是简单地“拆文件”,而是让不同变化原因尽可能彼此隔离。例如,如果支付规则发生变化,理想情况下不应该迫使库存模块发生大量修改。
2.2 抽象与逐步求精
抽象(Abstraction)意味着:隐藏与当前问题无关的细节,只暴露当前层次真正需要的信息。
例如:paymentService.pay(order),调用者只关心:支付订单,而不需要知道:HTTP 如何调用、签名如何生成、JSON 如何序列化、网络重试如何处理、第三方支付 API 如何变化。这就是抽象。
与抽象对应的是逐步求精(Stepwise Refinement)。这个求精过程可以理解为:业务目标→系统能力→子系统→模块→组件→接口→算法→代码。
抽象与逐步求精共同解决一个问题:如何控制认知复杂度。一次性面对全部细节,人类很难有效理解大型系统。因此优秀设计通常允许我们在不同抽象层次之间切换。
2.3 信息隐藏
信息隐藏(Information Hiding)是软件设计理论中的重要思想。其核心是:模块应该隐藏那些最可能发生变化的设计决策。
例如,一个订单模块内部使用MySQL,并不意味着其他模块必须知道表结构、SQL语句、索引、ORM 映射方式。理想结构是:数据库技术成为实现细节,而不是业务模块之间的共同知识。
信息隐藏的价值在于:把变化隔离在边界内部。因此,“什么应该被隐藏”往往比“什么应该被拆出来”更加重要。
2.4 高内聚与低耦合内聚
内聚
内聚(Cohesion)描述一个模块内部各部分之间的关联程度。理想情况下,一个模块应该围绕相对统一的职责组织。
例如:Order应该只针对 createOrder、 cancelOrder、calculateTotal、 changeStatus等功能模块。这些功能模块行为都与订单相关,因此具有较高内聚。而如果一个模块同时包含:Order、Email、ImageProcessing、DatabaseBackup、Payment,则内聚性较低。
耦合
耦合(Coupling)描述模块之间的依赖程度。
例如:A → B意味着 A 依赖 B。如果形成:A → B → C → D → E,则一个变化可能沿依赖链传播。
因此现代软件设计通常追求:高内聚、低耦合。但这里需要避免机械理解。低耦合并不意味着:模块之间完全没有关系。如果两个模块本来就需要协作,那么强行隔离可能反而增加复杂性。真正需要优化的是:不必要的耦合。
2.5 接口与契约
接口(Interface)定义了两个设计单元之间如何协作。契约(Contract)则进一步定义:双方必须遵守什么规则。
例如, API:POST /orders,可能规定:
{
"productId": "P1001",
"quantity": 2
}
同时定义:请求字段、数据类型、必填条件、错误码、幂等规则、权限要求、超时语义、版本兼容策略。
因此现代软件系统越来越强调:Contract-first / API-first Design,接口不是“函数签名”这么简单。它实际上构成了一个边界上的协议。
2.6 SOLID 与现代设计原则
SOLID 包含:
  • Single Responsibility Principle:单一职责原则
  • Open/Closed Principle:开闭原则
  • Liskov Substitution Principle:里氏替换原则
  • Interface Segregation Principle:接口隔离原则
  • Dependency Inversion Principle:依赖倒置原则
这些原则主要帮助设计者控制对象之间的职责和依赖。但现代软件工程不应把 SOLID 当作万能规则。例如:为了满足“单一职责”而创建几十个只有几行代码的类,可能导致过度抽象。
因此应该将 SOLID 放在更大的设计原则体系中理解,也就要在这个过程业务边界→职责划分→信息隐藏→接口设计→依赖管理→实现细节中理解。
现代设计还需要考虑:简单性(simplicity)、明确性 / 显式性(explicitness)、不可变性(immutability)、依赖方向(dependency direction)、可测试性(testability)、韧性 / 弹性(resilience)、可观测性(observability)、可演进性 / 可演化性(evolvability)。
2.7 设计原则的适用边界与权衡
设计原则不是法律。例如,“低耦合越低越好”,并不成立。因为降低耦合通常需要增加接口、转换层、消息、配置、网络通信等,这些都会增加复杂度。因此设计的核心不是:最大化某一个指标,而是在多个目标之间取得合理平衡。
常见平衡关系包括:简单性↔ 灵活性、性能↔ 抽象、一致性↔ 可用性、自治性↔ 运维复杂度、复用性↔ 独立性、安全性↔ 易用性、成本↔ 性能。这也是软件设计区别于机械工程规范的重要地方之一:软件设计通常没有脱离上下文的唯一最优答案。
3 软件设计的构造单元与边界
3.1 模块
模块(Module)是软件设计最基本的结构化单元之一。模块通常具有:明确职责、明确接口、内部实现细节、对外依赖。
例如:
Order Module
├── Order
├── OrderService
├── OrderRepository
└── OrderValidator
模块化的核心不是目录结构,而是控制依赖方向和信息暴露范围。一个好的模块应该能够在相对有限的范围内理解、测试和修改。
3.2 组件
组件(Component)通常比单个类或函数具有更大的粒度。它可以是:
  • 一个可替换的软件单元;
  • 一个独立模块集合;
  • 一个具有明确接口的运行或部署单元。
需要注意:组件不等于JAR,也不等于 Docker Image。JAR、动态库、容器镜像等首先是软件交付或部署制品,而组件是更高层次的设计概念。组件强调的是:职责+ 接口 + 可组合性 + 可替换性。
3.3 子系统
当系统规模进一步扩大,可以把相关组件组织成子系统。例如:
E-Commerce
   ├── Customer Subsystem
   ├── Product Subsystem
   ├── Order Subsystem
   ├── Payment Subsystem
   └── Inventory Subsystem
子系统提供了更高层次的认知边界。
3.4 接口与契约
不同组件之间需要通过接口协作。接口可能表现为:方法调用、RPC、消息、事件、文件、数据库协议。随着边界从进程内扩展到网络,契约的重要性不断增加。
例如,进程内调用:A → B,如果 B 发生异常,A 可以直接收到异常。而跨网络:A → Network → B,则增加了超时、重试、部分调用失败等情况,因此:分布式边界不是免费的抽象。
3.5 服务
服务(Service)通常代表一个可以被其他系统或业务能力调用的软件能力。服务具有明确职责、对外接口,需要考虑生命周期 、运行环境、依赖关系等。例如:Order Service、Payment Service、Inventory Service、Customer Service。
服务可以部署在同一个进程中,也可以部署在不同进程甚至不同机器上。Service 是逻辑概念,不必然等于网络进程。
3.6 微服务
微服务(Microservice)是一种更加严格的服务化架构风格。通常强调:独立部署、有明确边界、相对自治,能独立演进,通过网络协议协作,尽量减少共享内部状态。典型结构:
但是微服务不是模块化的终点。微服务会引入新的复杂性:网络失败、分布式事务、数据一致性、服务发现、配置管理、可观测性、版本兼容、部署治理、安全认证、调试困难。因此,能够拆成微服务,不代表应该拆成微服务。
3.7 模块化单体与微服务
现代工程实践越来越重视模块化单体(Modular Monolith)。它可以保持一个部署单元,但内部具有严格模块边界。例如:
Application
├── Order Module
├── Payment Module
├── Inventory Module
└── Customer Module
模块之间通过明确接口协作,而不是随意访问彼此内部实现。这种结构能够同时获得、单体部署的简单性、模块化设计的结构性、较低的运维成本、为未来拆分保留可能性。因此,现代架构选择不应该简单理解为:Monolith < Microservices,而应该理解为:微服务针对复杂的系统,模块化单体也要考虑系统规模、需求。真正的问题是:当前系统是否需要分布式架构带来的收益,以及是否能够承担它带来的复杂度。
3.8 职责划分与边界设计
职责划分与边界设计是软件模块化的核心。职责划分回答“谁负责什么”,即根据业务职责、数据所有权、业务规则和变化原因,将系统功能合理分配给不同的模块、组件或服务;边界设计回答“哪些应该放在一起、哪些应该彼此隔离”,通过明确的接口、数据和依赖关系隐藏内部实现,减少不必要的耦合并控制变化的影响范围。合理的设计通常遵循高内聚、低耦合、信息隐藏和单一职责等原则:相关职责应尽量集中在同一设计单元中,独立变化的职责应适当分离,同时避免过度拆分。需要注意的是,模块边界、服务边界和部署边界并不一定相同,可以先建立清晰的逻辑边界,再根据性能、扩展性、团队协作、故障隔离等实际需求决定是否进一步形成独立的服务和部署单元。因此,边界设计的目的不是把系统拆得越细越好,而是以合理的职责划分控制复杂性、隔离变化并降低维护成本。
4  从需求到架构与详细设计
4.1 从需求模型到设计模型
需求模型关注:系统应该提供什么能力。设计模型关注:系统应该如何组织这些能力。
例如:需求:用户可以取消订单。进一步分析,业务规则:只有未发货订单可以取消。再进一步形成:用例、场景、领域模型、职责分配、架构视图、接口设计、数据设计、UML,以及从架构设计逐步落到代码设计的全过程。因此,设计是需求语义向技术结构映射的过程。注意:领域驱动设计(Domain-Driven Design,DDD)强调:软件结构应该与业务领域中的重要概念建立稳定映射。
4.2 架构设计与关键结构决策
架构设计是在系统整体层面确定主要结构、核心组件及其协作关系的过程,重点解决系统“如何组织”的问题。它需要根据业务需求、质量属性和工程约束,对系统的部署方式、模块与服务划分、数据组织、通信机制、技术选型以及安全、性能、可靠性等关键方面作出决策。例如,选择模块化单体还是微服务、同步调用还是事件驱动、集中式还是分布式数据管理,都是典型的架构决策。这些决策通常具有影响范围大、修改成本高、长期影响明显等特点,因此需要分析业务背景、候选方案及其权衡,而不能仅依据技术流行程度进行选择。架构设计的核心不是追求某一种“最佳架构”,而是在特定约束下建立能够满足当前需求并支持未来演进的系统结构。
架构决策通常具有两个特征:1)高影响,一个决定会影响大量模块;2)高修改成本,一旦实施,再修改会产生很大成本。因此应该优先识别:哪些决策是高风险、难改变的决策?这些问题值得优先进行架构分析。
4.3 模块与组件设计
模块与组件设计是在职责划分和架构决策的基础上,将系统进一步组织为具有明确职责、清晰接口和相对独立实现的设计单元。模块主要用于组织和隔离相关功能,强调逻辑上的高内聚与低耦合;组件则通常具有更完整的功能边界,可以通过明确的接口与其他组件协作,并可根据需要独立复用、测试或部署。设计时应重点考虑职责分配、接口定义、依赖关系、数据与状态管理以及变化隔离,使模块或组件之间通过稳定的契约协作,而不是依赖彼此的内部实现。合理的模块与组件设计能够降低系统复杂度、控制变化传播,并为后续的代码实现、测试、复用以及服务化演进提供良好的结构基础。
4.4 API 与契约设计
API(Application Programming Interface)与契约设计是定义软件组件之间如何协作以及必须遵守什么规则的过程。API不仅包括接口地址、请求参数和返回数据,还应明确数据格式、错误处理、认证授权、幂等性、超时、版本兼容等行为约定。良好的 API 设计应具有清晰、稳定、可理解和可演进的特点,使调用方能够在不依赖实现细节的情况下使用服务。对于跨模块、跨进程和跨服务的系统,API 契约尤其重要,因为接口一旦被其他系统依赖,其变化就可能产生较大的影响。因此,现代软件工程通常强调“契约优先(Contract-First)和 API 优先(API-First)”的设计思想,通过明确接口边界和兼容策略降低系统之间的耦合,为独立开发、测试、部署和持续演进提供基础。例如:POST /orders,除了考虑成功响应,还需要定义:
400 Invalid Request
401 Unauthorized
403 Forbidden
409 Conflict
429 Too Many Requests
500 Internal Error
如果 API 没有稳定的契约,服务之间就会形成隐性耦合。显然,API 是架构边界的一部分,而不是实现细节。
4.5 数据与状态设计
很多系统的问题最终都可以追溯到:数据如何组织、谁拥有数据、谁可以修改数据。数据与状态设计是确定系统中数据如何组织、存储、访问、流转和保持一致的过程,也是架构设计的重要组成部分。设计时需要明确数据模型、数据所有权、存储方式、缓存策略以及状态的生命周期,并根据业务需求确定一致性、事务性和并发控制策略。对于分布式系统,还需要进一步考虑数据复制、分片、最终一致性以及跨服务数据协作等问题。良好的数据与状态设计应尽量明确数据由谁拥有、谁可以修改以及如何保证其正确性,避免无序共享状态和隐式数据依赖,从而降低系统耦合并提高可靠性、可扩展性和可维护性。
4.6 演进式设计与持续重构
现代软件系统不应该把设计视为一次性产物。演进式设计与持续重构,是现代软件团队应对需求变化和技术不确定性的核心实践。演进式设计强调:不要试图在项目初期一次性设计出完美架构,而是以可工作的软件为起点,通过小步迭代、快速反馈和真实业务验证,让架构、模块边界与抽象逐步“生长”出来。持续重构则是这一理念在日常开发中的落地方式:在不断交付功能的同时,持续进行小幅、安全、可验证的代码改进,消除重复、澄清职责、降低耦合、提升内聚,避免技术债不断累积。二者相辅相成——演进式设计提供方向与原则,持续重构提供手段与节奏;它们共同依赖自动化测试、持续集成、代码评审和良好的版本控制。其目标不是追求一次到位,而是让软件在变化中保持可理解、可修改、可扩展,从而更从容地拥抱未来。这就是现代软件工程的重要思想:设计不仅仅是编码之前的活动,而是贯穿整个软件生命周期的活动。
5 面向质量属性的设计
5.1 性能与可扩展性
软件性能与可扩展性是保障系统在高负载和业务增长下持续稳定运行的两个关键维度。性能关注系统“跑得多快、多稳”,通常体现为响应时间、吞吐量、延迟、并发能力和资源利用率等指标;可扩展性关注系统“能长多大、多快适应增长”,即当用户量、数据量或请求量上升时,能否通过纵向扩容或横向扩展,以合理成本维持可接受的性能水平。二者密切相关但并不等同:单机高性能不代表能平滑扩展,而良好的可扩展性也未必意味着单次请求最快。
在实践中,需要结合容量规划、性能基准测试、全链路压测、 profiling 与可观测性,识别瓶颈,并运用缓存、异步、批处理、负载均衡、无状态服务、分区分片、弹性伸缩等手段,在一致性、可用性、成本和复杂度之间做出权衡。其目标不是追求极限指标,而是让系统在变化中保持稳定、可预测、可演进,为业务增长提供可靠支撑。
5.2 可用性与可靠性
软件可用性与可靠性是衡量系统能否持续、正确提供服务的两个核心质量属性。可用性关注“系统在需要时是否可访问、可使用”,通常用正常运行时间比例或停机时间衡量,如 99.9%、99.99%;可靠性关注“系统在规定条件下和规定时间内是否无故障地完成既定功能”,强调故障频率、平均无故障时间、平均恢复时间、数据正确性与结果一致性。可用性是可靠性的重要体现,但二者并不等同:系统可能通过快速故障转移保持高可用,却因数据错误或结果不一致而可靠性不足;也可能单机非常可靠,却因维护或单点故障导致可用性下降。
在实践中,需要通过冗余部署、故障隔离、健康检查、自动故障转移、超时重试、熔断限流、优雅降级、备份恢复、幂等设计、可观测性、混沌工程和 SRE/SLO 体系等手段,在成本、复杂度、一致性与恢复速度之间取得平衡。其目标不是追求永不故障,而是在故障不可避免的前提下,保障业务连续、数据可信、恢复可控,让系统在长期运行中赢得用户信任。
5.3 安全性与 Security by Design(设计安全,也称安全内建)
软件安全性与 Security by Design 是保障系统在恶意环境和复杂供应链中可信运行的关键。安全性关注系统与数据是否免受未授权访问、泄露、篡改、破坏和滥用,核心包括机密性、完整性、可用性,以及认证、授权、审计、不可否认等能力。Security by Design 则强调安全不能依赖事后补丁或上线前扫描,而应从需求、架构、设计、编码、测试、部署到运维的全生命周期内建:通过威胁建模识别风险,遵循最小权限、纵深防御、默认安全、攻击面最小化、零信任和隐私保护等原则,结合输入验证、加密与密钥管理、依赖与供应链扫描、SAST/DAST/SCA、代码评审、日志监控和应急响应,并借助 DevSecOps 将安全左移、持续自动化。其目标不是追求绝对安全,而是在风险、成本、体验与交付速度之间做出可接受的权衡,让安全成为系统的默认属性和持续能力,从而保护用户、业务与信任。
5.4 可维护性与可演进性
可维护性与可演进性是决定软件能否长期健康存活、持续创造价值的两个关键质量属性。可维护性关注“系统改起来是否容易”,即在修复缺陷、调整功能、优化性能或升级依赖时,能否快速理解、安全修改、充分验证和顺利发布,通常体现为代码可读性、模块化、低耦合高内聚、测试覆盖、文档完备、可观测性以及构建部署自动化等。可演进性关注“系统能否随变化持续成长”,即面对业务需求、用户规模、技术栈和组织结构的长期变化,能否以合理成本进行扩展、替换、重构和迁移,而不必推翻重来,通常依赖清晰的边界、稳定的接口、演进式设计、持续重构、向后兼容、版本策略、可插拔架构和自动化交付。二者相辅相成:可维护性是可演进性的基础,缺乏可维护性的系统难以持续演进;可演进性则是可维护性在时间维度上的延伸,避免只解决眼前修改而积累长期僵化。
在实践中,需要借助领域驱动设计、模块化或微服务、整洁架构、契约测试、CI/CD、代码评审、技术债管理和可观测性等手段,在短期交付与长期成本之间做出权衡。其目标不是写出永不修改的代码,而是让系统在变化中保持可理解、可修改、可替换、可迁移,从而降低总拥有成本,延长生命周期,支撑业务持续创新。
5.5 可测试性
软件可测试性设计(Design for Testability)是指在架构、模块与代码设计阶段,就有意识地让系统更容易被观察、控制、隔离和验证。它关注的核心不是“测试写得多不多”,而是“测试写得快不快、稳不稳、准不准、成本低不低”。良好的可测试性通常表现为:职责清晰、模块化、低耦合、接口稳定,依赖可注入、可替换,全局状态和隐藏依赖少,时间、随机数、网络、数据库等外部因素可被模拟或隔离,关键行为可观测,测试数据可构造,失败可定位。
在实践中,常结合依赖注入、面向接口编程、分层架构、契约测试、测试替身、测试金字塔、自动化 CI 和可观测性等手段,让单元测试、集成测试、契约测试和端到端测试各司其职。可测试性设计与 TDD、持续重构、持续交付相辅相成:它让快速反馈成为可能,也让重构和演进更有底气。其目标不是追求 100% 覆盖率,而是在质量、成本与交付速度之间取得平衡,使验证成为内建能力,而非事后负担。
一个重要思想是:如果一个系统很难测试,问题可能不仅在测试代码,而可能在系统设计本身。
5.6 可观测性
软件可观测性(Observability)是指通过系统对外产生的日志、指标、追踪、事件、性能剖析等遥测数据,推断和解释其内部运行状态、行为与故障根因的能力。它源于控制论,强调系统不仅可被监控,还可被主动探索和提问。传统监控主要依赖预定义指标和已知故障模式,回答“系统是否正常”;可观测性则借助高基数、高维度和上下文关联的数据,支持对未知问题、复杂交互和长链路调用进行下钻分析,回答“为什么异常、哪里异常、影响什么”。在云原生、微服务和分布式架构中,可观测性通常以日志(Logs)、指标(Metrics)、追踪(Traces)为三大支柱,并扩展至持续性能剖析(Profiling)、事件与变更记录等,通过 OpenTelemetry 等标准实现统一采集与关联。其核心价值在于缩短故障发现与恢复时间(MTTR)、提升系统可靠性与性能、优化资源成本、改善用户体验,并支撑 SRE、DevOps、AIOps 和持续交付实践。可观测性不仅是工具组合,更是一种工程能力与文化:在设计与开发阶段就内建可观测性,使系统在运行时能够被理解、调试和持续改进。因此,可观测性应该在架构和接口设计阶段被考虑,而不是系统上线后再添加日志。
5.7 成本、能效与绿色软件工程
软件成本、能效与绿色软件工程是相互关联的三个概念:软件成本不仅包括开发、测试、维护和许可等显性支出,还包括运行阶段的计算、存储、网络、冷却和电力等隐性支出;能效则衡量软件完成特定功能或服务所消耗的能量,通常与算法效率、架构设计、资源调度、代码质量和基础设施利用率密切相关。绿色软件工程是在软件全生命周期中系统性地考虑环境影响与可持续性的工程方法,它把能耗、碳排放和资源效率作为与功能、性能、成本同等重要的指标,通过需求分析、架构设计、编码实现、测试部署和运维监控等环节进行度量与优化。其核心目标是在满足业务功能和性能要求的前提下,尽量减少不必要的计算与资源浪费,从而降低软件的总拥有成本、能源消耗和碳足迹。换言之,低效软件会推高云账单、电费和碳排放,而绿色软件工程通过能效优化实现经济、环境与性能的多赢。因此,性能优化、成本优化和绿色软件设计在很多场景中具有共同的工程基础。
5.8 面向X设计(Design for X)
软件 Design for X(DFX,面向 X 设计)是一种面向产品全生命周期的设计理念与方法体系,其中 X 代表需要在设计阶段就被显式考虑的各种质量属性、生命周期关切或利益相关方目标,如可测试性、可维护性、可靠性、安全性、性能、可扩展性、可部署性、可观测性、可用性、可访问性、成本、能效与可持续性等。它强调“下游问题、上游解决”,在架构与详细设计阶段就预测并权衡开发、测试、部署、运维、升级、退役等环节的约束,通过设计准则、模式、检查表和度量指标,把非功能需求转化为可验证的设计决策。
在软件工程中,DFX 不是单一方法,而是一种并行工程与质量属性驱动的设计思维:例如 Design for Testability 让系统更易自动化验证,Design for Maintainability 降低长期演进成本,Design for Energy Efficiency 与 Design for Sustainability 则把能耗、碳排放和资源效率纳入设计目标,是绿色软件工程的重要实践。其核心价值在于减少后期返工、降低总拥有成本、缩短交付周期,并在功能、性能、成本与环境影响之间取得平衡。
其核心是:把质量属性前移到设计阶段,而不是等系统完成以后再“补救”。
6  架构风格与现代设计范式
软件架构风格是组织系统结构、交互方式与治理规则的惯用模式;现代设计范式则是在云原生、分布式、数据驱动和 AI 背景下形成的更高层组织原则。它们并非互斥,实际系统往往是多种风格的组合:分层与模块化提供内部秩序,SOA 与微服务提供服务边界,事件驱动提供异步解耦,Serverless 与云原生提供弹性与免运维,AI-Native、Agent 与 Workflow 则把模型、工具和流程纳入架构一等公民。
分层与模块化架构是最基础也最持久的组织方式。分层按职责横向切分,如表现层、应用层、领域层和基础设施层,规则是上层依赖下层抽象,下层不反向依赖上层,并尽量避免跨层调用。模块化按业务能力或限界上下文纵向切分,如订单、库存、支付等模块,每个模块高内聚、低耦合,封装自己的领域模型、应用服务和数据访问逻辑,仅通过接口、DTO 或领域事件协作。两者结合形成“纵向模块、横向分层”的结构,支持独立开发、测试和演进,降低变更影响范围,提高可维护性、可测试性和团队并行能力。
面向服务架构(SOA)以“服务”为基本构建单元,把业务功能封装为可发现、可复用、自治、松耦合、平台无关的服务,并通过标准化契约和消息通信进行交互。常见角色包括服务提供者、服务消费者、服务注册中心和企业服务总线(ESB);ESB 负责消息路由、协议转换、数据转换、服务编排、安全与监控。SOA 强调企业级复用、异构系统集成和集中治理,适合大型组织整合遗留系统。但它的代价是 ESB 可能成为中心瓶颈,治理和性能开销较高。与微服务相比,SOA 更偏集中式、粗粒度和共享契约,微服务更偏去中心化、细粒度和独立部署。
微服务架构将单一应用拆分为一组小型、自治服务,每个服务围绕一个业务能力构建,运行在独立进程中,拥有独立数据存储,通过 REST、gRPC、消息队列等轻量协议通信,并可独立开发、测试、部署和扩缩容。其核心原则包括单一职责、围绕业务能力组织、去中心化治理与数据管理、基础设施自动化、容错设计和可观测性。微服务通常配合 API 网关、服务注册与发现、配置中心、CI/CD、容器与 Kubernetes、断路器、Saga 和事件驱动。优势是技术异构、弹性伸缩、团队自治和快速交付;挑战是分布式事务、网络延迟、服务发现、安全、监控和运维复杂度。服务边界通常依据限界上下文划分。
事件驱动架构(EDA)把“事件”作为一等公民。事件表示已经发生的事实,如“订单已创建”“支付已完成”,生产者发布事件而不直接命令消费者,消费者通过事件总线或流平台异步订阅和处理。EDA 能实现时间解耦、空间解耦、削峰填谷、弹性扩展和跨服务协作,常用于实时分析、IoT、订单履约和通知系统。常见模式包括事件通知、事件携带状态转移、事件溯源和 CQRS。它通常带来最终一致性,并需要处理幂等、重复消费、顺序、Schema 演进、死信队列、可观测性和调试复杂度。EDA 既可以独立使用,也可以作为微服务之间的协作骨架。
Serverless 与云原生范式是一组方法集合,而非单一架构。云原生强调容器、Kubernetes、服务网格、声明式 API、不可变基础设施、CI/CD、弹性伸缩和可观测性;Serverless 则进一步把基础设施管理交给平台,典型形态是 FaaS 与 BaaS,按事件触发、自动扩缩、按需付费、免运维。Serverless 适合事件处理、API、批处理、定时任务和轻量集成,能显著降低运维成本和空闲资源浪费。但它也有冷启动、执行时长、状态管理、本地调试、供应商锁定和可观测性等挑战。云原生与 Serverless 的共同目标是让系统更弹性、更自动化、更贴近业务价值。
AI-Native Architecture(AI 原生架构)把 AI 模型、数据和反馈闭环作为系统的一等公民,而不是事后附加的智能功能。它通常包含数据管道、特征存储、向量数据库、模型注册与 serving、提示与上下文管理、RAG、评估体系、反馈采集、模型监控和安全合规。与确定性服务不同,AI 原生架构具有概率性、数据依赖、持续演进和 GPU 成本高等特点,因此需要新的可观测性、评估、版本管理、回滚和治理机制。架构重心从“请求—响应”转向“数据—模型—推理—反馈—再训练”的闭环,并强调人机协作、隐私、安全与成本效率。
Agent 与 Workflow是 AI 时代重要的设计范式。Workflow 是确定性的编排方式,定义步骤、状态、分支、重试、补偿和人类任务,适合可预测、可审计、强一致的业务流程。Agent 则是目标驱动的自治实体,能够感知环境、规划任务、调用工具、维护记忆、反思结果,甚至多智能体协作,适合开放、动态和需要推理的任务。现代系统常把两者结合:用 Workflow 提供可靠骨架、权限边界和治理,用 Agent 提供动态决策和工具调用能力;Workflow 可以编排 Agent,Agent 也可以调用 Workflow。其挑战包括可靠性、幻觉、权限控制、成本、可观测性、评估和人类在环。
总体来看,这些架构风格和设计范式不是替代关系,而是可组合的架构工具箱。选择时应综合考虑业务边界、团队拓扑、一致性要求、延迟、成本、合规、安全和能耗。现代架构的共同趋势是:从单体走向服务化与事件化,从固定基础设施走向云原生与 Serverless,从确定性软件走向 AI 原生与 Agent 化;而共同目标始终是高内聚、低耦合、可演进、可观测、安全、可持续。
7 设计质量、权衡与知识沉淀
7.1 设计评审
设计评审是项目设计阶段的重要质量控制环节,主要对系统整体架构、功能设计、技术方案、接口设计以及数据结构等内容进行全面检查和确认。通过组织项目相关人员对设计成果进行审查,及时发现设计过程中存在的问题、不合理之处及潜在风险,并提出相应的修改和优化建议。
设计评审应重点关注设计方案是否符合项目需求和相关技术规范,系统架构是否合理,功能划分是否清晰,模块之间的接口及数据交互是否完整,以及系统的安全性、可靠性、可维护性和可扩展性是否满足项目要求。评审过程中应形成明确的评审意见和问题清单,并由相关责任人员进行整改和跟踪。
设计评审完成后,根据评审结果对设计文档及相关技术方案进行修改完善。经确认符合要求后,形成最终设计成果,为后续的开发、测试和项目实施工作提供依据。
7.2 质量属性与架构权衡
软件质量属性是衡量系统整体质量和运行效果的重要指标,主要包括性能、可靠性、安全性、可维护性、可扩展性、可用性等方面。在软件架构设计过程中,不同质量属性之间往往存在一定的相互制约关系,因此需要结合系统实际需求,对各项质量属性进行综合分析和权衡。
在架构设计阶段,应首先明确系统的核心质量需求,并根据业务特点确定不同质量属性的优先级。例如,为提高系统性能,可以采用缓存、异步处理和负载均衡等架构方案,但可能会增加系统复杂度和数据一致性管理的难度;为提高系统可靠性,可以采用冗余部署、故障转移等机制,但同时会增加系统资源和运维成本。因此,架构设计不能单纯追求某一项质量属性,而应在性能、可靠性、安全性、可维护性以及成本等因素之间取得合理平衡。
通过对不同架构方案进行分析和比较,结合项目需求、技术条件及实施成本,选择能够满足核心业务需求的架构方案,并对关键架构决策及其权衡结果进行记录,为后续系统开发、测试和维护提供依据。
7.3 设计的可追溯性。
软件设计的可追溯性是指软件设计过程及设计成果能够与项目需求、系统功能、技术方案以及后续实现和测试活动建立明确的对应关系。通过建立完整的追溯关系,可以确保每项设计内容均具有明确的需求来源,同时能够在需求发生变更时快速定位受影响的设计模块和相关实现内容,从而降低变更带来的影响和风险。
在软件设计过程中,应建立需求与设计、设计与实现、设计与测试之间的关联关系,并通过需求规格说明书、概要设计说明书、详细设计说明书、接口文档及测试用例等相关文档进行记录和维护。对于重要的架构决策、功能模块及关键技术方案,应明确其对应的需求依据和设计目的,保证设计内容具有清晰、完整的来源和依据。
通过加强软件设计的可追溯性,可以提高设计过程的规范性和透明度,便于项目成员理解系统设计思路,也有助于开展设计评审、变更管理、问题定位和测试验证工作。同时,在项目后期进行系统维护和功能扩展时,可依据已有的追溯关系快速了解相关模块的设计依据及影响范围,提高软件维护和演进的效率。
7.4 架构决策记录
ADR(Architecture Decision Record,架构决策记录)是一种轻量的架构知识管理方式。它是一种用于记录软件架构重要决策及其背景信息的文档形式,主要用于说明在架构设计过程中采用某项技术方案或架构方案的原因。与传统设计文档主要描述“系统采用了什么方案”不同,ADR更加关注“为什么做出这样的选择”,从而保留架构决策过程中的背景、约束条件、备选方案及权衡结果。
在软件架构设计过程中,对于系统架构、技术选型、框架选择、数据存储方式、通信机制以及关键模块设计等具有较大影响的决策,应及时通过ADR进行记录。ADR通常包括决策背景、问题描述、候选方案、决策内容、选择原因、优缺点及可能产生的影响等信息。通过对这些内容进行完整记录,可以使项目成员了解架构决策的依据,避免因人员变动或时间推移导致决策背景丢失。
ADR还能够为后续的设计评审、需求变更和系统维护提供参考。当业务需求、技术条件或系统运行环境发生变化时,可以通过已有ADR了解当初的设计考虑,并重新评估原有决策是否仍然适用。因此,ADR不仅是架构设计过程的记录,也是保证软件架构可追溯性和可维护性的重要手段,有助于提高架构决策的透明度和一致性。
7.5 设计债务与技术债务
设计债务与技术债务是软件开发过程中由于时间、成本、资源或业务需求等因素,在设计和实现阶段采取临时性、折中性方案而形成的潜在问题。虽然这些方案能够在短期内满足项目进度或功能交付要求,但如果长期得不到处理,可能会导致系统复杂度增加、维护成本上升,并对后续功能扩展和系统演进产生影响。
设计债务主要体现在软件架构和系统设计层面,例如模块划分不合理、模块之间耦合度较高、接口设计不完善、职责边界不清晰以及架构扩展能力不足等。技术债务则更多体现在具体实现层面,例如代码重复、实现方式不规范、测试覆盖不足、依赖组件版本过旧以及临时解决方案长期保留等。这些问题在短期内可能不会直接影响系统运行,但随着系统规模扩大,其维护和修改成本通常会逐渐增加。
在项目开发过程中,应对设计债务和技术债务进行识别、记录和持续管理。对于因项目进度等原因暂时无法解决的问题,应明确产生原因、影响范围、风险程度及后续处理计划,并在适当阶段进行重构和优化。通过建立债务清单并结合版本迭代持续偿还,可以避免技术债务不断累积,保持系统架构的稳定性、可维护性和可演进能力。
因此,设计债务与技术债务并不意味着所有临时方案都是错误的,而是一种对当前成本与未来成本进行权衡后的结果。关键在于对债务保持可见、可追踪,并根据项目实际情况合理控制和偿还,从而实现开发效率与长期软件质量之间的平衡。
7.6 从设计到实现、测试与反馈的闭环
软件开发并不是一个单向、一次性的过程,而是由设计、实现、测试、反馈和持续改进组成的循环过程。设计阶段根据业务需求和质量目标确定系统架构、功能模块、接口及关键技术方案;实现阶段依据设计成果完成具体功能开发,并通过代码评审等方式保证实现与设计的一致性;测试阶段则通过功能测试、性能测试、安全测试等手段验证软件是否满足需求和设计目标。
在测试和实际运行过程中发现的问题,应及时反馈到设计和实现阶段进行分析和处理。对于功能缺陷,可以通过修改代码和补充测试用例进行解决;对于设计层面的问题,则需要重新评估相关架构方案和设计决策,必要时进行架构调整或重构。同时,应将重要的问题处理过程、设计变更及测试结果进行记录,使相关修改具有明确的依据和可追溯性。
通过建立“需求—设计—实现—测试—反馈—改进”的闭环机制,可以使软件在持续迭代过程中不断发现和解决问题,并将测试结果和实际运行反馈转化为后续设计优化的重要依据。该闭环不仅能够提高软件质量,也有助于及时控制设计债务和技术债务,保证系统能够随着业务需求和运行环境的变化持续演进。
小  结
软件设计不是编码之前的一次性活动,也不是 UML 图的集合。它的本质是在需求、约束和质量属性的共同作用下,通过合理划分职责和边界、建立稳定的协作契约、控制依赖与复杂性,并对各种工程目标进行权衡,形成能够被实现、验证和持续演进的软件结构。本章可以归纳为七个核心认识。
第一,设计连接问题空间与解决方案空间
需求描述问题,设计构造解决方案,实现将解决方案变成可执行软件。
第二,设计具有多个层次
软件设计可以分为不同层次,不同层次解决不同问题。
第三,边界是软件设计的核心
模块、组件、服务、限界上下文等,本质上都在回答:哪里应该形成边界?好的边界可以隔离:变化、复杂性、责任、数据、团队协作。
第四,设计原则不是绝对规则
SoC、抽象、信息隐藏、低耦合、高内聚、SOLID 等原则应该用于帮助我们思考,而不是机械套用。设计最终需要进行权衡。
第五,分布式架构不是免费升级
从模块到服务,从单体到微服务,会带来新的能力,也会带来新的复杂性。因此,架构复杂度应该由真实需求驱动,而不是由技术潮流驱动。
第六,质量属性必须进入设计
现代设计不能只考虑“功能能不能实现”,还必须考虑:性能、可用性、可靠性、安全性、可扩展性(可伸缩性)、可维护性、可测试性、可观测性、成本、可持续性。
第七,设计本身也需要持续演进
软件不会在第一次设计之后永久保持不变。真正成熟的软件工程体系应该能够:设计→实现→验证→运行→观测→反馈→重构→重新设计。
因此,可以概括为:软件设计的目标不是预测未来,而是建立能够应对未来变化的结构。进一步说:好的软件设计,不是让变化消失,而是让合理的变化变得可控。

相关学习资料