搜索/让 AI 在大型 Java 后端中遵守代码风格和模块规则

让 AI 在大型 Java 后端中遵守代码风格和模块规则

在由模块和提供方组成的大型 Java 服务器中让 AI 工作时使用。它会先读入口代码,遵守自动检查的代码风格和所支持的最低 Java 版本,声明用到的每个模块,先在源码中确认再下结论,并在推送前先在本地测试。

要使用这个AI规则
## 修改之前,先弄清模块与提供方如何配合 这类大型服务器是由多个模块组装而成的,而不是作为一个应用整体写出来的。模块声明会给出模块的名称以及它对外暴露的服务接口;提供方用某一种具体技术实现这些服务;其他模块只消费已声明的接口。提供方通过平台的服务加载器机制被发现,并遵循固定的生命周期:先是准备阶段,构建对象并读取配置,然后是启动阶段,激活各项服务。 ### 动某个环节之前先把整条流水线梳理一遍 采集器通过传输层发送遥测数据,接收器插件对其进行解析和校验,分析引擎借助项目自有的领域语言对其做聚合,存储插件持久化结果,查询插件再把结果提供给界面。某一个环节的改动,通常需要在另一个环节有相应的配套改动。 ### 先读入口点 动手编辑之前,先读核心模块定义、模块系统基类、领域专用语言的语法文件、应用程序入口点,以及架构文档。名字并不能告诉你一个功能到底是怎么工作的。 ## 遵守构建时自动检查的代码风格 这里的代码风格由机器检查,所以违反风格会直接让构建失败,而不是引发一场争论。不要向标准输出打印内容;使用项目的日志门面。不要添加作者注解。不要使用制表符、通配符导入、未使用或多余的导入、空语句,也不要出现多个连续的空行。保持一行一条语句,在逗号、分号和类型转换之后保留空白,在运算符两侧保留空白。 ### 必须遵循的写法 为覆写的方法加上注解。相等性方法和哈希方法要一起覆写。给每个文档中的参数标签、返回值标签和异常标签都写上真正的说明。长整型常量要用大写的后缀字母书写。switch 中的默认分支放在最后。遵守文件长度上限,接近上限的文件要拆分。 ### 命名 常量和静态变量使用全大写加下划线,类型参数使用大写字母,类型使用帕斯卡命名法,局部变量、参数和成员使用驼峰命名法。相比手写样板代码,优先使用不可变集合工厂方法和项目的注解处理器,并优先把局部变量和参数声明为 final。 ### 不要内联写出全限定类名 应当添加导入,并在字段声明、方法签名、局部变量和泛型参数中使用短名称。可以接受的例外只有两种:两个被导入的类之间确实存在简单名冲突;以及某处文档链接中短名称会让读者产生歧义。 ### 不要写单行委托方法 一个方法体里只是转发给另一个类的包装方法,只增加了一跳调用而没有任何价值。应当在调用处内联该调用,或者直接调用底层对象,包括通过方法引用的方式。 ### 许可证头 源码文件、标记文件和配置文件都带有项目的许可证头。无法容纳注释的格式已经在许可证配置中被排除;请遵循该配置,而不要在不该加的地方手工添加许可证头。 ## 按职责而不是比喻给类和方法命名 类和方法要以它们的功能来命名,而不是以某种抽象的比喻来命名。优先使用具体的动词,例如加载、应用、注销、编译、校验、提交、回滚,而不是诸如播种、注水、引导、预热这类比喻式的说法。 ### 让名字直接说明这项工作 一个加载静态规则的类,就应该以"加载静态规则"来命名,而不是取一个形象生动的意象。一个按计划同步数据库状态的定时器,就应该以那次同步来命名,而不是叫"滴答"。 ### 出现比喻,说明该拆分了 如果你不借助比喻就没法给一个方法命名,那这个方法多半承担了过多职责。把它拆开,直到每一块都能有一个平实的功能性名称。 ## 只为代码本身说不清的地方写注释 不要解释代码已经表达清楚的内容。只在光读代码还不够的地方写注释:在本方法中看不出来的不变式、跨越多处的顺序约束、某个不明显的分支为什么存在、某个看似多余的调用为什么是必需的。如果代码本身就很清楚,那就不要写注释。 ### 把分散各处的逻辑串联起来 某个检查之所以存在,只是因为别处做出了某项保证,那就值得用一行注释指向那项保证。针对别处某个已知缺陷的绕行方案,值得附上指向该缺陷的链接。 ### 删掉复述性的文档 方法文档描述的是契约,而不是方法体。需要多个段落展开的理由属于设计文档,而不属于源码文件。 ## 使用框架断言,修改测试前先征求同意 始终使用测试框架的断言方法。绝不要在测试中使用语言内置的断言语句。即便测试运行器碰巧启用了这类语句,框架断言仍然是代码库其他地方的惯例,能在任何运行器配置下都成立,并且会产生一个结构化的失败信息,把实际值指出来,而不是抛出一个没有上下文的错误。 ### 修复失败的测试,而不是悄悄改掉它们 如果既有的单元测试、集成测试或端到端测试失败了,要设法修复它们。如果某个测试用例需要修改,或者某些测试代码需要删除,这种改动必须在应用之前经过人工复核:把提议的修改或删除以及其中的理由摆出来,等待明确批准,而不要自行改动或移除测试。 ### 让测试类型与其覆盖范围相匹配 单元测试范围和集成测试范围使用不同的模式和不同的测试装置。要在行为真正所处的那个范围上去证明它,而不是在每一层重复同一条断言。 ## 写出能在所支持的最低 Java 版本上编译的代码 一个支持多个长期支持版本的项目,必须能在其中最老的那个版本上编译和运行,即使构建机器上装的是更新的编译器也一样。要按最低版本来假设,而不是按你的编辑器所接受的来假设。 ### 当最低版本较老时应避免的语法结构 箭头形式的 switch 表达式、直接返回列表的流终结操作、文本块、记录类型、类型检查中的模式匹配,以及密封类型,相对于较老的基线版本都属于更新的特性。取而代之,应使用带显式 case 和 break 的传统 switch、用收集器来构建列表、字符串拼接、带自动生成访问器的普通类、类型检查之后的显式类型转换,以及普通的类或接口。 ### 安全可用的语法结构 不可变集合工厂方法、较新的 optional 方法、lambda 表达式、方法引用,以及流接口本身,在较老的基线版本上都可用,无需任何绕行处理。 ### 靠搜索来验证,而不是靠信任构建结果 在源码树中逐一搜索每一种被禁用的语法结构,确认没有匹配之后,再声称兼容。在更新的编译器上构建,并不会替你发现这些问题。 ## 让子模块和生成的代码与构建保持同步 协议定义和 Web 界面可能位于子模块中。克隆时始终要连同子模块一起克隆,并在协议变更需要时显式更新它们。一次没有反映到子模块指针上的协议变更,在其他任何人那里都无法构建。 ### 先生成,再分析 在分析生成的代码之前先运行编译步骤,并且在运行测试之前先安装项目,以便预编译器和生成的类被导出。绝不要手工编辑生成的类;应当修改产生它们的定义,然后重新构建。 ### 跨模块改动之后重新构建整个项目 如果你改动了超过两个模块,或者从远端拉取或变基过,就要构建整个项目,而不要挑单个模块来构建。当归档文件大小没变但内容变了时,增量式的单模块构建可能会在本地仓库或库目录中留下陈旧的归档文件,从而导致极难追查的运行时故障。 ## 让文档和变更日志内容准确、显示正确 文档由一个 markdown 引擎渲染,所以要考虑它最终呈现出来是什么样子,而不只是源文件读起来怎么样。相对文件路径是有效的,在仓库里和文档站点上都能正常工作;请继续使用相对路径,而不要用绝对链接。只要行为发生变化,就要更新变更日志。 ### 变更日志的每一条都保持在一行内 不要在一个条目内部把一个句子硬换行拆到多个源码行上;嵌套的子条目没问题,因为它们是真正的列表项。变更日志会成为发布说明的正文,而发布页的渲染器会把单个连续的换行变成一次换行显示,所以被折行的条目会在发布页上呈现出句子中间参差不齐的断行,尽管文档站点会重新排版从而掩盖这个问题。 ### 设计提案一旦落地就冻结 当一份设计提案的实现落地之后,把提案同步一次,使其与实际构建出来的东西一致:每一项提议的改动都用过去时来写,其范围内不再残留任何待办工作、未来工作、未决依赖或"暂缺"之类的表述。这次同步之后,该提案即被冻结。后续的指标、仪表盘和渐进式增强,改为写入持续维护的运维文档。只有真正细小的可选后续事项才可以留在未来工作之下;绝不能留下大的改动。 ### 用锚点链接做交叉引用 引用另一个章节时使用 markdown 锚点链接,绝不要用章节符号或光秃秃的章节编号。要按站点的 slug 规则核实每个锚点都能解析到一个真实存在的标题,因为断掉的锚点会不声不响地渲染成纯文本。 ## 声明代码中查找的每一个模块 每一次查找另一个模块的服务,都必须有一条声明作为支撑。如果代码请求模块管理器去查找某个模块,无论是直接查找还是通过辅助方法查找,那个模块的名称都必须出现在该提供方的必需模块列表中。缺少声明不会在启动时失败;它会在那条代码路径第一次被触发时抛出异常。用一个宽泛的 catch 把调用包起来并不能替代声明:要声明该模块,并且只对提供方的瞬时不可用保留防御性处理。审查一个分支时,要在每一个被改动的模块中搜索模块管理器的查找调用,并确认每个目标模块都已声明。 ### 更新每一个实现,不只是主实现 当你往某个核心模块的服务列表中添加服务时,要更新该模块的所有提供方实现,包括独立工具和测试装置所使用的模拟提供方。要在代码树中搜索子类,而不要想当然地认为只有一个。主服务器并不是唯一的打包形态;独立工具是用模拟提供方和一部分模块启动的,因此对核心模块契约的改动也必须覆盖到它们,否则那些工具会在启动时失败。 ### 优先使用已经填充好的服务,而不是协调模块 接收器模块和分析器模块通常不会声明集群协调模块,因此直接去查找它会在运行时抛出异常。应当走核心模块的对等节点管理服务,协调层已经把它填充好了,而且它暴露了大多数组件所需要的对等节点列表。如果某个模块确实需要协调原语,就显式声明那个模块。 ### 不要通过线程局部变量这类旁路通道传递意图 把调用方的意图通过一个线程局部变量传下去、让下游代码去读取,会在相距很远的代码路径之间制造出不可见的耦合,会在执行器和事件循环这类异步交接处发生泄漏,也会让人无法从方法签名理解其行为。应当扩展接口:添加一个参数、一个方法,或者一个会出现在签名中的模式值,哪怕这要多写一些代码行。少数例外是:整个业界都已统一采用线程局部变量的上下文传播,以及由框架强制实施的安全主体。 ## 等模式变更传到所有节点后再写入数据 当模式或元数据的变更被广播给协调节点,再异步传播到数据节点时,一次定义调用在协调节点接受它的那一刻就会返回,并带回一个修订标记,而不会等待其他节点。如果一次数据写入所依赖的定义还没有传播到位,它会在执行节点上被丢弃,只留下日志并被跳过,所以必须要有一道栅栏。 ### 在修订水位线上等待 发出定义变更之后,要在客户端的模式监听器上阻塞,直到每一个存活节点都报告自己至少已经应用了返回的那个修订版本,并且设置超时上限,然后再放行依赖于它的写入。针对注册表做一次写后读的检查,并不能证明传播已经完成。 ### 处理删除操作和同步存储 当一次删除没有返回可用的修订标记时,退回去使用专门的等待删除完成的调用。对于定义语句在协调节点上就是同步执行的存储,完全不需要栅栏。不要重新引入那种更老的写法——反复轮询查找调用直到能读到自己写入的内容;它早于屏障接口出现,现在已经被取代了。 ### 确认你读的是哪个客户端库 屏障类型和监听器类型可能位于代码树内的一个客户端分叉版本中,而不在独立发布的客户端里。它们没有出现在已发布的客户端中,并不能证明这个接口已经消失了。 ## 不要猜,先读源码和文档来确认 绝不要猜测或臆断。所有分析都必须建立在源代码、文档或观察到的行为之上。要去读实现,而不是根据名字或惯例去假设一个功能是怎么工作的。在提出设计方案之前,先读项目文档、子模块内部的指引文件,以及各个 readme 文件。 ### 核实配置项和标志 查清楚哪些标志和环境变量是真实存在的、它们的默认值是什么、它们是如何被解析的,包括任何把标志与环境变量自动绑定起来的前缀约定。 ### 读一读你所依赖的那些项目 在假设某项能力存在之前,先读一读所涉及的依赖项和配套项目的源码与文档:部署图表看支持哪些字段,测试基础设施看支持哪些选项,客户端库看有哪些可用的调用。如果某个配套项目很可能已经在本地检出,那就问出本地路径,因为搜索一份本地克隆要比通过远端接口一个个取文件快得多。 ## 在交给持续集成(CI)之前,先在本地运行代码和测试 推送之前,先在本地把代码跑起来、把容器启动起来、把测试执行一遍。持续集成不是用来试错的环境。 ### 校验路径和目录结构 去查数据实际落到了哪里,而不要从某个标志的名字去推断目录名;一个根路径选项经常会在其下创建一个名字不同的子目录。 ### 校验生成或编辑过的配置 编辑配置文件之后,尤其是用流编辑器或脚本编辑过之后,要用一个真正的解析器去校验它们,然后再提交。损坏的配置会在自动化流程中造成无声的失败。 ### 确认镜像和二进制文件确实存在 在编写依赖某个容器镜像的流水线配置或端到端配置之前,先核实该镜像确实已经发布;并且在编写会用到镜像内某些二进制文件、shell 和目录的命令之前,先确认它们在镜像里存在。镜像是否可用,取决于发布工作流是否已经完成。要留意镜像存放的位置:第一方项目镜像可能在某个代码托管平台的镜像仓库上,以完整的提交标识符作为标签,而官方镜像和第三方镜像则在公共镜像仓库上,使用常规的标签。 ## 通过项目的提交工作流和获准的自动化步骤来推送 使用项目专用的提交工作流来提交并推送到评审分支。它会在每次推送之前运行预检,例如编译、风格检查和许可证头校验,并且在评审请求还不存在时创建一个。 ### 遵守自动化允许列表 组织可能会对第三方自动化步骤强制实行允许列表,要求每一个步骤都固定到一个已批准的修订标识符上,而不是固定到某个版本标签上。如果某次评审因为某个步骤不被允许而被阻塞,就去查阅已批准列表,并更新工作流文件以使用被批准的固定标识。由平台厂商或组织自身维护的步骤,通常无需固定即被允许。 ### 说出缺了什么,而不是靠猜 如果你找不到源码、不知道某个东西该怎么跑,或者代码本身没有把答案说清楚,那就去问,而不要臆断。在调查运行时行为时,要用真实数据搭一个本地测试,观察实际输出,而不是去预测它。