引言
优秀的函数通常保持简洁。
- 刻意控制函数长度有助于剔除不属于该功能模块的冗余代码,从而提升代码的可读性和健壮性。
- 短小且功能明确的函数,有助于提升命名的准确性。而好的命名可以协助读者快速理解其作用,而无需逐行查看代码,减少了理解成本。
有经验的程序员通常坚持每个函数的行数不超过50行,这种原则对我产生了深远的影响。
然而,理想与现实总是存在差距。尽管短函数的优点明显,但在实际项目中,我们仍然会遇到一些过长的函数。为了更好地处理这一问题,我阅读了《重构》一书,并总结了以下三个关键问题:
- 如何判断一个函数是否过长?
- 函数过长会造成什么负面影响?
- 如何修改和重构「过长函数」?
特征
暗中作祟的函数命名
在《代码重构之[神秘命名]》中我们提到:为取名而付出的纠结,往往能够促使我们去修改设计上的缺陷。
如果一个函数的名称是 A ,但实际上包含了 A B 两部分功能,那剔除 B 这一部分代码,能够有效的精简函数长度。实际上,造成函数变得越来越长,越来越臃肿的原因,大多数情况下,就是因为函数的功能不够单一。
因此,函数功能不单一,不仅代表函数名称不够准确,也可能代表该函数的长度还可以再精简一些。
观察上下文中的注释
我们遵循一条原则:每当感到需要使用注释来说明什么的时候,我们就应该把需要说明的代码部分写进一个独立的函数中,并以其用途(而非实现手法)命名。
因此,我们可以将函数中出现的注释,视为一种函数过长的信号。
为什么重构?
- 长函数提高了理解成本,当需要维护长函数时,会花费更多的时间去定位需要修改的部分。而短函数更具阐释力。
- 长函数的内部常常会杂糅许多计算功能,当我们修改其中一处代码时,还需要确保这不会影响到其他部分,如果上下文存在依赖关系,那修改后还会扩大测试范围。而短函数更易于维护。
如何重构?
提炼函数
我们观察到下面示例代码中,存在一句"打印info中所需信息"的注释,这代表注释之下的代码,可能与注释之上的代码逻辑不属于一个逻辑模块,这是一个重构的信号。
scss
public class Example
{
public void PrintCharacterInfo()
{
PrintTitle("该角色信息如下:");
var character = QueryCurCharacter((c) => c.state == CharacterState.Select);
var characterPrintInfo = character.GetPrintInfo();
//打印info中所需信息
Console.WriteLine(characterPrintInfo.nickName);
Console.WriteLine(characterPrintInfo.sex);
Console.WriteLine(characterPrintInfo.skinName);
Console.WriteLine(characterPrintInfo.hairColor);
}
}
scss
public class Example
{
public void PrintCharacterInfo(Character character)
{
PrintTitle("该角色信息如下:");
var character = QueryCurCharacter((c) => c.state == CharacterState.Select);
var characterPrintInfo = character.GetPrintInfo();
PrintInfo(characterPrintInfo);
}
public void PrintInfo(CharacterPrintInfo printInfo)
{
Console.WriteLine(characterPrintInfo.nickName);
Console.WriteLine(characterPrintInfo.sex);
Console.WriteLine(characterPrintInfo.skinName);
Console.WriteLine(characterPrintInfo.hairColor);
}
}
以查询替代临时变量,再提炼
下面的例子是原作者编写的示例代码(原著中将查询的过程封装成函数,但C#中有更好用的"属性")。使用"查询"替代"临时变量"最大的好处是:
- 可以使"查询的过程"与原函数之间形成明显的边界。
- "查询的过程"函数可以多次复用。
使用"临时变量"的场景是:那些只被使用一次,且后续逻辑不会再变更的变量。如果这个变量可能被使用多次,或者这个变量后续可能会被修改,则推荐你使用"查询函数"。
csharp
public float GetFinalPrice()
{
var basePrice = itemCount * itemPrice;
if (basePrice > 1000)
{
return basePrice * 0.95f;
}
else
{
return basePrice * 0.98f;
}
}
csharp
public float BasePrice
{
get => itemCount * itemPrice;
}
public float GetFinalPrice()
{
if (BasePrice > 1000)
{
return BasePrice * 0.95f;
}
else
{
return BasePrice * 0.98f;
}
}
以命令取代函数
"命令"的定义: 函数,不管是独立函数,还是以方法(method)形式附着在对象上的函数, 是程序设计的基本构造块。不过,将函数封装成自己的对象,有时也是一种有用的办法。这样的对象我称之为"命令对象"(command object),或者简称"命 令"(command)。这种对象大多只服务于单一函数,获得对该函数的请求,执行该函数,就是这种对象存在的意义。
以命令取代函数的好处是,我们可以将复杂的计算,以更加灵活的方式呈现出来,对于这个命令对象,我们甚至可以添加更多的相关操作,比如取消操作。特别是为了应对单元测试、多场景调用的情况,命令对象总是能够灵活的满足上述需求,将命令发起者和命令接收者分隔开。
但命令对象本身就是复杂的,编写一个命令对象的代码量远多于函数。在大多数情况下,能够使用函数尽量使用,只有在普通函数无法满足需求,且不得不使用命令对象,且需要将函数的发起方和接收方解耦时,我才会选择它。
下面是一个命令对象取代函数的简单例子:
csharp
public float GetScore(Student stu, ScoreRule rule)
{
var result = 0f;
//复杂的计算
return result;
}
csharp
public float GetScore(Student stu, ScoreRule rule)
{
return new StudentScore(stu, rule).Get();
}
public class StudentScore
{
private Student stu;
private ScoreRule rule;
public StudentScore(Student stu, ScoreRule rule)
{
this.stu = stu;
this.rule = rule;
}
public float Get()
{
//复杂的计算
}
}
以多态取代分支表达式
多态是面向对象的基本特征之一,但这并不代表所有的分支表达式都应该用多态替代。如果你有如下两种情况,原作者推荐使用多态。
- 每个条件下的逻辑复杂。
- 在多个地方反复调用此分支表达式。
下面是一个非常典型的示例。当我们需要多个地方、多次调用 GetCarColor 方法时,可以将此拆分成不同的 Car 类,每个类都可以实现自己的 GetCarColor 方法
typescript
public Color GetCarColor(CarType type)
{
switch(type)
{
case CarType.XiaoMi:
{
return Color.Orange;
}
case CarType.BMW:
{
return Color.White;
}
case CarType.AuDi:
{
return Color.Black;
}
}
}
kotlin
public interface Icolor
{
Color GetColor(){}
}
public class XiaoMiCar : IColor
{
public Color GetColor()
{
return Color.Orange;
}
}
public class BMWCar : IColor
{
public Color GetColor()
{
return Color.White;
}
}
public class AuDiCar : IColor
{
public Color GetColor()
{
returnColor.Black;
}
}
拆分循环
拆分循环的意义在于,将两段不同的逻辑在形式上拆分开。原作者认为,一个循环只为了做一件事,这样后来的读者能够很快速的、清晰的了解到这段循环的作用。
而且原作者还提到了,如果觉得拆分循环会对性能上造成压力,那么就在循环被拆分后,再去做性能优化,这是两件事,不能一起做。
下面是一个简单例子:
ini
public void UpdateStudentInfos()
{
for (int i = 0; i < studentList.Count; i++)
{
UpdatePersonInfo(studentList[i].personInfo);
UpdateScoreInfo(studentList[i].scoreInfo);
}
}
ini
public void UpdateStudentInfos()
{
for (int i = 0; i < studentList.Count; i++)
{
UpdatePersonInfo(studentList[i].personInfo);
}
for (int i = 0; i < studentList.Count; i++)
{
UpdateScoreInfo(studentList[i].scoreInfo);
}
}