"每一个问题都可以再分解为多个更小的问题."
2.1 指导原则
将每一个代码单元的 branch point 的个数限制为最多 4 个. 如果做到这个呢? 这个就需要将复杂的代码单元进行分解, 分解为更为简单的代码单元. 限制分支点的个数也意味着更容易进行单元测试.
实际上这样的做法就是在限制代码的复杂程度.
因为如果一个代码单元中的代码可能�路径更多, 则意味着它更加复杂.
上面说的 branch point(分支结点)指的是: 根据某个条件的不同可能会有不同的执行方向的代码结点. 比如 if 或 switch 等.
计算分支结点个数的方法是: 利用 branch coverage 来计算. 即最少的, 且覆盖到代码单元中所有路径的执行方式的个数.
这里又有所有的执行路径和所有的分支路径的区分. 即 cyclomatic complexity, 或者叫做 McCabe complexity. 它们的值为分支路径的个数加 1 来计算.
比如想要把代码单元的分支结点个数限制为 4 个, 则 swiftlint 中的 cyclomatic_complexity(环路复杂度) 规则的数目可以设置为 5, 如下所示:
# 代码单元结点分支个数
cyclomatic_complexity:
warning: 5
error: 6
加一的目的是在实际使用的时候更简单. 比如一个没有分支的代码单元的环路复杂性是 1, 因为它只有一条执行路径.
2.2 动机
遵守这个原则的动机就是要减少代码的复杂度:
- 简单的代码单元显然更加易读, 更易于�修改.
- 简单的代码单元更加容易测试.
2.3 如何在实践中运用
在 Java 中有如下的语句�被认为是分支结点:
- if
- case
- ?
- && ||
- while
- for
- catch
一般来说, 一个复杂的代码单元都是由�若干个代码块结合在一起的, 这样的代码的环路复杂度就是所有的小块的和. 而另外一些是多重的 if-then-else 等, 或者是多分支的 switch. 针对这些复杂代码的解决方法各不相同, 下面就来看看.
2.3.1 处理�多路条件判断
针对复杂条件或条件嵌套的一个解决方式是在外部建立和条件对应的条件对象集合(比如一个字典), 根据条件直接就在对象集合中取得对应的对象, 从而完成对应功能.
更高级的办法是运用重构规则: Replace conditional with Polymorphism, 这个规则的意思是利用多态行为来代替条件判断.�(实际是�前面那个方法的面向对象的�实现)
比如之前的代码是这样的:
enum Flags {
case one
case two
case three
case four
case five
case six
case seven
}
public func getFlagWith(num: Int) -> Flags {�// 环路复杂度是8
switch num {
case 1:
.one
case 2:
.two
case 3:
.three
case 4:
.four
case 5:
.five
case 6:
.six
case 7:
.seven
default:
.one
}
}
多态代替条件判断的重构方法需要首先建立一个� Flag 接口:
public protocol Flag {
func getFlag() -> Flags
}
然后建立�和条件对应的多个类, 实现这个接口, 从而可以返回对应的对象, 从而完成不同功能:
public class One: Flag {
public func getFlag() -> Flags {
return .one
}
}
public class Two: Flag {
public func getFlag() -> Flags {
return .two
}
}
//...
则在使用的时候就可以这样了:
public func getFlagWith(�flagObj: Flag) -> Flags {�
return fagObj.getFlag()
}
2.3.2 处理嵌套条件判断
对于嵌套条件判断, 可以先明确里面的一些终止值, 然后利用 guard return
来去掉一些条件判断.
实际上这个方法应用了一个重构模式: Replace Nested Conditional with Guard Clauses 模式.
但如果只应用这个模式, 实际分支结点个数是不会变化的. 它只是减少嵌套层次的一个办法.
另外一个办法就是将条件再次分解到另外的方法中.
2.4 反对声音一览
针对这个原则, 仍然有不少人持反对态度, 下面就来看看.
2.4.1 高环路复杂度无法避免
反对者说: 因为我们的业务就是这么复杂, 没办法减小环路复杂度.
2.4.2 分解的方式没有减少环路复杂度的总量
的确是这样, 但是更加可读, 易于理解.
附录
附上再次修改的 swiftlint 配置文件:
disabled_rules: # rule identifiers to exclude from running
# - colon
# - comma
# - control_statement
# - for_where
# - force_unwrapping
opt_in_rules: # some rules are only opt-in
- empty_count
- missing_docs
- closure_end_indentation
- closure_spacing
- force_unwrapping
- implicitly_unwrapped_optional
- operator_usage_whitespace
- redundant_nil_coalescing
included: # 包含目录`--path` is ignored if present.
- ./
excluded: # 排除目录, 这个是在包含目录之前进行排除的
- Carthage
- Pods
force_cast: warning # implicitly
force_try:
severity: warning # explicitly
# 行宽
line_length: 120
# 类型体的长度
type_body_length:
- 300 # warning
- 400 # error
# 方法或函数体的长度
function_body_length: 30
# 方法或函数的参数个数限制
function_parameter_count: 6
# 文件的长度限制
file_length:
warning: 500
error: 1200
# 代码单元结点分支个数
cyclomatic_complexity:
warning: 5
error: 10
# 类型名称的长度规定
type_name:
min_length: 2 # only warning
max_length: # warning and error
warning: 40
error: 50
excluded: iPhone # excluded via string
# 标识符长度规定
identifier_name:
min_length: # only min_length
error: 2 # only error
excluded: # excluded via string array
- id
- vm
- URL
- GlobalAPIKey
reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit, html, emoji)