AI 辅助代码重构的一些原则
目录
已经 2025 年了,AI 工具可以为我们生成高质量的代码,甚至可以帮我们做好代码片段的协调组织。对于从 0 开始的项目,项目代码质量都会比历史项目高出好几个等级,前提是编码人在逻辑能力与代码组织能力要求比较强,才能 hold 住 AI 的代码生成与应用,甚至对 AI 生成的代码进行判别与进一步根据实际情况做调整优化。对于已存在的项目,我们依然可以利用 AI 来帮助重构代码,但是重构代码时,需要知道重构的一些原则,才能更好的指导 AI 辅助我们重构,本文介绍了一些细节上的重构原则,这篇是 2023 年写的,2025 年了我们觉得还没有过时。
重新组织方法函数
1、重复代码
- 同一个类存在相同或类似的方法
- 不同类存在相同或类似的方法
什么叫相同或相似?
方法函数签名相同或类似,我们就认为他们有必要进一步提炼重复代码,提炼的同时,我们仍然很需要注意方法函数取名、参数取名、过程变量取名,让我们阅读一个方法函数,就如同阅读一段简单易懂的文本段。(现在的 GPT 对于通用算法给出的编码都有很好的名称定义)
2、长函数(方法)
代码中的函数怎么理解?
函数 可以理解为 做什么
或 如何做
在很早以前,大部分大佬都建议使用短函数,以前可能还会忌讳函数层次多,导致调用栈多,影响性能,如今硬件告诉发展已经各个编码语言的完善,我们已经不用再太担心函数调用的太多额外开销。
编码除了接近计算机外,我们还要考虑接近人,机器经常执行这段代码,所以这段代码需要接近计算机,且要求高性能,不要让计算机做不必要的操作,而作为代码,在产品生命周期内,可能会被不同的很多人阅读、完善、重写甚至扩展,所以又需要这段代码接近人,让人容易阅读。
从大多开发者的经验总结而来,代码就如同编写一个故事,计算机可以通过代码还原故事展现,人可以阅读代码了解故事、完善故事,读者有时会通过函数的名称,就可以想象出函数的功能,根本不需要知道其中写了什么,只要不影响主要支线故事,甚至这个函数都可以在之后被作者重写,仍然不影响主要支线故事情节与发展。
短函数的好处:
-
复用机会大。
利于抽象过程与组合。
方便作者自己梳理业务流程以及提炼抽象过程,组合各个短函数得到更大的函数。
-
方便阅读。
短函数在阅读时像是在阅读注释,还能让外层代码更加简洁清晰,比如控制层大概就是完成控制逻辑顺序、判断、循环相结合的代码段。
-
方便调整。
短函数其实就是一个过程的抽象,相当于一个故事中的小插曲,编剧随时可以更换,相当于飞机的一个零件,一旦出现问题,更换零件即可。
哪些场景有提炼短函数的信号:
-
需要大量注释的地方。
一旦感觉需要注释来说明点什么的时候,我们就需要把这些要说明的懂些放入一个独立的短函数中,并使用用途来命名,即使是短短一行代码。
-
判断和循环往往都是提炼的信号。
为什么我们建议控制层,尽量只保留控制代码行,我们需要把判断分支逻辑、循环逻辑提炼到函数中。可以表达:结果判断、如何做、做什么。
-
代码超过一屏
这个一屏,根据时代的发展而发展,虽然我们也见不得 50 行的函数的,但在实际底层数据结构没有设计好的业务中,经常会出现曲线救国的代码,他们难免需要超过 50 行的代码才能完成一个独立单一职责的功能。
反正就是一个原则:尽量将一个独立的单一职责的代码逻辑封装成一个通用函数,且尽量短,这个短可能就涉及到各方面的计算机系统知识,操作系统、数据结构、计算机网络等等。
注:关于命名,不要用 怎么样做
来命名,而是以它 做了什么
来命名。如果一块代码,我们暂时无法想到如何给他定义一个有意义的名称时,说明我们暂时还不知道它具备封装的条件,如果已经 2、3 个地方确实需要复制他了,我们即使无法想出一个有意义的名称来时,我们也需要封装,并给他一个 做了什么
的通用名称,并给以一定的注释说明。
3、超级多的 if 或 switch
我们需要考虑多态的方式来替换这种写法,避免每次都要新增一个条件判断,以及一个逻辑分支。
函数抽象了过程,多态抽象了主体,不同主体调用不同的过程逻辑方法,这样这个函数的封装才更有意义。当然在我们还不善用多态或是项目环境暂时不支持多态主体的编写方式时,函数封装还是有一定意义的,至少模块化,方便后续的二次重构或重写,所以我们仍然需要去考虑抽象具体逻辑。
4、inline 与表达式
前面 1、2、3 都建议及时代码即使简短到一行,只要他表达了做了一件事,我们也建议封装。
但这里就是为了防止我们过度封装,一行代码是一个变量和值的比较判断,我们需要提炼吗?
我们的建议是暂时不需要,除非我们已经感觉到今天还会其他逻辑会用到类似的判断,否则,我们就让这个判断语句 inline 在逻辑代码中,不需要单独提炼,直到有人还需要相同的判断。
除了函数 inline,我们在定义变量的时候,要明确他用途,且会被用到,且不会与其他变量或表达式结果的用途一样,否则我们只要用其他相同用途的变量或表达式即可,并不需要额外开辟空间来单独定义一个变量。
private function moreThanFive(){
return $this->num > 5;
}
public function get(){
// if ($this->moreThanFive()){
if ($this->num > 5){
// $price = order.GetPrice();
// return $price;
return order.getPrice();
}
}
moreThanFive 函数如果是一个复杂的表达式逻辑,那我们当然会建议使用短函数来封装这个表达式的判断。
当表达式很复杂,我们可以通过临时变量的定义来为表达式做解释说明,方便阅读,当然也多出了一些额外的变量定义,对于编译型语言来说,对机器的影响不大,但会大大提高可阅读性,对于解释性语言,可能会有点影响,但大部分业务逻辑并不会太消耗内存,更多的问题在于计算,所以比起可阅读与可维护来说,性价比是比较高的。
$isVip = $status && ($id>0);
$isAllow = (1==$allow);
$isCompleted = in_array($doStatus, [1,2]);
if($isVip && $isAllow && $isCompleted){
//do something
}
注:机器并不理解是否 vip,是否允许、是否完成,机器只知道前三行的判断逻辑。而对于人来说,需要知道更高层的逻辑判断:isVip、isAllow、isCompleted。
5、临时变量取名
在循环、长逻辑中,如果存在赋值临时变量,我们建议给变量的名称也取一个适合当下的业务名称定义,虽然短函数的上下文,不容易忘记,但阅读代码是一件高度集中的事情,我们还是不希望阅读者去记忆这些$i、$n、$m等简单到没有任何意义的临时变量名称,如果是索引就用$index,或是$id,如果是一个主体,可以用$one,或更具体 $oneOrder 等名称,不给阅读者带来心理负担,减少临时记忆与回顾。
而且这些变量,容易在同一个函数中前后被赋值为不同的值,具备不同含义,如果出现这个情况,本身他们就应该定义不同的临时变量。
其他语言类似。
主体之间特性移动
1、函数与其他主体的多个函数存在交流,则将函数单纯作为委托函数
2、主体单一职责,如存在多个职责,说明需要考虑继承或组合主体对象
当然如果存在职责比较少的类,其方法可以暂时寄存在其他相关主体类中,或是采用工具函数的方式存在,直到他们相关职责多了起来,就需要重新建立这个主体类。
3、隐藏委托方主体
不要把我们自己和第三方类的关系暴露给调用者,调用者只调用我们代理的方法,至于我们和第三方如何交互、工作原理不暴露给调用者,避免调用者和第三方耦合在一起。
4、直接使用委托方主体
如果我们代理委托方主体的方法越来越多,我们就可以让调用者直接考虑调用委托方主体(新成立一个主体类)的对应方法。调用者就不需要通过我们来代理这些方法。
重新组织数据
1、属性数据隐藏
如果属性变量不允许外部直接访问或修改,设置有私有,且提供访问及设置方法(有时也不提供,只允许主体类自己的方法修改)。
主体类中访问属性变量,可以直接访问变量,也可以通过 get 等间接方法访问变量,如果有需要对变量做额外处理的话,比如缓存等。
2、存在数据与行为的属性,应该定义为主体对象
如果属性变量,包含数据和行为时,即不再是基础数据类型时,应该将其定义为主体类对象。
3、Array 只用于相同数据类型的数组
Array 存在多种形态,形态不固定,容易导致意外。
如果数组中的元素各自代表不同的东西,即数据类型已经不一样,我们建议使用主体类 objec 代替。
简化条件表达式
1、复杂条件判断提炼
2、分支逻辑提炼
对于 1、2,我们的目标就是想让控制逻辑代码,阅读起来就像是在看一段伪代码注释,清晰明了。
3、搬移每个分支都存在的相同代码到条件之外
如果是 return 简单的数据类型,我们也建议直接 return,不需要放到条件外部,如果放外部也没问题,只是为了养成一个及时 return 的习惯,走最短路径。
4、多个条件分支都属于特殊情况时,采用卫语句
卫语句,其实就是特别重视某一分支,在为真时及时返回,其他情况返回默认值,也就是 3 提到的走最短路径。
当然并不是所有条件判断都适合这么写,只有条件极其罕见的情况,所有分支属于正常行为的户啊,我们仍然采用 if … else 的方式。
5、多态完善 switch 分支逻辑
经常会遇到一个表达式,根据对象的类型或一个枚举的不同,而选择不同的行为操作。这个就很典型了,典型的多态行为,我们可以考虑将每个条件式的分支逻辑,封装到对应的 subclass 主体中,并实现一个接口函数(抽象函数)。
简化函数调用
1、函数方法名重命名
原则:如果函数的名称未能揭示函数的用途,那么修改函数名称。
步骤:
- 新建重命名函数
- 旧函数调用新函数
- 旧函数调用点替换成新函数
2、添加函数参数
添加函数参数,是下下策,不得已的时候才这么干。
3、函数职责单一,查询与修改分离
4、相似函数多,可以通过参数化统一函数
5、函数参数较多,其中某个参数频繁传值,可以考虑拆分多个相似函数
4、5 可根据参数个数多少、参数使用频率,再结合团队实际情况适当使用。
6、函数中某个参数是枚举值时,可以考虑拆分多个相似函数
7、尽量使用主体对象作为参数,避免过多的参数列表
在需要传入多个参数时,我们提倡使用主体对象数据结构,除非参数都来自不同的数据结构,但如果来自不同的数据结构,那么我们可能要反思这些数据结构是否合理,或者这个函数的参数是否合理。
8、新建参数对象
在 7 中,如果我们发现数据确实来自于不同的数据结构对象,可以简单理解为是一堆数据泥团,那么我们就可以根据这个客观实际情况,建立一个参数数据对象,比如我们的请求数据对象,就是类似的情况。把这些数据组织在一起,后续我们可能也会发现这个数据对象会慢慢延伸出很多行为。
9、并不是所有属性变量都需要 setter 方法
只有那些有外部变更需求的属性变量,才需要提供 setter 方法函数。
10、使用工厂方法代替构造器
当然我们还需要将构造器作为私有,只能通过工厂方法来 new 对象,使得该类的所有具体对象的构建过程够会经过工厂方法,方便后续增改构建过程。这也是引入工厂方法构建不同类的方式。
class Employee
{
private $_type;
public static $manger = 2;
public static $engineer = 1;
private function __construct($type){
$this->_type = $type;
}
public static function create($type){
// return new __CLASS__($type);
switch ($type) {
case self::$manager:
return new Manager();
case self::$engineer:
return new Engineer();
default:
# code...
return new Employee();
}
}
}
11、使用异常代替错误码
如果某个函数返回一个特定的代码(special code),用以表示某种错误情况,那么改用异常(Exception) 。
清楚的将”普通程序“和”错误处理“分开,这使的程序更容易”理解“。