ARTICLE DETAIL

资讯详情

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

可理解性:从系统结构到接口契约的工程质量指南

可理解性:从系统结构到接口契约的工程质量指南 可理解性我第一次被这个词“教育”是在一次跨团队交接会上。对方丢给我一套只写了接口名、没有文档的微服务我盯着一排handleXxx方法看了两小时愣是没分清哪个是下单、哪个是回调、哪个是重试。旁边维护了三年的大哥叹了口气说功能没毛病就是不好懂。这句话背后的东西就是标题里那个定义——系统结构、功能、接口等被开发人员或维护人员理解的难易程度。这个概念看起来短牵扯的东西却很长模块怎么分、接口怎么定、命名怎么起、文档怎么写、测试怎么组织全算进去。它不是某个能装上的插件也不是一次重构就永久解决的毛病而是贯穿系统生命周期的质量属性。这篇文章适合三类人刚接手陌生系统、正在“看得懂但改不动”里挣扎的维护者在设计模块边界和接口契约时想让协作更顺手的后端/架构方向工程师以及想搞清楚“可理解性到底能不能量化”的质量效能团队。我会把这个定义从理论拆到实操结合系统结构设计、接口定义、文档与测试组织这些具体场景讲得尽量能直接上手。1. 可理解性的本质它不只是“代码看得懂”1.1 三个被理解的对象结构、功能、接口定义里明确点了三样东西系统结构、功能、接口。这三者的“被理解”其实是不同层面的问题。系统结构说的是模块划分、分层关系、依赖方向和目录组织回答的是“这套系统由哪些部分组成、彼此怎么连”。一个结构可理解性好的系统外部看像一张清晰的城市地图先分几个区每个区干什么区与区之间走哪条主干道一目了然。功能说的是系统到底提供哪些能力业务怎么流转状态怎么变迁回答的是“每个部分到底在干什么”。接口说的是系统内外交互的契约包括方法签名、消息格式、传输协议、参数语义回答的是“别人怎么调用它、调用后会发生什么”。我常用的一个类比结构是分区和路网功能是地标建筑的作用接口是路牌和交通规则。地铁坐多了你会发现哪怕第一次到一座城市只要路牌清楚、线路图规范你也能顺利到达目的地。可理解性差的系统就是一座路牌乱写、地铁图错位的城市老住户觉得没问题新来的每一步都在猜。1.2 可理解性在质量模型里的位置做质量的人应该都见过 ISO 9126 和 ISO 25010 这两套质量模型。有个细节经常被搞混早期 ISO 9126 里面向终端用户的“可理解性Understandability”被归在易用性Usability特性下而面向开发维护人员的“易分析性Analyzability”被归在可维护性Maintainability下。到 ISO 25010 之后可维护性又进一步细化出模块性、可重用性、可分析性、可修改性、可测试性。标题这个定义说的是“被开发人员或维护人员理解”所以它更贴近可维护性这条线准确说是可维护性的地基。为什么说是地基因为维护动作的第一步永远是理解先搞懂现状才能评估改动范围先看明白依赖才能预测影响面先读通接口语义才能写出正确的调用。如果这一步卡住了后面的稳定性、可测试性、可修改性全是空谈。很多团队把质量重心放在测试覆盖率和线上监控上却忽略了最前面的理解成本结果就是缺陷修得很快、但新缺陷也来得很快因为改代码的人根本没真正理解系统。1.3 可理解性、可读性、可维护性的区别这三者经常被混着讲我简单捋一下。可读性是代码层面的指单行、单函数是否容易读可理解性是系统层面的指整体结构与行为是否容易懂可维护性则是修改和演进是否容易它是结果。可读性是可理解性的必要不充分条件——代码每行都通顺不代表模块边界清晰、接口语义明确反过来可理解性差的地方可读性通常也好不到哪去。用写文章来类比可读性是句子通顺、用词准确可理解性是整篇的逻辑脉络清楚、章节划分合理可维护性是后面改稿、续写、换人接着写都不费劲。只盯着单行代码的整洁不关心结构就像句子写得很漂亮但全文没有分段读起来照样累。2. 系统结构层面的可理解性架构要先“一眼看懂”2.1 分层与边界结构理解的第一道门槛我接手过不少“能跑但不敢动”的系统它们的共同点往往不是代码写得乱而是边界模糊。Controller 里直接拼 SQL、Service 里塞定时任务、工具类里存放着改了一半的业务逻辑这种结构下没人能准确回答“这个功能到底属于哪一层”。合理分层的意义不只是架构洁癖而是给理解者一个默认的“搜索路径”。一个请求进来你默认先在 Controller 层看参数校验和路由再到 Service 层看业务编排最后到 Repository 层看数据访问。如果这个路径不成立阅读者就得满项目乱翻理解成本成倍上升。实操里我建议每个模块先用一句话说清职责说不清就是边界有问题。比如“订单模块负责订单生命周期管理与履约状态同步”这就是一句合格的话“订单模块负责订单相关的所有事情”这就等于没说因为它没有给阅读者任何边界预期。2.2 命名与目录组织给理解者的“路标”系统结构的可理解性很大程度靠命名和目录承载。包名、目录名、模块名是阅读者最先接触的信息它们就是地图上的路标。很多项目的路标是失效的common、utils、base、srv这类名字看着通用实际上慢慢变成了杂物间啥都往里丢最后谁也不知道里面有什么、该不该依赖它。我比较推荐按业务域组织目录而不是按文件类型堆叠。比如order目录下面放controller、service、repository子包而不是在一个巨大的controller包里堆几百个类。前者让阅读者按业务线索走后者让阅读者按技术类型走——问题是大多数人接手业务时脑子里装的是业务线索不是技术分类。另外消灭无信息量的缩写也很重要。getBatchData和getBatchDataByDateRange对阅读者的友好度差一个量级。命名多打几个字不亏节省的是每个后来者几十分钟的猜谜时间。2.3 依赖关系当结构图开始像蜘蛛网结构层面最伤可理解性的是依赖混乱。一个模块依赖另一个模块的私有实现、A 调 B 而 B 又调回 A、到处都是跨层调用这种系统的结构图画出来就像蜘蛛网没有人能在脑子里完整模拟一条链路。依赖必须单向这是结构可理解性的底线。上层可以依赖下层下层不能反向依赖上层模块之间依赖接口而不是依赖实现。判断标准很简单你能不能用一句话说清“谁依赖谁、为什么依赖”。如果依赖理由要靠“历史遗留”来解释那这段依赖就是风险点。落地手段上除了评审时人工看图还可以用架构守护工具把依赖规则固化成自动化检查。比如 ArchUnit直接写在测试里谁违反依赖规则 CI 就红。这种“结构可理解性”的保护不能只靠自觉要靠机制。2.4 一个硬件接口的旁证jlink、stlink 的结构可理解性软件之外硬件行业对“结构可理解性”的体会更深。去看 jlink、stlink 这类调试器的接口定义引脚数量、顺序、电平标准、协议时序都有严格的规范引脚定义一清二楚工程师拿到线就能接。要是引脚定义混乱、GND 和 VCC 位置反人类轻则调试失败重则烧板子。软件模块之间的接口其实就是这种“接插件”。接口定义得好接入方按照契约对接就行接口定义得敷衍依赖方就得去读内部源码、猜隐含约束理解成本全部转嫁给调用者。这也是为什么很多资深工程师看一个系统第一件事是翻接口定义而不是翻实现代码——接口质量基本决定了系统的可理解性上限。3. 功能与接口层面的可理解性接口是系统的门面3.1 为什么接口定义是第一入口新人接手项目几乎无一例外从接口开始读REST 的 URL、RPC 的方法签名、消息队列的 Topic、公共类的方法列表。接口就是系统的门面没人会先钻进数据库表结构去理解业务。所以接口定义的清晰度直接决定了一个系统的第一印象也决定了后续所有深入理解的成本。接口可理解性的核心原则是调用方只依赖契约不依赖内部实现。一个接口如果必须知道内部状态才能安全调用比如“要先调用 init 再调用 query”这种隐式顺序就是理解陷阱。好的接口设计应该让调用方的每一个合理猜测都能成立而不是让调用方去猜“到底要不要传这个参数、传了会怎样”。这里我特别想提“接口封装”。很多人以为封装是为了隐藏实现其实封装更大的价值是降低理解成本。你把一堆底层操作收敛成一个语义明确的门面接口调用方只需要理解一个概念而不是理解一串机械步骤系统整体可理解性自然就上去了。3.2 接口命名的三种典型误区接口命名是功能可理解性的重灾区我总结了三种最常见的毛病。第一种是动词含糊。handle、process、do、deal这类动词几乎不带信息量后面挂着什么对象都得猜。第二种是参数类型当名字。getByIdsAndStatus看着比get强但还是在描述“我接收了什么”而不是“我要做什么”。第三种是暴露实现细节。接口名叫saveOrderToMysqlAndSendKafka倒是诚实但阅读者需要理解的技术细节太多而且实现一旦变化名字立刻过期。反面例子人人都见过public void handle(Long id, Integer st) { Order o repo.findById(id); if (st 1) { o.status 2; repo.save(o); msgSender.send(ORDER_PAID, o); } }正面例子只需要把意图说出来public void confirmPayment(Long orderId) { Order order orderRepository.findById(orderId); order.markPaid(); orderRepository.save(order); paymentEventPublisher.publish(new OrderPaidEvent(orderId)); }差别很明显正面版本的接口名和内部步骤都在回答“发生了什么业务事件”而不是“我调了哪些底层方法”。阅读者不需要知道st 1是什么意思因为confirmPayment本身就是语义。3.3 接口契约与文档别让注释骗人接口可理解性离不开文档但很多项目的文档和代码是两张皮。注释写着“创建订单”代码实际干的是“创建草稿订单并校验库存”这种不一致比没有注释更坑人因为阅读者会相信文档。我比较推荐契约优先的做法对外接口用 OpenAPI、protobuf 或类似机制定义好契约代码和文档从同一份定义生成。这样接口签名、字段含义、约束条件就不会漂移。对于内部模块间接口至少要在接口注释里写清四件事业务语义是什么、参数和返回值的含义、异常和边界情况有哪些、是否幂等以及并发约束是什么。顺带说一句“多源接口配置”这类场景。企业里做多数据源接入时每个上游接口的命名、字段、语义都不一样如果接入层不做一个统一的内部契约维护者就要同时理解十几种外部格式理解成本直线上升。多源不可怕可怕的是多源带来的多套心智模型。所以在接入层定义一套统一内部契约、由适配层做转换是降低整体可理解性的关键手段。3.4 接口幂等性与自动化把“理解”固化下来“接口幂等性”这个词这几年越来越高频它表面上是可靠性的问题本质上也是可理解性的问题。一个不幂等的接口调用方必须小心翼翼地控制重试时机必须记得“上次可能已经成功过”这种心智负担就是理解成本。反过来接口声明“按业务幂等键去重、重复调用安全”调用方就能放心重试理解成本大幅降低。好的接口设计从来不只是实现正确还要让调用方容易正确。接口自动化测试对可理解性也有很大帮助它相当于一份可执行的文档。Java 生态里常见的接口自动化测试框架把请求、断言、数据组织在一起读测试就能理解接口的预期行为。我最看重的是另一个价值当有人改接口改坏了语义时自动化测试会第一时间跳出来提醒等于把“契约不能被悄悄破坏”这件事固化进流程。4. 如何评估可理解性先量化再优化4.1 静态指标可理解性的代理指标可理解性本身没法直接用数字测但可以用一批代理指标做初筛。我常用的参考如下。指标关注点我的参考值圈复杂度单个函数的路径数量不超过 10超过就要考虑拆分函数长度单个函数是否职责单一20 行以内最好超过 50 行要警惕类/模块依赖数依赖是否过多一个模块直接依赖超过 10 个建议审视环依赖数量结构是否有回路0 个出现环必须拆公共接口的入参数量接口契约是否清晰超过 5 个参数建议封装成请求对象无信息量目录占比命名是否在传达含义common/utils 等目录应远小于业务包需要说明的是指标只是代理变量不是目标本身。圈复杂度低但业务语义混乱照样可理解性差目录名字规范但依赖乱成一团也好不到哪去。指标的作用是快速圈定嫌疑区域真正的判断还是要靠人。4.2 最诚实的评估方式新人上手时长我评估一个系统可理解性最常用的办法不是看指标而是看新人多久能独立完成一次小改动。说白了可理解性就是“一个合格的新手需要多久才能安全上手”。这个指标比任何静态扫描都诚实。具体做法是给新人布置一个难度中等的缺陷修复任务记录从拿到任务到提交代码的时间以及过程中需要向老同事求助的次数。一个模块如果平均需要老同事讲三遍才能动手那不管代码写得多么“整洁”它的可理解性就是不及格。我在不少项目里观察过同一批新人修不同模块可理解性好的模块可能一下午搞定差的模块往往要拖两三天差距就是结构、功能、接口三个层面叠加出来的。4.3 评审阶段的检查单与其事后测不如在评审阶段就把可理解性当成硬指标。我每次做代码评审和架构评审都会过一遍下面的问题这个模块能用三句话讲清职责吗讲不清边界就有问题。依赖方向是否单向、是否面向抽象接口名表达的是业务语义还是实现细节调用方是否只依赖契约、不需要关心内部状态只读接口签名和注释新同事能不能写出正确调用出错时能从日志和接口信息直接定位问题模块吗这些问题任何一个答不上来Review 我就不会给过。因为可理解性差的问题一旦合入主干后面每个接触这段代码的人都会替你付利息。5. 提升可理解性的实操套路5.1 命名与结构先行代码即文档说一句可能有点绝对的话注释写得多不如命名写得好。类和方法的命名就是最常被阅读的“文档”它应该表达意图而不是表达实现。我前面给的confirmPayment和handle的对比就是最典型的例子。实操上我给团队立过一个规矩如果一段代码需要读两遍才能明白它在干什么那第一件要做的事不是加注释而是重命名。把函数拆到意图自解释的粒度再配合调整目录结构往往比堆注释有用得多。一个长函数拆成validateInput、buildOrder、persistOrder、publishEvents四个短函数后阅读者根本不需要注释顺着名字就能读懂全过程。5.2 注释的“三七开”原则注释不是不能写但要分清写什么。我自己的比例大概是三七开七成靠代码自解释三成注释解释“为什么”而不是解释“是什么”。“是什么”代码自己已经说了注释重复一遍只会增加阅读噪音“为什么”代码说不出来比如“这里为什么要先扣库存再锁单”“为什么这个字段允许为空”这些才是注释该干的事。接口注释则是另一套标准密度要高得多。对外接口的注释必须包含业务语义、参数和返回值的含义、异常与边界情况、幂等性说明、并发约束。调用方读接口注释应该能回答“能不能调、怎么调、调了会怎样”这三个问题。5.3 用测试当文档让行为被锁定我一直把测试当作“活的文档”。测试用例的命名可以直接写成完整句子比如shouldChangeOrderStatusToPaidWhenPaymentConfirmed读测试列表就等于读系统行为清单。对接口而言契约测试的价值尤其大它把接口的请求结构、响应结构、错误码约定都锁在代码里谁改了契约谁就要面对红灯。还有一个容易忽略的好处测试是唯一不会和代码脱节的文档。代码更新后旧测试跑不过新测试本身就是对新行为的最新描述。相比 wiki 和设计文档它天然有“保鲜机制”。5.4 提升可理解性的最小落地清单如果你手里正好有一个“能跑但难懂”的模块别想着一步到位重构你可以按下面的顺序逐步推进。先画现状只画事实画模块清单、依赖箭头、每个模块一句话职责。画不出来就是可理解性差的直接证据。定目标边界想清楚这个模块最终应该长什么样边界在哪里。只改表达、不改逻辑先重命名、调整目录、拆分长函数这一步风险最低。用测试锁住行为重构前先补关键行为的测试确保逻辑没变。再拆依赖处理循环依赖、收敛跨层调用这一步要配合架构守护工具。评审跟进后续每次改动都按 4.3 的检查单过防止反弹。这套流程的核心是“先让表达变清晰、再让结构变清晰”因为表达的调整不需要动逻辑风险小、见效快结构的调整则必须建立在表达已经清晰的基础上否则改完还是一团雾。6. 常见问题与排查技巧实录6.1 典型问题速查表下面这些场景我基本都在真实项目里见过整理成一张表方便对照排查。症状典型根因处理建议每行代码都看得懂但整个模块看不懂模块边界和命名问题按业务域重组目录重命名暴露意图接口一堆参数调用方不敢动缺少请求/响应对象契约混乱封装参数对象收敛入参文档和代码不一致文档没随代码更新契约优先用测试和注释锁住行为新人三个月不敢碰某模块依赖复杂、可理解性差按最小落地清单做模块级重构改一个模块隔两个模块才出问题依赖未收敛、调用链过长依赖分析 架构守护测试接口调用要“按固定顺序”才安全隐式依赖内部状态重设计接口消除隐式顺序约束6.2 亲测有效的几个应急技巧先说第一个十分钟“电梯结构图”。我接手任何陌生代码库第一件事不是看代码而是花十分钟画一张只包含模块、依赖箭头和一句话职责的结构图。如果十分钟画不出来说明这个系统的结构可理解性已经亮红灯了接下来所有深入工作都会有偏差。这张图也是后面和同事对齐认知的最好工具。第二个技巧是建一个“无信息量黑名单”。目录名、变量名、方法名里出现tmp、data、handle、misc这类词评审时一律打回重命名。别小看这个动作它逼着写代码的人想清楚每个东西到底承载什么语义命名过程本身就是一次理解梳理。第三个技巧叫“文档过期检测”。每次评审问一个问题如果只读接口签名和注释新同事能不能写对这个接口的调用如果答案是不能要么接口设计有硬伤要么文档已经过期处理掉再合入。这个检测成本极低但能拦截掉大部分可理解性隐患。6.3 到什么程度算“足够可理解”可理解性没有一个绝对达标线但有一个我非常认可的经验标准当你把一个模块交接给另一个同事时他在不追问你细节的前提下能独立完成一次小改动并给出清楚的改动说明这个模块的可理解性就及格了。注意标准不是“没有坏味道”也不是“代码很优雅”而是“交接成本足够低”。可理解性的本质是把理解成本从别人身上转移到自己写代码的阶段——你多花十分钟想清楚命名和边界后来者就能省下十个小时的猜测。这笔账怎么算都划算。我个人这些年最深的体会是可理解性不是锦上添花的加分项而是其他所有质量属性赖以为继的地基。测试覆盖率再高改代码的人看不懂结构照样会改出隐蔽缺陷监控再完善维护者不理解业务流转照样会在线上事故里摸瞎。最后再分享一个压箱底的小技巧每次提 Pull Request 之前把自己当成第一次看这段代码的陌生人把自己这次改动的 diff 从头读一遍凡是需要想两秒才看懂的地方就是可理解性有缺口的地方当场改掉再提交。这个习惯看起来普通坚持下来比任何扫描工具都管用。
返回列表