背景
常听同事们讲某一段代码复杂度高,不好理解。那么什么样的代码算是复杂度高呢?
python
if A:
for B in range(10):
if B % 2 == 1:
print B
else:
for C in range(10):
if C % 2 == 1:
print C
else if D:
print D
else:
if E:
pass
else:
for F in range(10):
print F
以上代码的直观上看复杂度就挺高。解决问题的前提是先量化问题,圈复杂度就是对代码复杂度的量化。这段代码的圈复杂度为8。一般来说,圈复杂度大于7的函数就算是复杂函数了。
概念
圈复杂度(Cyclomatic complexity),是一种软件度量。用来表示程序的复杂度。它可以用来衡量一个模块判定结构的复杂程度,数量上表现为独立现行路径条数,也可理解为覆盖所有的可能情况最少使用的测试用例数。
计算方法
方法一
V(G) = E - N + 2
其中,e表示控制流图中边的数量,n表示控制流图中节点的数量。下面是典型的控制流程,如if-else,While,until和正常的流程顺序:
方法二
V (G) = P + 1
P表示控制节点的数量。 其中P为判定节点数,判定节点举例:
- if语句
- while语句
- for语句
- case语句
- catch语句
- and和or布尔操作
- ?:三元运算符
举例
css
A = 10
IF B > C THEN
A = B
ELSE
A = C
ENDIF
Print A
Print B
Print C
以上代码的控制流为
- 根据方法一:V(G) = 7-7+2 = 2
- 根据方法二:V(G) = 1+1 = 2
优化方法
重新组织你的函数
技巧1 提炼函数
有一段代码可以被组织在一起并独立出来:
scss
void Example(int val)
{
if( val > MAX_VAL)
{
val = MAX_VAL;
}
for( int i = 0; i < val; i++)
{
doSomething(i);
}
}
将这段代码放进一个独立函数中,并让函数名称解释该函数的用途:
scss
int getValidVal(int val)
{
if( val > MAX_VAL)
{
return MAX_VAL;
}
return val;
}
void doSomethings(int val)
{
for( int i = 0; i < val; i++)
{
doSomething(i);
}
}
void Example(int val)
{
doSomethings(getValidVal(val));
}
最后还要重新审视函数内容是否在统一层次上。
技巧2 替换算法
把某个算法替换为另一个更清晰的算法:
c
string foundPerson(const vector<string>& peoples){
for (auto& people : peoples)
{
if (people == "Don"){
return "Don";
}
if (people == "John"){
return "John";
}
if (people == "Kent"){
return "Kent";
}
}
return "";
}
将函数实现替换为另一个算法:
c
string foundPerson(const vector<string>& people){
std::map<string,string>candidates{
{ "Don", "Don"},
{ "John", "John"},
{ "Kent", "Kent"},
};
for (auto& people : peoples)
{
auto& it = candidates.find(people);
if(it != candidates.end())
return it->second;
}
}
所谓的表驱动。
简化条件表达式
技巧3 逆向表达
在代码中可能存在条件表达如下:
kotlin
if ((condition1() && condition2()) || !condition1())
{
return true;
}
else
{
return false;
}
应用逆向表达调换表达顺序后效果如下:
kotlin
if(condition1() && !condition2())
{
return false;
}
return true;
技巧4 分解条件
在代码中存在复杂的条件表达:
ini
if(date.before (SUMMER_START) || date.after(SUMMER_END))
charge = quantity * _winterRate + _winterServiceCharge;
else
charge = quantity * _summerRate;
从if、then、else三个段落中分别提炼出独立函数:
ini
if(notSummer(date))
charge = winterCharge(quantity);
else
charge = summerCharge (quantity);
技巧5 合并条件
一系列条件判断,都得到相同结果:
csharp
double disabilityAmount()
{
if (_seniority < 2) return 0;
if (_monthsDisabled > 12) return 0;
if (_isPartTime) return 0;
// compute the disability amount
......
将这些判断合并为一个条件式,并将这个条件式提炼成为一个独立函数:
csharp
double disabilityAmount()
{
if (isNotEligableForDisability()) return 0;
// compute the disability amount
......
技巧6 移除控制标记
在代码逻辑中,有时候会使用bool类型作为逻辑控制标记:
ini
void checkSecurity(vector<string>& peoples) {
bool found = false;
for (auto& people : peoples)
{
if (! found) {
if (people == "Don"){
sendAlert();
found = true;
}
if (people == "John"){
sendAlert();
found = true;
}
}
}
}
使用break和return取代控制标记:
c
void checkSecurity(vector<string>& peoples) {
for (auto& people : peoples)
{
if (people == "Don" || people == "John")
{
sendAlert();
break;
}
}
}
技巧7 以多态取代条件式
条件式根据对象类型的不同而选择不同的行为:
csharp
double getSpeed()
{
switch (_type) {
case EUROPEAN:
return getBaseSpeed();
case AFRICAN:
return getBaseSpeed() - getLoadFactor() *_numberOfCoconuts;
case NORWEGIAN_BLUE:
return (_isNailed) ? 0 : getBaseSpeed(_voltage);
}
throw new RuntimeException ("Should be unreachable");
}
将整个条件式的每个分支放进一个子类的重载方法中,然后将原始函数声明为抽象方法:
csharp
class Bird
{
public:
virtual double getSpeed() = 0;
protected:
double getBaseSpeed();
}
class EuropeanBird
{
public:
double getSpeed()
{
return getBaseSpeed();
}
}
class AfricanBird
{
public:
double getSpeed()
{
return getBaseSpeed() - getLoadFactor() *_numberOfCoconuts;
}
private:
double getLoadFactor();
double _numberOfCoconuts;
}
class NorwegianBlueBird
{
public:
double getSpeed()
{
return (_isNailed) ? 0 : getBaseSpeed(_voltage);
};
private:
bool _isNailed;
}
简化函数调用
技巧8 读写分离
某个函数既返回对象状态值,又修改对象状态:
arduino
class Customer
{
int getTotalOutstandingAndSetReadyForSummaries(int number);
}
建立两个不同的函数,其中一个负责查询,另一个负责修改:
csharp
class Customer
{
int getTotalOutstanding();
void SetReadyForSummaries(int number);
}
技巧9 参数化方法
若干函数做了类似的工作,但在函数本体中却 包含了不同的值:
scss
Dollars baseCharge()
{
double result = Math.min(lastUsage(),100) * 0.03;
if (lastUsage() > 100)
{
result += (Math.min (lastUsage(),200) - 100) * 0.05;
}
if (lastUsage() > 200)
{
result += (lastUsage() - 200) * 0.07;
}
return new Dollars (result);
}
建立单一函数,以参数表达那些不同的值:
sql
Dollars baseCharge()
{
double result = usageInRange(0, 100) * 0.03;
result += usageInRange (100,200) * 0.05;
result += usageInRange (200, Integer.MAX_VALUE) * 0.07;
return new Dollars (result);
}
int usageInRange(int start, int end)
{
if (lastUsage() > start)
return Math.min(lastUsage(),end) -start;
return 0;
}
技巧10 以明确函数取代参数
函数实现完全取决于参数值而采取不同反应:
ini
void setValue (string name, int value)
{
if (name == "height")
_height = value;
else if (name == "width")
_width = value;
Assert.shouldNeverReachHere();
}
针对该参数的每一个可能值,建立一个独立函数:
arduino
void setHeight(int arg)
{
_height = arg;
}
void setWidth (int arg)
{
_width = arg;
}