如何进行软件设计 - OSCHINA - 开源 × AI · 开发者生态社区

发布时间:2026/11/3 2:51:47

如何进行软件设计 - OSCHINA - 开源 × AI · 开发者生态社区 首发于wx【小小寰宇】软件开发中有一个常见的误区拿到需求就开始编码边做边想。这种做法在小型项目中或许可行但面对复杂系统时缺乏设计的后果往往是返工、烂尾或者难以维护。软件设计本质上是一种预先的思考——把我们从代码细节中抽离出来用更高层次的抽象去审视问题我们要解决什么为什么值得解决可能的方案有哪些设计文档是这种思考的成果但不是流水账不是功能列表更不是代码的翻版。它是逻辑的载体——记录我们如何从问题走到解决方案。一、设计目的回答为什么而非做什么设计的起点是目的而不是任务。实现一个 RPC 通讯协议不是目的这只是描述要做什么。真正的目的应该是“为分布式服务提供一种高效的远程调用机制使得调用方能像调用本地函数一样调用远程服务同时保证可靠性和性能。”区分目的和任务| 任务 | 目的 | | — | — | | 实现缓存模块 | 解决热点数据的重复计算问题响应时间降至50ms以内 | | 引入消息队列 | 解耦生产者和消费者避免同步调用导致的系统阻塞 | | 重构数据库访问层 | 统一数据访问模式消除散落的SQL语句 |用目的表述的好处从当前状态出发设计总是从项目当前的状态开始。如果接手的是已有系统设计的目标是改进这个系统而不是构建一个理想中的系统。明确起点有助于避免两种错误•历史虚无主义忽视现有实现设计出没人能用的完美方案•路径依赖陷阱被现有实现绑架只是在打补丁记录起点Git版本号、时间戳或简单描述在XXX功能已上线、YYY模块开发中的情况下设计Z特性。二、统一语言团队共享的领域词汇DDD 中有一个核心概念叫通用语言Ubiquitous Language——同一限界上下文内所有人用同一套术语描述领域无歧义、无冗余。软件设计同样如此最重要的工作之一是建立统一语言。语言塑造思维语言不仅是描述工具更是思考工具。用模糊的语言思考问题思维本身就会变得模糊。两种思考方式对比用基础设施语言思考“客户端连接服务端发送请求数据服务端处理后返回响应。”思考的问题如何建立连接如何组织数据如何传输字节解决方案向 Socket 编程靠拢。用领域语言思考“调用方Caller向被调用方Callee发起 RPC 调用。调用方可能重试被调用方可能返回异常。”思考的问题调用方和被调用方的职责边界在哪里重试策略如何设计异常如何分类解决方案向领域模型靠拢。两种语言导向两种不同的设计。当我们谈论同一个问题就应该使用同一套语言。不是先有代码再有文档而是先有共享的语言语言会引导我们走向一致的设计。知识消化语言是如何产生的DDD 强调知识消化Knowledge Crunching——团队通过与领域专家的持续对话从混沌的业务知识中提取出精确的领域模型。语言不是预先定义的而是在解决问题的过程中逐渐清晰问题 → 讨论 → 发现歧义 → 命名澄清 → 更精确的问题 → 更精确的语言以订单系统为例不是先定义术语表再开始工作而是在解决问题的过程中自然产生术语。限界上下文不同语境的隔离有些术语在不同上下文中含义不同。例如订单销售领域指销售订单仓储领域指出库单财务领域指应收账款。DDD 通过限界上下文隔离这种歧义每个上下文有自己独立的通用语言。软件设计也需要这种意识设计文档中引入新术语时明确它属于哪个上下文例如**调用方Caller**RPC上下文发起RPC请求的进程。是RpcCall的发起者。 **被调用方Callee**RPC上下文接收并处理RPC请求的进程。是RpcCall的接收者。 **连接Connection**网络上下文与目标进程建立的TCP连接。一个连接可以承载多个调用。说出口的语言就是模型DDD 有句名言“说出口的语言就是你的模型”。团队讨论时如果不得不绕着弯说话说明模型还不够清晰。设计文档同样如此读起来别扭、充满例外和补充说明说明语言还不够统一。好的设计文档•术语前后一致——全文用同一套词汇不出现同义词混用•语言即代码——代码中的类名、方法名与设计文档的术语对应•团队共享——开发、测试、产品都能读懂术语应该像呼吸一样自然融入设计的每个句子中。但这不意味着变成术语手册——文档读起来应该流畅、直击要害。统一语言必须在代码中落地。否则语言和代码是两张皮沟通成本不减反增| 设计文档中的术语 | 代码中的体现 | | — | — | | 调用方Caller |class Caller或interface Caller| | 发起RPC调用 |caller.call(method, args)| | 重试策略 |RetryPolicy枚举或配置类 |代码即模型。代码中的类名、方法名、变量名与设计文档术语不一致说明设计没有真正指导实现。实践每引入一个新术语同时在设计文档中定义、在代码中实现。三、设计逻辑抓住核心取舍设计逻辑回答我们如何达成目标核心挑战而不是面面俱到很多设计文档的错误是试图列出所有细节结果变成技术手册。设计逻辑的核心是识别2-3个关键挑战说明选择什么方案、为什么不用其他方案。以缓存系统为例关键取舍•淘汰策略为什么选 LRU 而不是 LFU因为假设最近访问的数据更可能再次被访问。这个假设是否成立需要验证。•一致性与性能业务允许短暂不一致可以设较长 TTL 提高性能需要强一致性则需要更复杂的同步机制。这两个取舍决定设计走向其他参数初始容量、扩容步长等可以在实现时调整。多个逻辑视图复杂系统的设计往往需要多个逻辑视图。例如设计高性能网络通讯框架功能视图建模协议栈的封装关系性能视图计算端到端延迟组成每个视图解决不同问题•功能视图数据如何流转模块如何协作•性能视图瓶颈在哪里优化的边际收益是多少•状态视图系统有哪些状态状态之间如何转换根据设计目的选择重点不需要穷举所有视图。分析与已有模块的关系确定设计逻辑之前需要回答新功能放在哪里实现| 情况 | 决策 | | — | — | | 新功能完全独立于现有系统 | 新建模块 | | 新功能是对现有模块能力的扩展 | 扩展现有模块不在本层重复实现 | | 新功能依赖现有模块但有清晰的边界 | 保持依赖模块接口不变在本层封装 | | 新功能与现有模块职责重叠 | 重构边界明确每个模块的职责 |反模式在新模块中重复实现依赖模块已有的能力。浪费开发资源还会导致后续维护困难——依赖模块升级时重复代码如何同步四、核心数据结构围绕问题建模设计逻辑是骨骼核心数据结构是肌肉——决定系统如何存储、传递和处理信息。围绕领域对象建模DDD 强调领域模型——软件要解决的业务问题在代码中的投影。领域模型不是数据库表的映射不是 API 请求/响应的结构而是业务概念在代码中的存在形式。RPC 领域的核心领域对象•RpcCall一次完整的远程调用包含调用方、被调用方、方法名、参数、结果/异常•RpcRequest从调用方发往被调用方的消息携带调用标识和参数•RpcResponse从被调用方返回给调用方的消息携带执行结果或错误信息•Connection调用方与被调用方之间的通信通道可承载多次调用抓住核心领域对象其他数据结构往往是它们的组合或衍生。例如重试队列中的元素就是RpcCall和重试次数的组合。定义关键成员而非全部细节设计阶段不需要定义每个字段的精确类型。重点是明确有哪些关键属性、这些属性之间的关系struct RpcRequest{string caller_id;// 调用方 ID string method_name;// 方法名 bytes arguments;// 序列化后的参数 string trace_id;// 追踪 ID};string还是string_view、用 JSON 还是 Protobuf 序列化是详细设计阶段的决策。数据结构之间的关系当有多个核心数据结构时需要描述它们之间的关系五、接口定义表达设计决策接口是设计的最终交付物——它连接了设计文档和代码实现。关注是什么而非怎么用设计文档中的接口定义重点是说明•接口的职责是什么•它与其他接口的关系是什么/** * 建立到指定服务的连接。 * * 设计意图连接是会话的起点一个连接对应一个长连接生命周期。 * 与 connect()对应的是 close()中间通过 send()/recv()进行通讯。 * * param service_address 目标服务的地址格式为host:port* return 连接句柄后续操作使用此句柄 */ ConnectionHandle connect(const std::stringservice_address);/** * 在连接上发送RPC请求。 * * 注意此函数是异步的调用返回时请求可能尚未到达对方。 * 调用方应通过 response()或回调获取响应。 * * param conn 已建立的连接 * param request RPC请求 * return 请求ID用于追踪和获取响应 */ RequestId send(ConnectionHandle conn, const RpcRequestrequest);这样的接口描述不仅说明了用法更传达了设计决策为什么发送是异步的、为什么要区分连接和请求。总结接口之间的关系定义多个接口后总结它们如何协作通过connect()建立连接用send()发送请求、response()获取响应通讯完成后close()释放连接。流程connect() → send() → response() → close()接口关系一目了然也为代码审查和测试提供检查点。内部接口同样重要设计涉及多个内部模块时模块之间的接口同样需要描述。例如 RPC 框架可能包含•编码层将请求/响应对象序列化为字节流•传输层将字节流可靠地发送到对端•路由层根据服务名找到目标地址传输层的消息格式是一种内部接口struct TransportMessage{uint32_t magic;// 协议标识 uint16_t version;// 协议版本 uint32_t body_length;// 消息体长度 uint8_t body[];// 消息体编码层的输出 uint32_t checksum;// 校验和};描述这个格式是为了帮助读者理解发送的消息具体是什么样的不是写协议规范。六、一致性校验设计的自我审视设计完成后需要进行系统性的一致性检查确保设计没有内在矛盾。检查维度| 维度 | 检查项 | | — | — | |概念一致性| 同一概念是否全文使用相同术语有没有同义异名或异义同名 | |状态完备性| 每个状态机的每个状态是否处理了所有可能的输入非法输入如何处理 | |接口完备性| 设计逻辑中提到的每个操作是否都有对应的接口 | |层次一致性| 本层接口是否承担了属于其他层的职责依赖关系是否清晰 |一个状态机的检查示例假设设计一个连接管理器检查点•状态完备性Disconnected 状态下调用close()会发生什么•事件处理connect()在 Connecting 状态被再次调用是报错还是排队•异常恢复网络断开后立即重连状态转换是否正确这种检查往往能发现设计中的漏洞。如果在实现阶段才发现修复成本会非常高。七、迭代完善设计是动态的设计不是一次完成的。随着对问题的深入理解、需求的变化、测试的反馈设计需要迭代演进。保持设计时间线每次迭代修改设计文档时回顾最初的设计目的和起点。典型场景关键迭代后的设计文档应该假设从一开始我们就选择了时间堆而不是描述先用了轮询后来换成时间堆。后者混淆了目标和中间方案——目标是实现一个高效的定时任务调度器不是实现轮询算法。结语设计是一种能力软件设计不是套模板、填表格的机械工作而是思考能力的体现。好的设计者具备以下特质•抽象能力从具体问题中提取本质忽略无关细节•取舍意识理解没有完美的方案只有最适合当前场景的选择•沟通能力用清晰的语言表达复杂的逻辑让设计文档成为团队协作的桥梁•批判思维不断质疑自己的假设主动寻找设计的漏洞设计能力的提升没有捷径。但有一个简单的方法认真对待每一个设计文档。把每一次写设计文档当作思维训练的机会而不是完成任务的形式主义。假以时日对问题的理解更深了设计的质量更高了沟通的成本更低了。设计改变代码更改变你。
延伸阅读

更多相关文章

2026/11/3 2:50:44

微软感知计划深度解析:AI时代网络安全防御的全新范式

当攻击者开始用人工智能以毫秒级速度发起渗透时,企业的安全团队还在手动翻阅成千上万条告警日志。这种攻防节奏的断层,已经成为当下网络安全领域最尖锐的矛盾。 微软给出的答案,是一套从零为AI时代打造的智能体安全系统——Project Percepti…

2026/10/25 13:18:38

近红外光谱分析:从分子振动原理到化学计量学建模实战

1. 从分子振动到光谱信号:理解近红外的起点搞了近十年的光谱分析,从实验室的傅里叶变换红外到现在的便携式近红外,我最大的感触是:很多人一上来就想调模型、跑算法,却把最根本的物理图像给丢了。这就像盖楼不打地基&am…

2026/11/2 1:34:50

计算机保研夏令营全攻略:从材料准备到八校考核实战复盘

1. 夏令营申请季的“信息战”:我的复盘与策略又到了一年一度计算机相关专业保研夏令营申请的高峰期。最近后台和私信里,很多大三的学弟学妹都在问同一个问题:“学长,我该报哪些学校?怎么准备才能提高入营和优营的概率&…

2026/11/3 2:49:53

HTTP 协议学习笔记:从报文结构到抓包实践

一、HTTP 概述本节先厘清 HTTP 的定位:它传输什么、处在网络模型的哪一层、用在哪些场景,以及它的通信模型。这些是后面理解报文、方法和缓存的基础。1. 超文本传输协议HTTP(HyperText Transfer Protocol,超文本传输协议&#xff…

2026/11/3 2:49:53

英伟达:双重采样修正强化学习奖励

📖标题:Double-Rollout Reinforcement Learning with Sampling-based Reward Estimation 🌐来源:arXiv, 2610.16593v1 🛎️文章简介 🔸研究问题:在训练大模型进行数学或代码推理时,如…

2026/11/3 2:49:53

卡不等于分支:我用 TRAE 宝配合 Hive 跑三张任务卡的一天

卡不等于分支:我用 TRAE 宝配合 Hive 跑三张任务卡的一天 一、场景:一张卡就是一个任务 项目组有一套自建的 Hive,用来做任务发放与流转,网页端就能看板。它的基本单位是"卡":一张卡就是一个任务&#xff0c…

2026/11/3 2:44:52

【维克】商品CTA策略:期货市场的量化主战场

"善战者,求之于势,不责于人。"——《孙子兵法兵势篇》将军不苛责士兵,而是善于借助"势"。量化交易也一样——高手不跟市场较劲,而是顺着市场的水势,顺势而为。而商品期货,就是那个&quo…

2026/11/2 6:08:01

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/2 6:39:12

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/3 1:04:48

STM32实战:基于MAX14431数字隔离器的SPI隔离通信方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/3 1:04:48

工业SPI隔离通信设计:MAX14431BAEE+与STM32L4A6RG实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/3 1:04:48

STM32+NRF24L01无线抢答器:硬件设计、通信协议与低功耗实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/2 6:08:01

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/2 6:39:12

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑