1. 概要设计和详细设计:先分清“定骨架”和“填血肉”
不少刚入行的工程师容易把概要设计和详细设计混为一谈,甚至觉得“设计嘛,就是画几张图,写几个文档,差不多就行了”。真等到项目进入开发、测试、验收阶段,才发现设计文档里全是槽点:接口字段对不上、模块边界含糊、数据库表结构漏了关键约束,最后只能一边写代码一边改设计,项目延期不说,返工成本高得吓人。
我做了这么多年软件项目,最大的体会就是:概要设计和详细设计是两件完全不同的事,前者回答“系统长什么样、分成几块、块与块之间怎么说话”,后者回答“每一块内部到底怎么实现、每个类怎么写、每个表怎么建”。说得更直白一点,概要设计是给系统搭骨架,详细设计是往骨架里填血肉。骨架搭歪了,后面填什么都别扭;血肉没填到位,骨架再合理也跑不起来。
这篇内容适合谁看呢?一是刚入行的开发工程师,想搞明白设计文档到底怎么落地;二是带项目、做评审的团队骨干,想梳理出一套能真正指导开发的设计方法;三是准备软考或者走技术晋升的同行,需要把“概要设计”“详细设计”的概念和实操彻底吃透。下面我会结合一个常见的业务系统案例,把两份设计的核心思路、具体做法、踩坑点一次说清。
1.1 概要设计完成的标志:系统边界、模块划分、接口契约都定了
很多人写概要设计说明书,喜欢堆一堆高大上的架构图、流程图,但问起“系统有哪几个模块”“每个模块对外提供哪些服务”“模块之间是同步调用还是异步消息”“数据存在哪里、怎么流转”,反而答不上来。这说明概要设计根本没做到位。
概要设计阶段,核心产出物是系统架构设计、模块划分、接口定义、数据结构设计、部署方案。它的验收标准不是“图够不够漂亮”,而是“任何一名开发拿到这份文档,都能清楚知道自己的模块从哪来、往哪去、跟谁对接、数据怎么存”。换句话说,概要设计做完,技术选型和整体结构就不能再大改了,后续开发应该是在这个框架里细化,而不是推翻重来。
我在实际项目中通常会做一件事:在概要设计评审会上,让每个模块的负责人用自己的话复述一遍整体架构、自己模块的职责、上下游依赖关系。如果复述不出来,说明设计没吃透,评审不予通过。这个方法看起来很土,但比看文档有效得多。
1.2 详细设计完成的标志:每个类、每个接口、每个表结构都有明确实现方案
详细设计阶段,关注点从“系统级”下沉到“模块级”“类级”。它要回答的问题是:某个功能点由哪几个类协同完成?每个类有哪些属性和方法?方法内部的关键流程是什么?数据库表有哪些字段、字段类型是什么、索引怎么建?接口的请求参数、返回结果、异常码各是什么?界面元素与逻辑层的交互怎么走?
详细设计的粒度要细到“开发人员拿到后,不需要再拍脑袋做决策,直接翻译成代码即可”。我见过很多团队的详细设计文档,写得跟概要设计差不多,全是空泛的“模块负责某某功能”,没有任何类图、序列图、字段说明、异常流程。这种文档本质上是一堆废话,对开发毫无帮助,评审时应该直接打回。
1.3 用建房子的思路理解两份设计
把软件开发比作盖楼会更直观。概要设计相当于建筑方案设计:楼盖多高、分几层、每层是什么功能、承重墙在哪、水电暖通的管井走哪、电梯和楼梯怎么布置。这些定了,施工队才能进场。详细设计则相当于施工图:每一堵墙的厚度、每一根钢筋的规格、每一处水电管线的精确位置、每个装修材料的品牌型号。没有施工图,工人根本没法干活。
“盖到一半再改方案”是建筑行业的大忌,软件项目同理。概要设计阶段多花一周时间把整体结构想清楚,后面省下的是几周甚至几个月的返工时间。这个道理很多人懂,但一到实际项目里,就因为“进度紧”“先跑起来再说”把设计环节压缩得可怜,最后反而更慢。
2. 概要设计实操:五件事决定系统走向
这部分我结合一个实际做过的客户管理系统来拆解。当时的需求是:管理客户信息、跟进记录、订单数据、统计报表,支持多角色登录和权限控制。看上去不复杂,但如果不做概要设计直接写代码,很快会发现客户、订单、跟进记录之间的数据关系理不清,权限逻辑散落各处,报表查询慢到没法用。
2.1 架构风格选型:别上来就微服务
听到“微服务”三个字,很多团队就兴奋,觉得不用微服务就显得不够先进。但微服务是有代价的:分布式事务、服务治理、链路追踪、部署复杂度都是实打实的成本。一个客户管理系统,用户量几百人,日均请求量几千次,单体应用加个缓存就绰绰有余,强行拆微服务只会让开发效率直线下降。
我在概要设计阶段首先问自己三个问题:系统预期的并发量是多少?数据量级到多少?团队规模和维护能力怎么样?这三个问题想清楚了,架构选型自然就有答案。小型业务系统用单体分层架构加关系型数据库就好;真正需要水平扩展、独立部署、分团队并行开发的时候,才考虑微服务。具体到那个客户管理系统,我选的是经典的分层架构:表现层、业务逻辑层、数据访问层,加一个MySQL做主存储,Redis做热点数据缓存。简单、可靠、团队里人人都熟。
2.2 模块划分:高内聚低耦合不是口号
模块划分是概要设计的核心工作,也是很多团队做得最敷衍的部分。常见错误有两种:一种是把模块划分等同于按功能页面划分,客户管理、订单管理、统计报表各算一个模块,结果订单模块要查客户信息、统计模块要读订单数据,模块之间交叉调用严重;另一种是模块粒度太大,“公共模块”这种大筐什么都往里装,最后公共模块成了垃圾堆。
我采用的划分方法是“按业务能力划分”。客户管理模块负责客户信息和标签;跟进模块负责跟进记录的增删改查;交易模块负责订单、合同和付款;报表模块负责统计查询;系统管理模块负责用户、角色、权限。每个模块只关心自己领域内的逻辑,对外暴露清晰的接口,模块之间通过接口交互,不允许跨层直连数据库。这样做的好处是:每个模块都能独立分配、独立测试、独立维护,改一个模块的内部实现不影响其他模块。
这里有一个实操细节:模块划分的时候,建议先把核心业务对象找出来,比如客户、订单、跟进记录、用户。然后围绕这些对象圈定各自的行为和关系,再按照“行为聚集”的原则合并成模块。如果两个对象之间的操作总是成对出现,比如“下单”必然要“改客户状态”,就要考虑是不是同一个模块更合适。
2.3 接口设计:先定契约再写实现
概要设计阶段必须把模块间的接口契约大致定下来。注意,这里“大致”不是说可以模糊,而是指接口的请求方、响应方、主要出入参、同步还是异步要明确,但具体字段的细节可以放到详细设计阶段补充。
举例来说,跟进模块需要通知交易模块“客户已被重点跟进”,概要设计阶段就要定义这个事件的名称、触发时机、关键载荷,比如客户ID、跟进状态码。至于这个事件是走HTTP回调还是MQ消息,数据格式是JSON还是对象序列化,可以在详细设计阶段再定。但事件本身是否存在、由谁发起、谁消费,必须在概要设计里说清楚。
我习惯在概要设计阶段画一张模块依赖矩阵,横轴和纵轴都是各个模块,交叉点标注依赖关系和数据流向。这样做有两点好处:一是能直观发现循环依赖,比如A模块调B模块、B模块又调A模块,这种必须调整;二是评审的时候大家对着矩阵挨个过,谁也说不出“我没看到依赖”这种话。
2.4 数据设计:概要阶段就要把数据流理顺
数据设计不是数据库设计师一个人的事。概要设计阶段,我们需要梳理系统的核心数据实体、实体之间的关系、数据在哪产生、在哪消费、在哪存储。关系型数据库还是NoSQL、要不要分库分表、数据要不要归档,这些问题在概要设计阶段就要有倾向性方案。
客户管理系统里,核心实体是客户、联系人、跟进记录、订单、订单明细。实体关系上,一个客户有多个联系人和多条跟进记录,一个订单属于一个客户、包含多个明细。单据关系其实比较清晰。真正需要花心思的是数据量评估:如果客户量预计百万级,跟进记录按每年每条客户几十条算,几年下来就是千万级甚至亿级。这时候就要考虑历史数据归档策略,或者初期就按时间分区建表,否则报表查询会成为灾难。
注意:数据量评估宁可高估一点也不要低估。我在另一个项目里就吃过亏,初期觉得数据量小,没考虑分区,结果上线半年后单表数据过千万,一条统计查询跑十几秒,最后只能停服迁移。这个代价比一开始多做一版数据设计要大得多。
2.5 概要设计说明书:写文档不是写作文
很多团队写概要设计说明书,追求长篇大论,动辄上百页。其实好的概要设计说明书应该是结构清晰、图文结合、点到要害。我的写法是:背景与目标、名词解释、架构设计(含架构图)、模块划分与职责、核心接口清单、数据结构概览、部署架构、风险与对策、附录。
架构图这里要注意,不要画那种花哨的、每层塞满十几个小方块的图。好的架构图让人一眼看懂系统分几层、每层的关键组件是什么、数据流向是什么。画完之后,拿给一个没参与需求讨论的同事看,如果他能在三分钟内说出系统的大致结构和主要模块,这张图才算合格。
部署架构容易被忽略,但它是概要设计里跟运维强相关的重要部分。应用部署在几台机器、数据库独立还是与应用同机、是否需要负载均衡、备份策略是什么,这些都要写清楚。项目进入验收阶段,“部署方式是否与设计一致”常常是验收组重点检查的内容,提前设计好能省掉很多麻烦。
3. 详细设计实操:把每个模块彻底钉死
概要设计完成后,进入详细设计阶段。这个阶段的工作量往往比概要设计大得多,因为它要把每个模块的内部实现方案一个个落定。很多团队在概要设计上花了大力气,到了详细设计就松懈,结果代码里各种临时设计、乱七八糟。
3.1 类设计:从模块职责推导类和职责
以客户管理模块为例,概要设计明确了它负责客户信息的全生命周期管理。到了详细设计阶段,就要落地到类级别。我会列出这些类:Customer实体类、CustomerRepository数据访问类、CustomerService业务逻辑类、CustomerController接口层类,以及CustomerValidator校验类。每个类的职责必须单一,比如CustomerService只做业务逻辑编排,不直接写SQL,SQL封装在CustomerRepository里。
这里有个特别实用的原则:如果一个类的private方法超过七八个,或者方法行数普遍超过50行,大概率是职责过重了,要考虑拆分。类设计不是一次到位的,等代码写起来发现不对劲,及时调整类结构比硬撑着写完要好得多。
对于类之间的关联关系,我习惯用UML类图表达,标注好继承、实现、聚合、依赖关系。类图不必精确到每个getter/setter,但关键的业务方法必须标明,因为这决定了类与外部协作的方式。
3.2 时序图与状态机:动态行为不能靠想象
静态的类图还不够,详细设计要能描述对象之间的动态协作。比如“用户新增一条跟进记录”这个操作,涉及Controller、Service、Repository、数据库四层交互。用文字描述又长又容易歧义,画一张时序图,谁先调用谁、谁返回什么、异常在哪一层抛出,一目了然。
状态机是详细设计里容易被忽视但又极其重要的内容。典型场景是订单状态流转:待付款、已付款、处理中、已完成、已取消、已退款。每个状态之间哪些迁移是允许的,哪些是禁止的,状态迁移需要满足什么条件,触发后要执行哪些动作。把这张状态机图理清楚,代码里的if/else会少很多,很多非法操作在入口处就能拦截。
我在实际编码中体会特别深:很多线上BUG,比如“支付回调重复处理导致订单状态错乱”“取消订单后还能继续发货”,根源都是详细设计阶段状态机没画清楚,开发者凭感觉写判断条件,状态之间出现漏洞。所以状态机设计必须覆盖到每一个核心业务对象的状态变化,尤其是异常路径:支付超时、回调失败、人工介入修改状态,都要考虑进来。
3.3 数据库表结构设计:字段级精确到类型约束
详细设计阶段的数据库设计,必须精确到字段级别。以客户表为例,要列出每个字段的名称、类型、长度、是否允许为空、默认值、索引类型、备注。客户姓名用varchar(50)还是varchar(100),手机号存字符串还是数字,性别字段用tinyint还是char(1),这些细节不决定清楚,开发的时候每个人按自己习惯建表,整个系统的数据风格就会非常混乱。
索引设计上,外键字段一般要加索引,高频查询条件的字段要建联合索引,查询很慢但又不适合建索引的字段,要考虑是否用冗余字段或缓存解决。这里有一个常见坑:开发初期数据量小,索引有没有都无所谓,等项目上线半年数据量上来,慢查询一个个冒出来才追悔莫及。详细设计的时候就把索引规划好,后面能省很多事。
对于枚举状态字段,我强烈建议使用整数类型加状态字典表,或者用字符串字面量但必须统一编码,比如订单状态用0/1/2/3还是“PENDING/PAID/DONE”,必须全项目统一。否则A服务返回1表示已支付,B服务把1当作待支付,联调排查起来非常崩溃。为了避免这种问题,我会在详细设计文档里附带一份“码表说明”,把所有业务状态码集中列出来。
3.4 核心算法与异常处理:全局兜底比不停地加if重要
详细设计里还要覆盖关键算法流程和异常处理策略。客户管理系统里有个典型算法:根据跟进频率和最近跟进时间计算客户活跃度,并给出“高/中/低”三档标签。这个计算的公式、权重、更新时机,要在详细设计里写明,否则开发自己发挥,不同人的计算结果不一致,产品验收时必然出问题。
异常处理上,要定一个统一方案。我个人推荐全局异常处理器加统一错误码体系:Controller层不捕获业务异常,由全局处理器统一处理并返回标准化错误JSON;每类业务异常对应一个自定义异常类和一个错误码,例如客户端参数错误编码10001、权限不足10003、数据不存在10004。千万不要每个Controller自己try/catch后随便返回一个“出错了”的字符串,那样前端没法根据错误码做差异化提示,排查问题也难。
注意:异常处理是详细设计里最容易被“忽略”但又最影响体验的部分。文档里最好列出常见异常场景及处理方式,至少要覆盖参数校验失败、业务条件不满足、外部依赖超时、数据库操作失败这四类。
3.5 详细设计说明书的写作节奏
详细设计说明书该怎么组织,我会按照模块分章:每一章先描述模块职责,然后是类图、核心时序图、状态机、接口定义表(含请求参数、返回参数、异常码)、数据库表结构、关键算法、涉及的外呼接口和中间件配置。这样开发一个模块,只看对应的那一章就够了,不用去翻整本厚文档。
还有一个节奏上的建议:详细设计不必一次性全部做完再评审,可以按模块分批评审。比如先把客户管理模块详细设计做完,评审通过后这个模块就进入编码阶段,边做后续模块的详细设计。这样既能保证质量,又能加速项目推进,团队人员也不会闲着等待。
4. 从概要设计到详细设计:落地过程中的真实经验
设计和编码之间的鸿沟,往往是项目延期和返工的高发区。下面说几个我自己摸爬滚打总结出来的经验,希望能帮你少走弯路。
4.1 设计评审的节点和怎么准备
概要设计和详细设计都需要正式评审,但评审的重点完全不同。概要设计评审重点看架构合理性、模块划分是否清晰、接口契约是否完备、技术选型是否匹配需求;详细设计评审重点看类设计是否合理、时序图是否覆盖关键路径、表结构是否完备、异常处理是否到位。
评审组织方面,我建议提前两天把设计文档发给所有参会人员,要求先看再评。会上按模块过设计,每个模块负责人先讲,其他成员提问题。为了让评审不流于形式,我会在文档末尾附一个“设计自查清单”,让作者在提交评审前先逐条自查。清单包括:模块职责是否一句话能讲清;是否存在循环依赖;接口是否有版本控制;表结构是否有索引缺失;异常路径是否全部处理;关键算法是否有伪代码或公式。
4.2 需求变更怎么影响两份设计
需求变更是软件项目永恒的主题。最怕的是变更来了,代码直接改,文档不更新,半个月后文档跟代码彻底对不上。我的经验是:小变更直接修改详细设计文档对应章节并记录变更日志;中等变更涉及接口调整或表结构变化,要评估影响范围,更新详细设计后通知相关模块负责人;重大变更影响系统架构或核心模块拆分,必须重新评审概要设计。
这里有个实操技巧:设计文档里每次修改,建议在文末附一张变更记录表,写明变更日期、变更人、变更内容、影响模块。这张表非常有用,不光是追溯历史,更重要的是评审新一轮设计时,大家能快速看到和上一版的差异,不用整份重新看。
4.3 文档和代码同步:不维护等于没写
说到文档和代码的同步,老实讲,大部分团队做不到。原因很现实:项目赶进度时,写代码的时间都不够,谁还顾得上更新设计文档。但真相是,当一个项目进入维护期、交接期、验收期,设计文档的作用才真正体现出来。
我自己的做法是:把设计文档放在代码仓库里,和源码一起管理。模块设计文档放在对应模块的docs目录下,修改代码的同时如果涉及设计的调整,顺手改掉文档。代码评审的时候,如果设计文档没有同步更新,评审不予通过。这个制度刚推行时会觉得麻烦,但坚持几次之后,大家就习惯了,维护成本并没有想象中高。
项目验收时,验收组经常要检查“概要设计说明书”“详细设计说明书”这些文档是否与实际系统一致。很多项目在验收前临时补文档,补出来的东西跟实际代码脱节,这时候不仅评分难看,还可能暴露管理上的混乱。所以平时把文档维护好,到验收时能省下大把临时加班的时间。
5. 常见问题与排查思路实录
最后这部分,我整理了一些在设计和开发衔接过程中反复出现过的问题,供你参考对照。
5.1 接口讨论不清导致联调返工
这是出现频率最高的问题。两个模块各自写接口,A模块的开发者认为“状态”传数字1/2/3,B模块的开发者觉得应该传“PAID/UNPAID”,联调时才发现不一致,只能改代码。根源就是详细设计阶段接口定义没做到字段级精确。
排查思路很简单:详细设计评审时,重点核对接口出入参的每一个字段——字段名、类型、长度、取值范围、是否必填、默认值。最好把接口定义直接写进文档,甚至用JSON Schema片段表述返回结构。这样开发时照着字段定义做,联调时问题会大幅减少。
5.2 过度设计:把简单系统搞复杂
过度设计和没设计是一对极端,但过度设计更隐蔽。比如客户管理系统非要上事件驱动架构,每个操作都发一条消息;或者为了“将来可能用到”,给每个表都加五六个冗余字段和一整套复杂的状态机。这些设计表面上很完整,实际上增加了大量开发和维护成本,却换不来实际价值。
我的判断标准很简单:当前明确的需求如果都不需要某项设计,就不要引入。架构上保持“最小可行”原则,预留扩展点可以,但不要提前实现。真到了需要的时候,基于合理的设计扩展并不难;但被过度设计拖累的团队,往往连重构的信心都没有。
5.3 表结构与业务状态脱节
数据库表设计和业务逻辑“各说各话”,也是一种常见病。比如业务模块里定义了“已取消”状态,但表设计时没有区分取消原因;或者业务上允许“客户被标记为无效”,但表结构里没有无效标记字段,只能靠删除或者用备注字段硬塞。
这类问题在详细设计评审时最值得花时间。我会对照核心业务流程图,逐步验证每一个业务状态和数据操作都能找到对应的表结构和字段。走完一遍流程,缺字段、多状态、字段含义矛盾的问题很快就会暴露出来。
5.4 团队协作中“文档没人看”
设计文档写得很全,但开发时没人翻,每个人按自己的想法写,最后系统变得四不像。这种情况在规则松散、不重视设计评审的团队里尤其明显。
要解决这个问题,除了前面说的“文档放仓库、修改代码同步更新文档”外,还需要一个强制动作:开发任务估算时,把“阅读并理解对应设计文档”作为任务的一部分写入排期;编码完成后的自测清单里,加一项“实现与设计文档逐条对照”。这两件事看起来是流程性的,但能强制团队成员养成看文档的习惯。
项目验收是设计工作的最好检验场。我经历过一个项目,因为概要设计时对接口契约定义清楚,验收测试阶段对接外部系统几乎没出问题;也经历过另一个项目,因为详细设计时状态机没画完整,验收时测出十几个边界场景异常,最后只能加班连夜修。设计工作是否扎实,平时看着无关紧要,一到验收就能原形毕露。
如果让我给一条最实用的建议,那就是:概要设计的时候把架构和模块边界当成最大的事来审,详细设计的时候把接口字段和状态流转当成最大的事来抠。这两件事做好了,哪怕其他文档写得一般,项目的可开发性、可维护性、可验收性都不会差。写代码重要,但把设计写清楚这件事,在我眼里比代码本身更决定一个项目的成败,因为代码能被重构,架构和模块边界的债,往往是整个项目周期都在还。