我们的Web3.0项目上线之后,代码越来越乱,bug越来越多,维护成本越来越高。后来我们花了一个月时间做了全面的代码重构,从烂代码变成了优雅代码。本文是这次重构的经验总结,包括智能合约重构、前端代码重构、测试体系建设、安全改进等。如果你在做Web3.0项目,或者对代码重构感兴趣,希望这篇文章能帮到你。

一、为什么要重构

先说说我们为什么要做代码重构。

我们的项目是一个DeFi应用,包括智能合约和前端DApp。最开始为了快速上线,代码写得比较随意,能跑通就行。上线之后,随着功能不断增加,代码越来越复杂,问题也越来越多。

主要的问题有几个:

  1. 智能合约代码混乱:合约里有大量重复代码,函数职责不清晰,变量命名不规范,注释很少。每次加新功能都要改很多地方,很容易出bug。
  2. 前端代码结构差:前端用的是React,但是组件设计不合理,状态管理混乱,业务逻辑和UI混在一起,代码重复严重。
  3. 测试覆盖率低:智能合约只有少量测试,前端几乎没有测试。每次改代码都担心出问题,上线之后经常出bug。
  4. 安全隐患多:智能合约有一些安全隐患,比如重入攻击、整数溢出、权限控制不严等。虽然还没被攻击,但是风险很大。
  5. 部署和升级困难:合约部署流程复杂,没有自动化,每次升级都要手动操作,容易出错。

这些问题导致开发效率越来越低,bug越来越多,团队的信心也受到了影响。所以我们决定停下来,花一个月时间做全面的代码重构,把烂代码改成优雅代码。

二、重构的原则和目标

重构之前,我们先确定了重构的原则和目标。

原则

  1. 不改变外部功能:重构只改内部实现,不改变对外的接口和功能
  2. 小步快跑:分阶段重构,每一步都可测试、可回滚
  3. 测试先行:重构之前先写测试,确保重构之后功能不变
  4. 安全第一:智能合约的重构,安全是第一位的,不能引入新的安全漏洞
  5. 文档同步:重构的同时更新文档,保持代码和文档一致

目标

  1. 智能合约代码结构清晰,模块化,可复用
  2. 前端代码组件化,状态管理清晰,业务逻辑和UI分离
  3. 测试覆盖率达到80%以上,核心代码100%覆盖
  4. 消除已知的安全隐患,通过安全审计
  5. 建立自动化的部署和升级流程

三、智能合约重构

智能合约是Web3.0项目的核心,也是重构的重点。我们用的是Solidity,重构主要做了以下几个方面。

1. 模块化拆分

最开始我们把所有逻辑都写在一个合约里,有上千行代码,很难维护。重构的时候,我们按照功能把合约拆分成了多个模块:

  • 核心逻辑合约:处理核心的业务逻辑
  • 权限管理合约:处理角色和权限
  • 金库合约:处理资金的存入和取出
  • 预言机合约:处理价格数据的获取
  • 升级管理合约:处理合约的升级

每个合约只负责一个功能,职责单一,代码清晰。合约之间通过接口调用,降低了耦合度。

2. 引入OpenZeppelin库

最开始我们很多基础功能都是自己写的,比如权限控制、安全数学运算、可升级代理等。但是自己写的代码容易出bug,也不够规范。

重构的时候,我们引入了OpenZeppelin库,这是Solidity领域最权威的开源库,代码经过了大量审计和测试。我们用了它的Ownable(权限)、SafeMath(安全数学)、ReentrancyGuard(重入保护)、ERC20(代币标准)等模块。

用了OpenZeppelin之后,代码量减少了很多,安全性也提升了。而且因为是标准库,其他开发者看代码的时候也更容易理解。

3. 统一命名和注释规范

最开始的代码命名很随意,有的用拼音,有的用英文缩写,有的变量名很长,有的很短。注释也很少,只有关键的地方有注释。

重构的时候,我们制定了统一的命名规范:

  • 合约名用大驼峰,比如MyContract
  • 函数名和变量名用小驼峰,比如myFunction
  • 常量用全大写加下划线,比如MY_CONSTANT
  • 事件名用大驼峰,比如MyEvent

注释方面,要求每个合约、每个函数、每个复杂的逻辑都要有注释。注释用NatSpec格式,这样可以自动生成文档。

4. 消除重复代码

最开始的代码里有很多重复的逻辑,比如权限检查、参数校验、事件触发等,在很多函数里都写了一遍。

重构的时候,我们把重复的代码提取成内部函数或者修饰器(modifier)。比如权限检查用onlyOwner修饰器,参数校验用内部函数。这样代码简洁了很多,也不容易出错。

// 重构前
function withdraw(uint256 amount) external {
    require(msg.sender == owner, "not owner");
    require(amount > 0, "invalid amount");
    require(amount <= balance[msg.sender], "insufficient balance");
    // ... 逻辑
}

// 重构后
modifier onlyOwner() {
    require(msg.sender == owner, "not owner");
    _;
}

function withdraw(uint256 amount) external onlyOwner {
    require(amount > 0, "invalid amount");
    require(amount <= balance[msg.sender], "insufficient balance");
    // ... 逻辑
}

5. 安全改进

重构的时候,我们对智能合约做了全面的安全检查,修复了很多安全隐患:

  • 加了ReentrancyGuard,防止重入攻击
  • 用SafeMath做整数运算,防止溢出(虽然Solidity 0.8之后内置了溢出检查,但是用SafeMath更保险)
  • 检查了所有的外部调用,确保没有权限问题
  • 加了暂停功能,出现紧急情况可以暂停合约
  • 加了事件日志,方便追踪和排查问题

四、前端代码重构

前端DApp的重构也是重点。我们用的是React + ethers.js,重构主要做了以下几个方面。

1. 组件化拆分

最开始的前端代码,很多页面都是一个大组件,几百行代码,UI和业务逻辑混在一起。重构的时候,我们按照功能把组件拆分成了:

  • 页面组件:负责页面的整体布局
  • 业务组件:负责具体的业务功能,比如转账表单、质押面板
  • UI组件:负责纯展示,比如按钮、卡片、模态框
  • Hook:负责业务逻辑,比如useWallet、useContract、useBalance

每个组件只做一件事,代码清晰,可复用性高。

2. 状态管理优化

最开始的状态管理很混乱,有的用useState,有的用useContext,有的存在localStorage里,数据不同步的问题经常出现。

重构的时候,我们引入了状态管理库(用的是Zustand,比Redux轻量),把全局状态统一管理。组件内部的状态用useState,跨组件的状态用全局store。这样状态清晰了很多,数据同步的问题也解决了。

3. 合约交互封装

最开始的代码里,和智能合约的交互逻辑散落在各个组件里,每个组件都自己创建合约实例、调用方法、处理错误。重复代码很多,而且错误处理不统一。

重构的时候,我们把合约交互封装成了专门的服务层:

  • 创建了合约实例的单例,避免重复创建
  • 封装了常用的合约调用方法,统一处理错误和loading状态
  • 封装了交易的发送和确认逻辑,统一处理交易状态
  • 封装了事件监听,统一处理合约事件

这样组件里只需要调用服务层的方法,不需要关心底层的实现,代码简洁了很多。

4. 错误处理和用户反馈

最开始的代码,错误处理很随意,有的地方catch了但是不提示用户,有的地方直接把错误信息弹出来,用户看不懂。

重构的时候,我们统一了错误处理:

  • 所有的异步操作都有try-catch
  • 错误信息转换成用户能理解的提示
  • 交易的状态(pending、success、failed)都有明确的UI反馈
  • 加了全局的错误提示组件,统一展示错误信息

5. TypeScript改造

最开始的前端代码是JavaScript,没有类型检查,经常出现类型错误。重构的时候,我们把代码改造成了TypeScript,给所有的变量、函数、组件都加上了类型。

用了TypeScript之后,很多潜在的bug在编译阶段就发现了,代码的可维护性也提升了很多。虽然改造花了不少时间,但是很值得。

五、测试体系建设

重构之前,我们的测试覆盖率很低,这也是我们不敢轻易改代码的原因。重构的时候,我们把测试体系建设作为重点。

1. 智能合约测试

智能合约的测试用的是Hardhat + Waffle + Chai。我们给所有的合约都写了测试,包括:

  • 正常流程的测试:每个函数的正常使用场景
  • 边界条件的测试:参数为0、最大值、空值等
  • 异常情况的测试:权限不足、余额不足、参数错误等
  • 安全测试:重入攻击、整数溢出等安全场景

核心合约的测试覆盖率达到了100%,所有的函数和分支都有测试覆盖。

2. 前端测试

前端测试用的是Jest + React Testing Library。我们给核心的组件和Hook都写了测试:

  • 组件渲染测试:确保组件能正常渲染
  • 交互测试:点击按钮、输入表单等交互是否正常
  • Hook测试:业务逻辑是否正确
  • 集成测试:核心流程是否正常

前端的测试覆盖率达到了80%以上。

3. CI/CD

我们搭建了CI/CD流程,每次提交代码都会自动运行测试、代码检查、编译。如果测试不通过或者有代码规范问题,就不能合并。

这样保证了代码的质量,也防止了bug进入主分支。

六、部署和升级流程改进

最开始的合约部署和升级都是手动操作,很容易出错。重构的时候,我们把部署和升级流程自动化了。

1. 部署脚本

用Hardhat的部署脚本,把合约部署的流程写成了代码。包括合约的编译、部署、初始化、验证,都可以一键完成。

2. 可升级代理

我们用了OpenZeppelin的可升级代理模式,合约逻辑可以升级,但是地址和存储不变。这样用户不需要切换合约地址,体验更好。

升级的时候,只需要部署新的逻辑合约,然后调用代理的升级方法就行,很方便。而且升级有多重签名保护,需要多个人确认才能升级,安全性更高。

3. 前端部署

前端的部署也自动化了,代码合并到主分支之后,自动构建和部署到IPFS或者服务器上。

七、重构的效果

经过一个月的重构,效果很明显:

  1. 代码质量提升:代码结构清晰,命名规范,注释完善,可读性和可维护性大大提升
  2. bug减少:测试覆盖率提升之后,很多潜在的bug被发现了,上线之后的bug减少了70%
  3. 开发效率提升:代码模块化之后,加新功能的速度快了很多,以前要改好几个文件,现在只需要改对应的模块
  4. 安全性提升:修复了很多安全隐患,通过了第三方安全审计,团队和用户都更放心了
  5. 团队信心提升:代码变好了,团队的信心也提升了,大家更愿意在这个基础上开发新功能

八、经验总结

这次重构,我们总结了一些经验。

1. 技术债务要及时还

代码烂了就要及时重构,不要等烂到没法维护了才重构。技术债务越积越多,最后重构的成本会非常高。最好的方式是每次加新功能的时候,顺便重构附近的代码,持续改进。

2. 重构之前先写测试

重构之前一定要先写测试,确保重构之后功能不变。没有测试的重构就是赌博,很容易改出bug。

3. 小步快跑,不要贪多

重构要分阶段进行,一次只重构一个模块,重构完就测试、验证,没问题了再继续。不要想着一次性把所有代码都重构完,那样风险太大。

4. 安全是Web3.0的生命线

Web3.0项目的智能合约管理着真金白银,安全是第一位的。重构的时候一定要把安全放在首位,每一步都要仔细检查,最好能做第三方安全审计。

5. 文档和代码同样重要

重构的时候不要忘了更新文档。代码再好,没有文档,后来的人也看不懂。好的文档能大大降低维护成本。

6. 不要为了重构而重构

重构的目的是提升代码质量和开发效率,不是为了用新技术或者写漂亮的代码。如果一段代码虽然不完美,但是稳定、好维护,就没必要为了重构而重构。

九、写在最后

Web3.0的项目和传统的Web项目不一样,智能合约一旦部署就很难修改,而且管理着真实的资产,所以代码质量和安全性尤为重要。

这次重构虽然花了一个月时间,但是很值得。代码从烂代码变成了优雅代码,bug少了,效率高了,团队的信心也回来了。

如果你也在做Web3.0项目,代码也越来越乱,不要犹豫,赶紧重构吧。越早重构,成本越低,收益越大。

最后用一句话结束本文:"代码是写给人看的,顺便让机器执行。"愿每一个Web3.0开发者都能写出优雅、安全、可维护的代码,为用户提供更好的产品和服务。