十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

想清楚再写代码:从需求分析到系统设计,避免返工的关键思路

想清楚再写代码:从需求分析到系统设计,避免返工的关键思路 很多人拿到需求第一反应是打开IDE新建文件手指放到键盘上准备开写。我早年也是这个习惯后来被一个需求教做人了客户说“加个导出功能”我以为一个按钮一把查询搞定结果这个功能改了四版第一版没问数据量导出接口直接超时第二版没确认字段口径导出的对账单金额跟财务系统对不上第三版才发现他们还要按门店分组小计。每改一次就要动一遍SQL、改一遍模板、调一次权限。那之后我养成了一个习惯写代码前先花时间把问题想透。这篇文章不聊具体语法也不讲某个框架的API就聊动手写代码之前必须想清楚的那几件事。软件设计和代码质量的差距往往不是敲键盘的速度决定的而是动手前脑子里那张图清晰不清晰决定的。你可以把写代码理解成装修房子。水电工进场之前设计图没定、开关插座位置没想好瓦工先进场把墙砌了后面每改一个插座位置都要砸墙。代码也一样前期多花一小时想清楚后面能省下几十小时的返工。这篇内容适合刚入行的前端后端同学也适合那些已经写了三五年但老觉得“需求改来改去”是别人问题的人。看完你会发现大部分返工不是需求方太善变而是我们自己没把变量理清楚。1. 先别问“怎么写”先问“要解决什么问题”1.1 一句话需求背后藏着多少没说出口的信息产品过来说“给客户做一个订单管理功能”听起来很清楚对吧但这句话落到代码里至少缺了下面这些信息谁在用这个功能运营人员、财务、还是客户自己登录后台看不同角色的权限边界完全不同。“管理”到底包含哪些动作是只读列表还是要能改状态、能退款、能导出、能作废订单数据从哪来本系统创建还是从第三方同步如果是同步过来的哪些字段允许编辑哪些字段只能展示列表要支持哪些筛选历史订单要保留多久数据量级是百万还是几万这些没搞清楚就建表十有八九要返工。最简单的做法是拿到需求后先复述一遍你的意思是不是说运营在订单列表页输入订单号能看到这个订单的商品明细并且可以对未发货的订单执行取消操作复述这个过程会把需求里模糊的部分暴露出来。1.2 三个问题把模糊需求变成可执行的用户故事我后来总结出一个笨但有效的办法任何需求来了都先问三个问题。第一给谁用明确使用角色。第二用户现在的痛点是什么没有这个功能他是怎么干的这一步最关键它告诉你需求背后的真实动机。例如“加一个导出功能”真实动机可能是财务每月要对账人工核对太累。如果知道动机是对账你就知道导出的字段口径要和财务系统一致输出格式要考虑Excel能直接二次处理。第三做完变成什么样才算好这就是验收标准。好用的是“运营在下单高峰后能一键导出当日所有订单”而不是“做一个导出按钮”。把这三个问题聊完需求基本能从一段口语变成类似用户故事的结构作为财务人员我希望按日期范围导出发货成功的订单明细包含商品名称、规格、数量、实际支付金额以便和电商平台对账。这句话写下来后面所有代码都围绕它展开不会跑偏。1.3 两个因为没想清楚而返工的教训我印象最深的一次返工是做一个统计报表。需求方说“展示最近七天的用户活跃数据”我直接按天group by图表也画好了。结果对方拿到后说我要的不是自然日是每天早八点到第二天早八点的活跃。因为他们的运营节奏按晚上十二点交接班算。改这个SQL本身不难难的是所有图表标题、导出文件名、缓存key全都要跟着改。另一次是做一个审批状态。我设计了待审批、已通过、已驳回三个状态。实际上线后发现还有“已撤回”和“审批中撤回后重新提交”这两种情况在原设计里根本没位置只能加状态加完还要处理旧数据的迁移。后来我学乖了凡涉及状态流转的第一件事让需求方把所有可能的状态和跳转条件写出来宁可多列也不要漏。多问一嘴状态机比后面补数据迁移省事得多。2. 先定数据模型再谈代码结构2.1 数据模型是整个系统的压舱石代码写得再乱只要数据结构是稳的后面都还有重构的余地。反过来表结构设计错了所有代码都会跟着错。所以动手前最值得花时间的地方不是画类图而是想清楚核心业务有哪些实体、这些实体的关键字段是什么、实体之间怎么关联。还是拿订单举例。你至少需要区分订单主表、订单明细表、支付流水表、操作日志表。订单主表里存订单号、用户ID、订单状态、金额、下单时间这些对整单生效的信息订单明细表存每一个商品条目包括商品ID、名称、单价、数量、快照信息。如果你把所有商品直接塞在主表的一个字段里后续要统计某个商品的销量就得写恶心的字符串解析代码数据库索引还用不上。2.2 订单模块的核心表字段可以怎么推导以一个普通电商订单为例主表核心字段大概是订单号、用户ID、订单状态、商品总金额、优惠金额、应付金额、支付状态、收货信息快照、下单时间、支付时间、发货时间、完成时间。这里每个字段都有它存在的理由。订单号要独立于自增ID因为订单号要暴露给用户和外部系统不能让人通过ID猜测单量金额字段要区分商品总金额、优惠金额、实付金额不然后面做对账你根本不知道钱是怎么算出来的。订单明细表至少要包含订单号、商品ID、商品名称快照、商品图片快照、规格快照、单价、数量、小计金额。注意“快照”两个字。商品名称、价格、图片这些字段在下单那一刻就应该复制一份到订单明细里而不是下单后通过商品ID去关联商品表实时查询。因为商品可能改价、下架、改名但订单里记录的应该是用户下单那一刻看到的信息。不做快照等买家秀和卖家秀对不上时再回来找订单数据就晚了。支付流水表记录每笔支付请求支付单号、订单号、支付渠道、支付金额、支付状态、回调参数、回调时间。这里有一个容易被忽略的点把第三方支付回调的原始报文整个存下来哪怕你暂时用不上。等出现掉单、金额对不上要找第三方扯皮时这笔原始报文是你唯一说得清的凭证。2.3 什么时候可以偷懒用JSON字段和冗余设计不是所有表都值得做成第一范式有些场景用JSON字段反而更合适。比如订单的收货地址省市区、详细地址、收货人、电话这些信息确实可以拆一张收货地址表但如果一个地址只服务一个订单、后续不会被复用直接在主表里放几个字段存下来就好。再比如用户自定义的一些偏好设置、表单里的扩展项结构不固定、查询条件也几乎不会用字段精准匹配这种就可以用JSON列存。但是要守住一条底线凡是将来要参与统计、筛选、关联查询的数据不要放进JSON。比如订单状态如果要按状态筛选就必须是独立字段不能塞在JSON里再靠函数提取。我自己常用的判断标准是这个字段将来会不会被“where”到会不会被“sum”“group by”到如果只是原样展示、存个快照JSON和冗余字段都没问题一旦要被查、被算就必须走上正规字段。提前想这一步能少写很多“JSON字段抽取”的临时脚本。3. 画清楚模块边界决定哪里“好改”哪里“不敢动”3.1 在纸上画出依赖方向而不是在脑子里想很多人写代码是拿到需求就直接从Controller写到Mapper写着写着发现订单Service里调了库存Service库存Service又反向调了订单Service的某个方法。早期业务简单的时候不觉得等两边都加需求就开始互相拖累改库存的逻辑要担心订单这边炸。动手前在纸上画出模块之间的调用关系非常有用。你不需要画标准UML哪怕画几个方框加箭头也行。画的时候重点看两件事箭头有没有打成环以及依赖的方向是不是反了。理想情况下依赖方向应该和业务方向一致上层应用依赖领域服务领域服务依赖基础设施基础设施不反向依赖业务。3.2 按业务能力划分模块不按代码类型划分我见过很多项目把代码按Controller、Service、Dao这样的技术层拆包这个做法在小项目里没问题但业务复杂以后会很难受。比如和订单相关的逻辑散在OrderController、OrderService、OrderDao里看起来是整齐的但订单的整个业务全被压在一个巨大的OrderService里面改一个下单流程要在一个几千行的文件里面找。更推荐的切法是把关联紧密的业务能力放进同一个模块。比如订单模块内部可以继续拆出下单、售后、对账三个子域各自持有自己的服务、数据访问和领域对象。这样做的好处是改售后逻辑时基本不会影响其他两个子域。模块划分的最高标准是单一方向的可替换性将来下单流程整体改造你可以在不动售后和对账的前提下替换掉整个子模块。3.3 稳定接口加不稳定实现是减少连锁改动的前提模块边界清晰之后模块之间的通信协议就成了核心。接口定义要稳定实现可以随便换这是几乎所有设计原则的共同指向。落实到代码层面接口的稳定体现在一次交互的动作名称、入参、出参要语义化。我举一个反例。有些人喜欢写一个通用的方法MapString, Object handle(String action, MapString, Object param)。所有调用都能走这一个方法但参数和返回值全塞在Map里没有类型约束。系统里就没法静态检查参数是不是传错了看代码的人也不知道这个action有多少种取值每个取值需要哪些key。这种接口灵活到了没有边界的地步后期维护的人基本靠猜。正向做法是定义明确的DTO。比如取消订单接口就定义成CancelOrderRequest里面带orderId、operatorId、cancelReason返回CancelOrderResult。哪怕事务里要调库存回滚库存模块也只对自己的RestoreStockCommand负责。两边各自演进互不越界。稳定的接口实际上是稳定的“协议”协议想清楚了实现怎么换都是实现的事。4. 动手编码前就要想好代码怎么“活着”4.1 命名是在给代码起地名别随手乱贴标签命名这件事要是等到写的时候再想一般都会偷懒。变量叫temp、data、list类名叫OrderInfoService、OrderInfoServiceImpl、OrderInfoUtil可能只有写的人自己当时知道区别一个月后回来看就已经分不清了。我给自己定的标准是命名要能回答“这个变量是什么”和“它在这个上下文里为什么存在”。订单金额和退款金额都要用变量存一个叫money一个叫refundMoney当下还好但如果这个money在某个函数里其实表示的是“用户本次可退的最大金额”那叫availableRefundAmount比叫money准确得多。好的命名反而能帮你在写代码之前就发现逻辑漏洞如果变量名说不清楚它的含义说明这段逻辑本身可能就是含糊的。缩写在命名里要克制。order、customer、payment这种通用缩写问题不大但像cfg、tmp、rmk这种只有你自己懂的缩写尽量别用。团队协作里命名最主要的价值是降低所有人的理解成本而不是让你少敲两下键盘。4.2 注释只写“为什么”不写“是什么”代码能跑起来的时候读代码的人看“是什么”就够了他真正需要注释帮助理解的是再往下走一层这里为什么这么做而不那么做。我写过一段非常不好维护的代码用了一个看起来绕了两层的逻辑。后来加注释时写了需求背景因为这个渠道的支付回调可能延迟所以不能在下单时直接改订单状态为已支付而是先记为支付中等回调到了再确认。这段注释现在还在包括我在内的后来所有人都靠它理解当初为什么不能简单同步处理。反过来那些“这一段是查询订单列表”的注释毫无价值。代码本身已经把逻辑写得明明白白注释再翻译一遍纯属噪音。另外如果你在代码里做了一些看似不符合常规的处理比如加了某个状态的兼容、做了某种字段拼接一定要在旁边写上“上游系统会传空值为了保证历史数据兼容这里只能保留默认值”之类的原因。这类注释保存的是业务上下文代码删了还能查到历史业务上下文丢了就再也找不回来了。4.3 错误处理和日志要提前规划而不是随意加错误处理最怕的就是到处try-catch然后吞掉异常。一个接口调了Dubbo、数据库、Redis每层都try一下打一行log最后问题发生时日志一片祥和根本看不出哪里断了。错误处理的策略应该是一个统一的方案而不是零敲碎打。我在代码里比较习惯的做法是底层抛出业务异常时不立刻catch而是让异常向上抛到统一的异常处理层由它转换成对用户友好的提示信息并统一记录日志。凡是“可预期但不该发生”的情况例如库存不足、重复支付、状态冲突都主动用有明确错误码的异常来表示而不是返回一个null或者false让上层猜。日志的规划也有讲究。写操作日志的时候至少要包含“谁在什么时间对哪个订单做了什么操作、操作前状态是什么、操作后变成什么”。举个例子运营把订单从待发货改成已发货如果日志只有“订单状态修改成功”出问题时根本没法复盘。如果记录了操作人、操作前后状态、物流单号基本可以还原整个操作过程。哪怕是查询接口也别把所有入参都一股脑打出来一个用户列表接口如果每次查询都把全部筛选条件打到日志里日志量会爆炸正确做法是记录“谁查了什么、命中了多少条、耗时多少”这比记录每个参数重要得多。5. 预判变化哪些地方要留扩展点哪些地方别多想5.1 把“已知的变化”和“猜出来的变化”分开很多架构设计死于过度抽象还没等业务变化代码已经被各种设计模式包成俄罗斯套娃。但一点都不预留也不行等变化真来了只能大改。关键是区分“已知变化”和“猜出来的变化”。已知的变化是那些业务方已经明确告诉你或者根据行业常识能推断出来的。比如电商系统大概率会接多个支付渠道、多个物流渠道这种在领域建模时就应该把支付、物流抽象成渠道每一家实现各自的适配逻辑。猜出来的变化是“说不定以后会做一个分销裂变”这种没有任何明确业务输入的想象先不要为它设计任何扩展接口。真正要支持它的时候再抽象那个时候模型更清楚成本并不会高多少。用一句容易记的话来说不要为假设的明天写代码要为已确认的明天做设计。5.2 在哪个位置做“开关”决定了改动范围一个很实用的预留方式是配置化。当不确定某段逻辑未来可能走哪个分支时先别急着用if-else把所有分支写死在代码里把行为差异的“选择权”放到配置中心。比如积分规则不同用户等级享受不同倍率不知道过两个月运营会不会调倍率那常量就应该放到配置表或者配置中心而不是写死在枚举和代码里。但配置化也不能滥用。如果一个功能的需求短期内根本没有变体你非要做成规则引擎那就是自己给自己挖坑。预留正确的常见姿势是核心流程保持单一清晰的主链路变化点用接口或配置做薄薄一层渲染。比如支付模块主流程永远是“创建支付单、发起支付、接收回调、更新状态”所有渠道差异都收敛到一个统一的PayChannel接口后面新增渠道只是新增一个实现类不会动主流程和其他渠道。5.3 判断是否抽象看“变化频率”而不是“规模”有时候一个功能很小但它几乎每个月都在变这种就值得抽象。比如营销活动的优惠计算哪怕规则现在只有满减一种如果运营每个月都会调整玩法就应该把规则拆成可配置的模板不要让满减逻辑散落在下单代码的各个角落。另外一种情况是代码写出来跑了好几年都不变哪怕它最初设计得很粗糙也不要因为“它规模大”就强行抽象一遍。抽象本身也是成本新抽象出来的层级需要持续维护没有业务变化驱动的抽象大概率会成为负担。判断一个地方要不要设计扩展点最有效的维度是变化频率和变化影响范围而不是这个模块现在有多少行代码。6. 开工前的最后一道检查把风险当成代码一样去排查6.1 技术选型不能只看名字先做验证性原型到了马上要动手的时候很多人会忽略技术选型验证这一步。看到一个第三方库GitHub星标高、文档全就直接用到核心链路里结果部署到生产环境才发现它在特定环境下有问题。我碰到过一次比较典型的事故接了一个做报表导出的第三方组件本地demo跑得很顺畅接入项目后在小数据量下也正常等到生产环境导几万行数据时内存直接溢出。后来查下来是那个组件在底层默认把全量数据加载到内存里处理配置里又没有提供批量刷新的开关。如果在上手之前先拿真实量级的数据做个压测原型这个问题一两个小时就能暴露根本不用等到线上。做验证性原型的范围要控制好只验证最核心、最有风险的点。比如你要引入一个新的数据库中间件那就模拟把核心表读写切过去跑真实业务场景如果你只是给列表页换一个UI组件那只需要确认基础交互和接口数据结构不需要搭一套完整脚手架再开工。6.2 把检查清单写下来逐项打勾再动手动手前最后一个环节我建议把要确认的事项全部列成清单。原因是人脑在信息过载时特别容易漏掉“以为说了但其实没说”的细节。清单不用花里胡哨展开的项目不同核心检查项是这些。检查项需要确认的核心内容是否完成需求闭环角色的核心流程是否完整正常路径、异常路径都覆盖了没有数据口径关键金额、状态、时间的定义和上下游是否一致数据模型哪些字段将来会参与查询统计是否独立成列哪些数据需要做快照模块边界模块间的依赖方向是否明确接口入参出参是否稳定错误处理与日志是否想清楚谁能吞异常、谁能抛异常、日志里有没有关键上下文风险验证第三方SDK、中间件、核心算法是否提前做过验证性原型代码规范有没有提前约定命名、格式化、基础代码扫描规则其中代码规范这块我建议在项目启动时就配好工具。像SonarQube或者IDE里的代码诊断插件不是写完了才跑一遍而是一开始就接入让每一行新代码都暴露在检查之下。很多低级的空指针、资源未关闭、魔法值问题在写的时候就由工具标出来比事后Code Review效率高得多。6.3 设计完别立刻编码放一晚上再回来看还有一个我后来养成的习惯设计方案写完后不要当天就动手尽量隔一个晚上第二天再扫一遍。这个“冷却期”特别管用因为写方案时你很容易陷入自己的思路里觉得自己全想明白了隔了一晚大脑跳出局部视角后很容易发现方案里的漏洞。我在一个订单对账模块的设计里当时画流程图时觉得每一步都覆盖了放了第二天一看发现漏掉了“平台优惠由平台承担、店铺优惠由店铺承担”这种分账规则。这个漏掉的部分如果没发现代码写一半再补可能要推翻一半表结构。现在我的习惯是但凡涉及核心业务的设计都给自己留一个“晚上”的缓冲哪怕第二天只花半小时重新过一遍收益都远超成本。我在实际项目中见过太多返工仔细复盘下来没有一次是“代码能力不够”导致的几乎都是前期设计没想透。实体关系没理清就建了表接口协议没定清楚就让前端并行开发状态流转没梳理明白就急着写第一个if-else。写代码这件事看起来比的是谁敲得快实际上比的是谁脑子里那张图更清楚。动手前花一两个小时把需求、数据、边界、风险都过一遍后面省下的可不止一两天。最后再分享一个小习惯把你这次踩过的返工原因作为一条检查项加进你下一次的开工清单里。每个项目都往里面加一条用不了几个项目你的清单就会比任何架构文档都值钱。
返回列表