PHP代码质量度量:圈复杂度(Cyclomatic Complexity)与CRAP指标的分析与优化
大家好,今天我们来深入探讨PHP代码质量度量中的两个重要指标:圈复杂度(Cyclomatic Complexity,CC)和CRAP(Change Risk Anti-Patterns)指标。理解并优化这两个指标,能显著提升代码的可读性、可维护性和可测试性,最终降低开发和维护成本。
1. 圈复杂度:代码复杂度的量化
1.1 什么是圈复杂度?
圈复杂度是一种衡量程序控制流复杂度的指标。它通过计算程序中线性无关的路径数量来评估代码的复杂度。简单来说,圈复杂度越高,代码的分支越多,理解和测试的难度就越大。
1.2 圈复杂度的计算方法
圈复杂度可以使用以下公式计算:
CC = E - N + 2P
其中:
- CC:圈复杂度
- E:图中边的数量(代表控制流的连接)
- N:图中节点的数量(代表语句块)
- P:连接组件的数量(通常为1,除非有多个独立的函数或模块)
实际上,在代码中,圈复杂度也可以通过统计以下关键词的数量来估算:
ifelse ifelsewhileforforeachcasecatch&&(逻辑与)||(逻辑或)?(三元运算符)
圈复杂度大致等于以上关键词的数量加1。
1.3 圈复杂度的解读
一般来说,圈复杂度的数值代表了代码需要测试的最小路径数量,以便覆盖所有可能的执行流程。
| 圈复杂度 | 代码复杂度 | 建议 |
|---|---|---|
| 1-10 | 简单易懂 | 保持现状 |
| 11-20 | 稍微复杂 | 考虑重构,分解为更小的函数或模块 |
| 21-50 | 复杂 | 必须重构,分解逻辑,提高可读性和可测试性 |
| >50 | 非常复杂 | 立即重构,否则代码难以理解和维护,bug风险极高 |
1.4 PHP代码示例与圈复杂度计算
考虑以下PHP函数:
function processOrder(int $orderId, string $paymentMethod, bool $isRecurring): string
{
$order = getOrderById($orderId);
if (!$order) {
return "Order not found.";
}
if ($order['status'] === 'pending') {
if ($paymentMethod === 'credit_card') {
if ($isRecurring) {
$result = chargeCreditCardRecurring($order['total']);
} else {
$result = chargeCreditCard($order['total']);
}
} elseif ($paymentMethod === 'paypal') {
$result = processPaypalPayment($order['total']);
} else {
return "Invalid payment method.";
}
if ($result === 'success') {
updateOrderStatus($orderId, 'processed');
return "Order processed successfully.";
} else {
updateOrderStatus($orderId, 'failed');
return "Payment failed.";
}
} else {
return "Order already processed.";
}
}
这个函数的圈复杂度较高。我们来分析一下:
if出现 4 次else出现 3 次- 因此,圈复杂度大约为 4 + 3 + 1 = 8。
虽然小于10,但是函数内部嵌套层级较深,可读性较差,需要进行重构。
1.5 圈复杂度的优化方法
- 提取函数:将复杂的逻辑分解为更小的、功能单一的函数。每个函数只负责完成一个明确的任务。
- 使用设计模式:策略模式、命令模式等可以有效地减少条件判断语句,降低复杂度。
- 简化条件判断:使用卫语句(Guard Clauses)提前排除特殊情况,减少嵌套层级。
- 使用多态:将不同的行为抽象成接口或抽象类,通过多态来实现不同的逻辑。
- 使用查找表(Lookup Table):将条件判断转化为查找表,避免大量的
if-else语句。
针对上面的例子,我们可以进行如下重构:
function processOrder(int $orderId, string $paymentMethod, bool $isRecurring): string
{
$order = getOrderById($orderId);
if (!$order) {
return "Order not found.";
}
if ($order['status'] !== 'pending') {
return "Order already processed.";
}
try {
$paymentResult = processPayment($order, $paymentMethod, $isRecurring);
updateOrderStatus($orderId, 'processed');
return "Order processed successfully.";
} catch (Exception $e) {
updateOrderStatus($orderId, 'failed');
return "Payment failed.";
}
}
function processPayment(array $order, string $paymentMethod, bool $isRecurring): string
{
switch ($paymentMethod) {
case 'credit_card':
return processCreditCardPayment($order, $isRecurring);
case 'paypal':
return processPaypalPayment($order['total']);
default:
throw new Exception("Invalid payment method.");
}
}
function processCreditCardPayment(array $order, bool $isRecurring): string
{
if ($isRecurring) {
return chargeCreditCardRecurring($order['total']);
} else {
return chargeCreditCard($order['total']);
}
}
重构后的代码,processOrder 函数的复杂度大大降低,逻辑更加清晰。processPayment 函数利用 switch 语句,将不同的支付方式进行分离。processCreditCardPayment 函数处理信用卡支付的逻辑。 通过这种方式,我们将原先复杂的函数分解为多个职责单一的函数,降低了圈复杂度,提高了代码的可读性和可维护性。
2. CRAP指标:综合评估代码质量
2.1 什么是CRAP指标?
CRAP指标,全称 Change Risk Anti-Patterns,是一个用于评估代码质量的综合指标。它结合了圈复杂度(Cyclomatic Complexity)和代码覆盖率(Code Coverage),旨在衡量代码修改的风险。
CRAP指标的核心思想是:复杂的、未经充分测试的代码,修改的风险最高。
2.2 CRAP指标的计算方法
CRAP指标的计算公式如下:
CRAP = CC^2 * (1 - coverage)^3 + CC
其中:
- CRAP:CRAP指标
- CC:圈复杂度
- coverage:代码覆盖率(0到1之间的小数,例如 80% 的覆盖率表示为 0.8)
2.3 CRAP指标的解读
CRAP指标的值越高,表示代码质量越差,修改的风险越高。一般来说,CRAP指标的值可以这样解读:
| CRAP指标 | 代码质量 | 建议 |
|---|---|---|
| 0-6 | 优秀 | 保持现状 |
| 7-12 | 良好 | 可以接受,但仍有改进空间 |
| 13-20 | 一般 | 需要关注,考虑重构和增加测试 |
| 21-30 | 差 | 必须重构和增加测试,否则代码难以维护,bug风险极高 |
| >30 | 非常差 | 立即重构和增加测试,否则代码很可能导致严重问题 |
2.4 PHP代码示例与CRAP指标计算
假设我们有一个函数,其圈复杂度为 8,代码覆盖率为 70% (0.7),那么它的CRAP指标为:
CRAP = 8^2 * (1 - 0.7)^3 + 8
CRAP = 64 * (0.3)^3 + 8
CRAP = 64 * 0.027 + 8
CRAP = 1.728 + 8
CRAP = 9.728
根据上面的解读,CRAP值为9.728表示代码质量良好,但仍有改进空间。我们可以考虑进一步降低圈复杂度,或者提高代码覆盖率,以降低修改的风险。
现在,假设另一个函数,其圈复杂度为 20,代码覆盖率为 50% (0.5),那么它的CRAP指标为:
CRAP = 20^2 * (1 - 0.5)^3 + 20
CRAP = 400 * (0.5)^3 + 20
CRAP = 400 * 0.125 + 20
CRAP = 50 + 20
CRAP = 70
CRAP值为70,说明代码质量非常差,修改的风险极高,必须立即进行重构和增加测试。
2.5 CRAP指标的优化方法
优化CRAP指标,实际上就是降低圈复杂度,提高代码覆盖率。
- 降低圈复杂度:参考 1.5 节的优化方法。
- 提高代码覆盖率:
- 编写单元测试:针对每个函数、每个类编写单元测试,覆盖所有可能的输入和输出。
- 使用测试驱动开发(TDD):先编写测试用例,再编写代码,确保代码从一开始就具有良好的可测试性。
- 覆盖所有分支:确保测试用例覆盖代码中的所有
if、else、switch等分支。 - 进行代码审查:通过代码审查发现潜在的缺陷和未覆盖的测试用例。
3. 工具支持
幸运的是,有一些工具可以帮助我们自动计算圈复杂度和CRAP指标,并生成报告。
- PHP_CodeSniffer: 这是一个PHP代码风格检测工具,可以配合自定义规则来检测圈复杂度。
- PHP Mess Detector: 这是一个专门用于检测PHP代码中潜在问题的工具,包括过高的圈复杂度。
- PHPCoverage: 这是一个用于收集PHP代码覆盖率信息的工具。
- SonarQube: 这是一个代码质量管理平台,可以集成多种静态分析工具,提供全面的代码质量报告,包括圈复杂度、代码覆盖率和CRAP指标。
- Psalm/Phan/Static Analysis Tools: 这些静态分析工具可以帮助你发现潜在的错误,并提供代码复杂度的信息。
4. 实践案例
接下来,我们通过一个实际的例子来演示如何使用工具来分析和优化代码。
假设我们有以下代码:
class DiscountCalculator
{
public function calculateDiscount(string $customerType, float $orderTotal): float
{
$discount = 0;
if ($customerType === 'new') {
if ($orderTotal > 100) {
$discount = 0.1; // 10% discount
} else {
$discount = 0.05; // 5% discount
}
} elseif ($customerType === 'loyal') {
if ($orderTotal > 50) {
$discount = 0.15; // 15% discount
} else {
$discount = 0.1; // 10% discount
}
if (date('w') == 5) { // Friday
$discount += 0.05; // Additional 5% discount on Fridays
}
} elseif ($customerType === 'vip') {
$discount = 0.2; // 20% discount
} else {
$discount = 0; // No discount
}
return $discount;
}
}
这个 calculateDiscount 函数的圈复杂度较高,逻辑也比较复杂。我们可以使用 PHP Mess Detector 来检测它的圈复杂度。
首先,安装 PHP Mess Detector:
composer require phpmd/phpmd
然后,创建一个规则集文件 ruleset.xml:
<?xml version="1.0"?>
<ruleset name="My Rules"
xmlns="http://pmd.sf.net/ruleset/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd"
xsi:noNamespaceSchemaLocation="http://pmd.sf.net/ruleset_xml_schema.xsd">
<description>My custom ruleset</description>
<rule ref="rulesets/codesize.xml">
<exclude name="TooManyMethods"/>
<exclude name="TooManyFields"/>
<exclude name="ExcessiveClassLength"/>
<exclude name="ExcessiveMethodLength"/>
<exclude name="ExcessiveParameterList"/>
<exclude name="ExcessivePublicCount"/>
</rule>
<rule ref="rulesets/codesize.xml/CyclomaticComplexity">
<properties>
<property name="reportLevel" value="7"/>
</properties>
</rule>
</ruleset>
这个规则集指定了圈复杂度的阈值为 7。
最后,运行 PHP Mess Detector:
./vendor/bin/phpmd src/ DiscountCalculator.php xml ruleset.xml
运行结果可能会显示 DiscountCalculator::calculateDiscount 函数的圈复杂度超过了阈值。
接下来,我们可以重构代码,降低圈复杂度:
class DiscountCalculator
{
private const NEW_CUSTOMER_THRESHOLD = 100;
private const LOYAL_CUSTOMER_THRESHOLD = 50;
private const FRIDAY = 5;
public function calculateDiscount(string $customerType, float $orderTotal): float
{
$discountStrategy = $this->getDiscountStrategy($customerType);
return $discountStrategy->calculateDiscount($orderTotal);
}
private function getDiscountStrategy(string $customerType): DiscountStrategyInterface
{
switch ($customerType) {
case 'new':
return new NewCustomerDiscountStrategy();
case 'loyal':
return new LoyalCustomerDiscountStrategy();
case 'vip':
return new VipCustomerDiscountStrategy();
default:
return new DefaultDiscountStrategy();
}
}
}
interface DiscountStrategyInterface
{
public function calculateDiscount(float $orderTotal): float;
}
class NewCustomerDiscountStrategy implements DiscountStrategyInterface
{
public function calculateDiscount(float $orderTotal): float
{
if ($orderTotal > DiscountCalculator::NEW_CUSTOMER_THRESHOLD) {
return 0.1;
} else {
return 0.05;
}
}
}
class LoyalCustomerDiscountStrategy implements DiscountStrategyInterface
{
public function calculateDiscount(float $orderTotal): float
{
$discount = ($orderTotal > DiscountCalculator::LOYAL_CUSTOMER_THRESHOLD) ? 0.15 : 0.1;
if (date('w') == DiscountCalculator::FRIDAY) {
$discount += 0.05;
}
return $discount;
}
}
class VipCustomerDiscountStrategy implements DiscountStrategyInterface
{
public function calculateDiscount(float $orderTotal): float
{
return 0.2;
}
}
class DefaultDiscountStrategy implements DiscountStrategyInterface
{
public function calculateDiscount(float $orderTotal): float
{
return 0;
}
}
在这个重构后的代码中,我们使用了策略模式,将不同的客户类型对应的折扣计算逻辑分离到不同的类中。DiscountCalculator 类只负责选择合适的折扣策略,而具体的折扣计算由各个策略类来完成。
通过这种方式,我们大大降低了 DiscountCalculator::calculateDiscount 函数的圈复杂度,提高了代码的可读性和可维护性。
接下来,我们需要编写单元测试,提高代码覆盖率。可以使用 PHPUnit 来编写单元测试。
首先,安装 PHPUnit:
composer require --dev phpunit/phpunit
然后,创建一个测试类 DiscountCalculatorTest.php:
use PHPUnitFrameworkTestCase;
class DiscountCalculatorTest extends TestCase
{
public function testCalculateDiscountForNewCustomerWithOrderTotalGreaterThan100()
{
$calculator = new DiscountCalculator();
$discount = $calculator->calculateDiscount('new', 150);
$this->assertEquals(0.1, $discount);
}
public function testCalculateDiscountForNewCustomerWithOrderTotalLessThanOrEqualTo100()
{
$calculator = new DiscountCalculator();
$discount = $calculator->calculateDiscount('new', 50);
$this->assertEquals(0.05, $discount);
}
// Add more test cases for other customer types and scenarios
}
编写更多的测试用例,确保覆盖所有可能的输入和输出。
最后,运行 PHPUnit:
./vendor/bin/phpunit
通过编写单元测试,我们可以提高代码覆盖率,降低 CRAP 指标,从而降低代码修改的风险。
5. 注意事项
- 不要盲目追求低圈复杂度:过度简化代码可能会导致可读性下降。需要在复杂度和可读性之间找到平衡。
- CRAP 指标只是一个参考:不要过分依赖 CRAP 指标。需要结合实际情况,综合评估代码质量。
- 持续改进:代码质量是一个持续改进的过程。定期进行代码审查,重构代码,编写测试用例,不断提升代码质量。
- 关注代码可读性:可读性好的代码更容易理解和维护,降低了出错的风险。
总结
通过今天的内容,我们了解了圈复杂度与CRAP指标的计算方式与优化方法。这两个指标可以帮助我们量化代码的复杂度,并指导我们进行代码重构和测试。最终,我们应该结合实际情况,综合评估代码质量,并持续改进,编写出高质量、可维护的PHP代码。