☰
从手动实践到框架开发:重复代码如何变成可复用骨架
2026/11/3 20:58:56 网站建设 项目流程

我大概到工作第三年才真正想明白"框架"这两个字意味着什么。当时我给自己定的框架开发实战笔记恰好整理到第六章,我把这一章的第一小节命名成"从手动实践到框架开发",原因很朴素:我见过太多人一上来就对着SpringBoot源码空谈框架,却连自己项目里重复了三遍的代码都懒得抽。这篇内容算是那个6.1小节的正式展开,用我自己真实走过的路子,讲清楚框架到底是怎么从手动实践里一步步"长"出来的。如果你也是写了好几个类似功能、开始闻到自己代码里有重复味道的人,这篇应该能帮你把模糊的感觉变成清晰的方法。

1. 三个真实场景:手动实践是怎么把我逼到"想写框架"的

1.1 场景一:每接一个第三方接口,都在重复同一件事

当时项目里要对接的第三方服务很多:短信、支付、快递查询、电子发票,五花八门。每个第三方的接口风格都不一样,有的是RESTful JSON,有的是SOAP XML,有的还要处理一堆加密签名规则。每对接一个,我的流程都高度雷同:看对方文档、写签名工具、封装HTTP请求、做鉴权、加超时重试、解析返回、设计异常处理。一套下来少则一天,多则一周。

等到对接第五个的时候,我复制粘贴的痕迹已经非常明显了,连重试策略都忘了收敛,五个对接代码里各写了一套,有的超时时间是3秒,有的是5秒,有一个干脆没设超时。同事问我一笔请求失败后到底会自动重试几次,我得打开三份文件才能回答他。那一刻我意识到,这种"手动实践"压根不是勤奋,而是在给团队埋雷。

1.2 场景二:同一个导出流程,我写了六遍

另一个场景更典型:报表导出。一开始只是一个订单导出,流程是查数据库、组装列表、用POI生成Excel、传OSS、发通知。第二个月加了一个用户导出需求,流程一模一样,只是查询条件和列定义变了。我当时又复制了一份,把查询方法改一改,列头改一改,完事。

第三个月,同事要用同样的逻辑导出财务汇总。我看了一眼那两份几乎逐行相同的代码,第一次产生了"这个函数不该被复制"的念头。可那会儿我只会复制,硬着头皮又写了一遍。六份导出代码,后来因为Excel样式调整,我改了六遍,其中两遍还漏改了,线上导出来的表格列宽是旧的。改到第三遍的时候,我连骂自己的力气都没了,只想赶紧找一个不用再复制的方法。

1.3 场景三:测试脚本越写越像"复制粘贴运动会"

测试这边也有类似问题。早期我们做接口自动化,用的是最原始的方式:Postman里存一堆请求,每次上线前手动点一遍。后来开始写Python脚本,用requests逐个调用,断言写了一大篇。每来一个新接口,测试脚本就复制旧脚本改改URL和断言。

直到我接触pytest框架,才注意到人家早就把"收集用例、执行用例、断言、出错定位、生成报告"这条流程抽象得干干净净。我只需要写一个test_开头的函数,其余的交给框架。那会儿我真切感受到,原来"框架"不是某个高深莫测的概念,它就是把所有人都要重复做的那套流程,替你完成掉,只留出必须由你填的空。

1.4 手动实践的三笔隐性账单

让我真正决定动手做框架的,不是代码行数,而是三笔隐性成本逐渐压过来的结果。

第一笔是改动成本。复制出来的每一份代码都是独立演化的,修一个bug要在所有副本里各修一次,漏掉一个就是线上事故。第二笔是沟通成本。就像前面那个重试超时问题,代码分散在N个文件里,团队里没人能说清楚"标准做法到底是什么",只能靠翻代码猜。第三笔是新人成本。新同事入职,想搞清楚我们"怎么接第三方接口",得把五份对接代码都看一遍才能总结出规律。如果能有一个统一封装,他只需要关心自己那部分的业务参数。

这三笔成本加起来,已经足以让我这种懒人下定决心:重复不是勤奋,是负债,而框架就是用来还债的。

2. 框架开发的第一步,不是写代码,而是找"不变点"

2.1 用做菜理解框架的骨架:流程与配料的哲学

很多人面对"框架开发"四个字会不知所措,觉得那是架构师才配干的事,要么去啃Spring源码,要么去买一堆设计模式的书。但我可以负责任地说,框架开发的第一步跟编程语言关系不大,跟设计模式关系也不大,核心只做一件事:把几次相似的实现摆在一起,找出"每次都不变的部分"和"每次都在变的部分"。

打个比方。番茄炒蛋和宫保鸡丁的配方完全不同,但它们的烹饪流程骨架是一样的:备料、热锅、下油、炒主料、调味、出锅装盘。饭店后厨管这个叫标准操作流程。你把这个流程写进菜谱,预留几个空位让厨师填具体食材和调料,这份菜谱就是一个小框架。框架开发的核心其实就是"把流程固定住,把变化值暴露成可填的空位"。

2.2 我实际怎么找不变点:三次实现并排逐行对照

具体做法很笨但非常有效:我把之前写过的三个第三方接口对接代码并排打开,逐行对照。样式完全一致的行,直接用高亮标出来。结果出乎意料——签名计算、HTTP封装、超时重试、响应解析这四块,超过80%的代码逐字相同。真正不同的只有三处:接口URL、请求参数结构、响应结果的处理逻辑。

这三处,就是我这套对接逻辑里的"可变点"。其余那一大片相同的代码,就是"不变点"。框架要做的事,就是把这些不变点固化成骨架,把可变点留成接口或配置位。

2.3 可变点的四种典型类型

后来我做过的框架多了,总结出可变点最常见的几种类型,这里用表格列出来:

可变点类型典型例子框架化的常用手段
流程步骤可变对接接口的鉴权方式不同模板方法 + 钩子方法
输入输出可变查询条件、返回字段不同泛型 + 参数化
算法策略可变同一步骤有不止一种实现策略 + 注册表
配置值可变URL、账号、超时时间不同配置化 / 约定优于配置

识别出可变点属于哪一类,基本上就能确定该用什么手段去框架化。这个分类动作本身,比写框架代码更重要——分类错了,后面全错。

2.4 "写到第三遍必须停下"的判断标准

我后来给团队定了一个简单的规矩:同一段逻辑,第一次正常写,第二次认真抽取,第三次必须停下来抽象。为什么是三次而不是两次?因为两次样本太少,你很难判断变化到底是真实的业务差异还是偶然情况。三次以上,不变的部分基本稳定,这时候抽象出来的框架才靠谱。

这个判断标准帮我避免了很多"为了抽象而抽象"的浪费。我自己也见过反例:有人刚写第二个接口就急着抽象框架,结果抽象出来的API根本覆盖不了第三个场景,推倒重来。记住,框架不是设计出来的,是从样本里归纳出来的,样本够了再动手。

3. 三种框架化的落地手法,由轻到重

框架的形态有很多种,但我实际用过、可落地性最强的就三种:模板方法、策略化、约定优于配置。逐个讲清楚。

3.1 模板方法:骨架固定,钩子留口

这是最入门也最安全的框架化手法。核心思路是:把流程写成固定方法,流程里的某些步骤开放成"钩子",由子类或回调函数来填充。

以对接第三方接口为例,我把"获取Token、签名、发送请求、解析响应、统一异常处理"写成模板方法,把"签名方式"和"响应解析"留给调用方:

public abstract class ThirdPartyClient { // 模板方法:定义整个流程,子类不要重写 public final Response execute(Map<String, Object> params) { String token = getToken(); String sign = doSign(params, token); // 钩子方法 String resp = doHttpPost(buildUrl(), params, sign); return doParse(resp); // 钩子方法 } protected abstract String doSign(Map<String, Object> params, String token); protected abstract Response doParse(String resp); }

调用方只需要继承这个类,实现doSign和doParse,其他东西——超时、重试、鉴权、异常兜底——框架全包了。这比复制粘贴五个HTTP工具类要强得多,因为重试策略、超时时间这类公共逻辑,你以后只改一处就够了。这个手法在很多成熟框架里都能看到,Spring的JdbcTemplate、各种BaseActivity、BaseService,本质上都是模板方法的变体。

3.2 策略与注册表:把算法的选择权交给运行期

模板方法适合"流程相同、个别步骤不同"的场景。但如果流程中某一步的实现有多个选择,而且这些选择可能在运行时才决定,就要用策略模式。

举一个现在很热的例子:AI Agent框架。做Agent的人经常要面对大模型提供商切换的问题,同一段编排逻辑,今天调OpenAI,明天可能换DeepSeek,后天切到本地部署的Qwen。那个"调用大模型"的动作就是一个策略点。框架的做法是定义一个统一的LLM接口,各家当作策略注册进去,上层编排逻辑完全不感知背后是谁。

class LLMClient(ABC): @abstractmethod def chat(self, messages: list[dict]) -> str: ... class OpenAIAdapter(LLMClient): def chat(self, messages): ... class DeepSeekAdapter(LLMClient): def chat(self, messages): ...

策略落地的关键还在于"注册表"。如果每次都在业务代码里new具体的Adapter,那策略等于没策略。需要一个Registry,让调用方只在配置文件或初始化阶段声明一次"我要用哪家",后面统一从注册表取。

class LLMRegistry: _clients: dict[str, LLMClient] = {} @classmethod def register(cls, name, client): cls._clients[name] = client @classmethod def get(cls, name) -> LLMClient: return cls._clients[name]

很多主流Agent框架(LangChain、agno这类)的核心抽象就是这个套路。你再想想SpringBoot的自动配置,本质也是这个逻辑:框架定义接口,starter通过条件注解加载对应实现。你从源码里读到的那些套路,其实就是策略加注册表,一点也不神秘。

3.3 约定优于配置:少写即正义,但别滥用

第三种手法最强大,也最容易产生"黑魔法"感。它的核心是:用一套人人遵守的约定代替显式配置,让框架在绝大多数情况下"什么都不用配就对了"。

最典型的例子还是SpringBoot。传统Spring项目要写一大堆XML配置,SpringBoot直接按目录结构约定自动装配:类放在controller包里,框架自动认为它是Controller;实现了CommandLineRunner,框架自动在启动后执行它;引入了一个starter依赖,框架自动加载对应的自动配置类。

同样的道理也体现在若依框架(RuoYi)上:按它的命名规范写Controller,用注解声明权限标识,框架自动完成鉴权、操作日志、数据权限过滤。再比如pytest:你只需要把测试函数命名为test_开头,pytest自动收集、自动执行、自动断言,连测试报告都给你生成好,这就是约定优先于配置的力量。

但我要提醒一句:约定优于配置是把双刃剑。约定意味着"隐性知识",新人不知道约定,框架在他眼里就是纯黑魔法,出了问题完全无从下手。所以用这个手法时,必须有非常详实的文档和示例代码,最好在项目里提供一套完整的约定示范工程。

3.4 三种手法怎么选:看变化发生的位置

我用一句话总结我的选择逻辑:

  • 流程稳定、步骤可替换,用模板方法;
  • 算法多选一、运行时才决定用哪个,用策略加注册表;
  • 大部分场景都不需要变化、只有少数特例才要覆盖,用约定优于配置。

多数成熟框架是三种手法组合使用的。SpringBoot主推约定优于配置,但内部大量使用条件装配(策略模式),对外又暴露了各种继承点(模板方法)。理解这三种手法,再去看SpringBoot、若依、pytest这些框架的源码,会有一种"原来是这么回事"的亲切感。

4. 一次完整实战:从手动接口开发到内部框架的全过程

理论讲多了容易飘,放一段真实经历把整个过程串起来。

4.1 需求场景:十几个管理后台接口同时砸过来

当时负责一个后台管理系统,业务方一口气提了十多个模块的CRUD接口:用户管理、角色管理、公告管理、字典管理……每一个都是标准的"列表查询、详情、新增、编辑、删除"。如果按老路子来,每个接口要写Controller、Service、Mapper、实体类、校验逻辑,再加分页查询和参数校验,一套下来得两三个小时,十几个就是几十个小时。

这块业务的重复度已经高到肉眼可见了。十个模块的差异,说到底就是表名不同、字段不同、个别模块多了几个特殊校验。这不就是典型的"流程不变、数据变"的可变点场景?

4.2 第一版手动实践:复制、替换、改字段、埋雷

一开始我就是最原始的打法:把用户管理那一套代码复制出来,全局替换类名和表名,再检查几处字段映射。做过的人都懂,这种活儿不费脑子但费眼神,而且极其容易埋坑。替换漏了某个字段,编译报错倒是小事,最怕的是运行期查出一个压根不该出现的字段,或者分页查询把别的表的字段带出来了。

那时候我干着干着就产生一种荒谬感:我一天下来生产了好几千行代码,但真正跟业务逻辑有关的可能只有几十行,剩下全是重复的结构。这就是最典型的手动实践阶段,它不创造价值,只是在把已有代码复制成更多副本。

4.3 第二版参数化:把差异收敛到配置和泛型

复制第三次之后我开始抽公共部分。思路很直接:把每次必改的东西抽进配置,表名、实体类、字段映射、查询条件、分页参数。Service层写一个通用的CrudService,用泛型接收实体。

public class CrudService<T> { @Autowired private BaseMapper<T> mapper; public PageResult<T> page(PageQuery query) { return mapper.selectPage(query); } public T getById(Long id) { return mapper.selectById(id); } public int create(T entity) { return mapper.insert(entity); } public int update(T entity) { return mapper.updateById(entity); } public int delete(Long id) { return mapper.deleteById(id); } }

这时候写一个新接口,只需要建一张表对应的实体类,继承CrudService,再写一个几乎为空的Controller。每个接口的开发时间从三个小时压缩到半小时。这一版让我第一次尝到"参数化"的甜头:我把所有不变的骨架写了一遍,之后每个新需求只需要填那三分之一的差异。

4.4 第三版框架化:代码生成器与约定模板

半小时还是太慢,瓶颈变成了"建实体类和空Controller"这件无意义的体力活。于是参考了若依框架里的代码生成器思路:根据数据库表结构自动生成实体、Mapper、Service、Controller四层代码,开发者只需要手动填补业务校验等特殊逻辑。

到这一步,新接口的开发节奏变成了:建表、跑一次代码生成器、把生成代码放进项目、补三行特殊校验、完成。十分钟左右一个接口。代码生成器本身,其实就是把我们已经总结好的"骨架模板"用自动化手段输出出来,它需要配合约定使用:表名怎么转类名、字段怎么转属性、哪些类型怎么映射,全是一套规则。

这版做出来之后,团队里最直观的感受是:新来的同事第二天就能上手写接口,而且写出来的代码风格和老师傅一模一样。这就是框架的价值,它不止省时间,还统一了团队的代码标准和心智模型。

4.5 三版效率与维护成本对比

对比项手动复制版参数化版框架化版
单个接口耗时2到3小时约30分钟约10分钟
新增接口成本复制+改字段建实体+继承建表+生成代码
改一处公共逻辑所有副本逐个改,容易漏只改基类,一处生效改基类+生成器模板
新人上手难度需要通读多份代码需要理解泛型只关心业务逻辑

这个表我后来在内部技术分享里放过很多次。每次我都会强调,三版的差异关键词不是"代码量",而是"变更的传播半径"。手动复制版里一次公共逻辑改动要扩散到N个副本,框架化版本里公共逻辑只有一份,改动天然集中。

4.6 从若依和SpringBoot身上印证这个演进路线

实战做完之后,再回头看成熟的框架,会发现它们就是别人的"从手动到框架"沉淀。

若依框架的代码生成器,本质上就是把我那第三版框架化做到极致,还加上了前端页面生成,你建完表它连增删改查的界面都给你生成了。SpringBoot的starter机制,就是把"我这套功能怎么接入各种环境"这件事框架化。再比如PyTorch的nn.Module,它封装好了参数管理、设备迁移、自动求导等一整套流程,我们只需要写forward方法——这就是模板方法思想和框架化的最佳代表。

你直接拿来用这些成熟框架,就是在用别人踩过的坑换自己的时间。而你要自己写一个小框架,走的也一定是类似路径:先手动,再参数化,最后框架化。跳过中间步骤直接画架构图,画出来的多半是空中楼阁。

5. 框架的代价与边界:千万别为了框架而框架

这段得泼点冷水。框架开发很爽,但不能把什么都框架化,否则会从"提升效率"变成"制造摩擦"。

5.1 过度框架化的典型症状:配置比业务代码长

我见过最典型的反面案例:一个内部项目,业务代码只有几千行,配置文件写了上万行,各种XML、YAML、属性配置层层嵌套。解Bug的时候,需要像剥洋葱一样跨五六个配置文件才能弄清一个参数最终落在哪里。后来团队实在受不了,推倒重来,才发现大部分配置项根本是无效的,是"可能以后会用到"才加上的。

这就是过度框架化的代价:框架把灵活性拉满的同时,也把理解成本拉高了。灵活性和易用性是一对天然矛盾,框架的每一层抽象都在消耗未来的阅读时间。所以框架开发一定要学会做减法,不是把所有的可变点都开放出来,而是只开放那些真实业务中出现过的可变点。

5.2 该用成熟框架还是自研:我的判断标准

要不要自研框架,我的判断标准非常简单:如果市面上有成熟且被广泛验证的框架,能用就用,不要自研。新手最容易踩的坑是"觉得自己写的框架比SpringBoot好",这个概率约等于零。SpringBoot、若依、pytest、React/Vue、PyTorch,这些框架背后是无数生产环境打磨出来的,它们在边界情况、性能、生态上的积累,远不是个人在半个月里能追上的。

那什么时候该自研?我总结下来需要同时满足几个条件:团队内重复度极高且痛点明确、已有成熟框架覆盖不到或改造代价大于自研、你已经完成清晰的不变点与可变点分析。比如我们后来做的一套内部Agent编排框架,就是因为通用Agent框架下写业务编排要粘大量胶水代码,而我们的编排流程高度固定,才决定自己封装一层薄薄的模板。如果你满足不了这些条件,别碰自研,老老实实选成熟框架。

提示:判断是否要自研时,可以先问自己一个问题——"这个坑是只有我们团队有,还是全行业都有?"全行业都有的坑,早有人填了,直接拿着用;只有你们团队独有的坑,才值得自研去填。

5.3 "先手动后框架"这个顺序为什么反不得

最后强调一下顺序问题。我看到过不少团队上来就画架构图,打算一劳永逸地"设计一个完美框架",结果做出来没人用。原因很简单:没经历足够多的手动实践,抽象出来的东西全是拍脑袋的"应该会变"和"可能有用",不是从真实业务里来的。

框架的开发一定要以手动实践为前提。手动实践不是浪费时间,你写的每一份重复代码都是在为抽象收集样本。样本够了,框架自然浮出水面;样本不够,你设计出来的只是一堆装饰性抽象,看着规整,实际一点用没有。我自己走过的路就是最好的证明:如果没有前六个第三方接口的手动实现,我压根不知道签名和解析才是真正的可变点。

6. 框架落地的最后一公里:从"我的框架"到"团队的框架"

一个框架写完,真正的问题才开始:怎么让团队接受它、用起来、持续演进。这里分享几条我踩过坑才总结出来的经验。

6.1 文档要写"怎么用",更要写"不要这样用"

文档这个问题没必要展开,真正值钱的是"反例"。新同事用框架时最容易踩的坑,不是不知道怎么用,而是按错误方式用了还觉得自己没错。我后来在内部框架的文档开头,专门加了一个"不要这样用"的区域,把历史上所有人写出的错误用法列出来,配上出错现象和正确做法。

举个例子,我们的内部CRUD框架默认带逻辑删除,但有人为了省事,在一个不该逻辑删除的明细表上直接调了delete方法,结果数据全被"假删"了,查都查不出来。这个案例被我写进文档的"不要这样用"区里,后面再也没人犯过同样的错。这个区比整个代码注释都有效,因为人都是看到别人摔过的坑才长记性。

6.2 渐进式落地与逃生通道缺一不可

框架落地最容易翻车的原因之一,是要求团队一次性全量迁移。正确姿势是渐进式的:先拿一个风险低的新项目做试点,跑顺了再逐步把老项目迁移过来。同时框架要提供"半自动模式"——允许使用者绕过框架,直接手动控制某些步骤,保留逃生通道。

没有逃生通道的框架,遇到一个预期之外的业务就会变成灾难。使用者被困在框架的规则里,做不了任何特殊处理,只能去改框架源码,改完框架的公共逻辑又污染了其他所有使用者。这个毒瘤一旦出现,框架离废弃就不远了。

6.3 版本演进的三阶段节奏:先跑通、再加固、后扩展

框架版本的演进节奏,我总结为三步:先跑通、再加固、后扩展。

第一阶段,只覆盖当前已确认的几个场景,不追求完备,接口保持尽量少。这时候框架多一个接口都是错,因为你还不知道其他场景会怎么用。第二阶段,基于真实使用反馈补齐边界处理、错误提示、日志输出,把"能用"变成"好用"。第三阶段,确认骨架稳了,再开放插件机制、扩展点,让有特殊需求的人能自行扩展。

三个阶段的顺序非常重要。我见过一个框架,第一天就把插件机制设计得完美精巧,结果主路径都没人用,插件机制成了自己欣赏的摆设。框架的扩展点永远是在使用者的真实需求逼出来之后再加,提前做就是浪费。

6.4 框架带给我的最大成就感与最大责任

写到这里,说点个人的真实感受。框架开发真正让我上头的,不是代码复用或效率提升,而是它把所有类似问题变成了同一套答案。以前新需求来了我要从头想一遍,现在我敢跟业务方说:这个不用讨论,走标准流程,你们只要给我三个参数。那种"问题有了标准答案"的感觉,对工程师来说是很大的成就感。

但反过来,框架也是我在这一行里背过的最大的锅。框架里的一个抽象错误,会被几十个项目同时放大。所以我现在的态度一直很收敛:能不动底层就不动底层,能动一个方法绝不动一个接口。框架的职责边界越清晰,它的生命周期就越长;边界一旦糊了,后面只会越来越难受。

最后分享一个小技巧。我每次写新功能之前,都会先问自己"这功能跟之前哪个功能有七分像"。如果有,我就把旧代码打开,把那三分不同的地方先圈出来。那三分,就是我该为框架预留的扩展点;那七分,就是我该复用的骨架。这个方法不起眼,但帮我避开了大部分"过度设计"和"重复劳动",也算是我从手动实践走向框架开发这一个章节里,最实用的一个落地动作。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询