如何增强C.R.A.P。类似开关功能的索引?

How to enhance C.R.A.P. index for a switch-like function?

我有一个非常典型的 类开关 函数,它 returns 对给定输入值(在本例中为体重指数)进行分类。 (我正在使用此功能,但它可以是任何其他具有相同性质的功能)

现在,大致是这样:

// ...

const TYPE_SEVERE_THINNESS = -3;
const TYPE_MODERATE_THINNESS = -2;
const TYPE_MILD_THINNESS = -1;
const TYPE_REGULAR = 0;
const TYPE_OVERWEIGHT = 1;
const TYPE_PRE_OBESE = 2;
const TYPE_OBESE_GRADE_I = 3;
const TYPE_OBESE_GRADE_II = 4;
const TYPE_OBESE_GRADE_III = 5;

// ...

public static function classification(float $bmi) : int
{
    if ($bmi <= 16.00) {
        return self::TYPE_SEVERE_THINNESS;
    }

    if ($bmi <= 16.99) {
        return self::TYPE_MODERATE_THINNESS;
    }

    if ($bmi <= 18.49) {
        return self::TYPE_MILD_THINNESS;
    }

    if ($bmi <= 24.99) {
        return self::TYPE_REGULAR;
    }

    if ($bmi <= 27.49) {
        return self::TYPE_OVERWEIGHT;
    }

    if ($bmi <= 29.99) {
        return self::TYPE_PRE_OBESE;
    }

    if ($bmi <= 34.99) {
        return self::TYPE_OBESE_GRADE_I;
    }

    if ($bmi <= 39.99) {
        return self::TYPE_OBESE_GRADE_II;
    }

    if ($bmi >= 40) {
        return self::TYPE_OBESE_GRADE_III;
    }
}

我要进行重构,我正在考虑对此功能进行所有可能的增强,特别是为了降低 C.R.A.P。索引(Change Risk Anti-Patterns)此时正在返回值 110.00

当然,可能还有很多可能的改进。欢迎提出建议。

但我的问题具体是关于降低循环复杂度,

a) 是否有任何其他方法来构建此代码以便 C.R.A.P。指数走低? b) 为了正确测试此功能,我应该生成一个断言每个案例的测试,还是执行多个测试来解决每个可能的案例? (我现在的答案可能是 "It's up to you",但也许存在一种更好的方法来降低圈复杂度,从而减少仍然覆盖所有或大多数可能场景的测试。)

如果我必须匹配相等的值,我只会使用哈希图(键值数组),但由于我正在评估范围,所以方法可能会有所不同。

更新: 在使用每个场景的示例构建测试用例后,CRAP 指数下降到 10.01。不过,我相信还有另一种方法可以执行值查找。

/**
 * Test it returns a valid WHO classification for BMI type
 *
 * @return void
 */
public function test_it_returns_a_valid_who_classification_for_bmi_type()
{
    // Sample bmi => expected type
    // Key must be a string later converted to float
    $testMatrix = [
        "15" => BMILevel::TYPE_SEVERE_THINNESS,
        "16.5" => BMILevel::TYPE_MODERATE_THINNESS,
        "18" => BMILevel::TYPE_MILD_THINNESS,
        "24" => BMILevel::TYPE_REGULAR,
        "27" => BMILevel::TYPE_OVERWEIGHT,
        "29" => BMILevel::TYPE_PRE_OBESE,
        "34" => BMILevel::TYPE_OBESE_GRADE_I,
        "39" => BMILevel::TYPE_OBESE_GRADE_II,
        "41" => BMILevel::TYPE_OBESE_GRADE_III,
    ];

    foreach ($testMatrix as $bmi => $categoryCheck) {
        $type = BMILevel::classification(floatval($bmi));

        $this->assertEquals($type, $categoryCheck);
    }
}

好的,我设法得到了一个相当合理的C.R.A.P。在保持测试绿色的同时进行一些重构后的索引。

我把这个函数变成了一个有上限的查找(自下而上)。我需要为超出范围的值添加一个额外的案例并涵盖该案例。

代码:

public static function classification(float $bmi) : int
{
    $classifications = [
        ['limit' => 16.0 , 'type' => self::TYPE_SEVERE_THINNESS],
        ['limit' => 16.99, 'type' => self::TYPE_MODERATE_THINNESS],
        ['limit' => 18.49, 'type' => self::TYPE_MILD_THINNESS],
        ['limit' => 24.99, 'type' => self::TYPE_REGULAR],
        ['limit' => 27.49, 'type' => self::TYPE_OVERWEIGHT],
        ['limit' => 29.99, 'type' => self::TYPE_PRE_OBESE],
        ['limit' => 34.99, 'type' => self::TYPE_OBESE_GRADE_I],
        ['limit' => 39.99, 'type' => self::TYPE_OBESE_GRADE_II],
        ['limit' => 60   , 'type' => self::TYPE_OBESE_GRADE_III],
    ];

    foreach ($classifications as $classification) {
        if ($bmi <= $classification['limit']) {
            return $classification['type'];
        }
    }

    return self::TYPE_OBESE_GRADE_III;
}

测试:

/**
 * Test it returns a valid WHO classification for BMI type
 *
 * @return void
 */
public function test_it_returns_a_valid_who_classification_for_bmi_type()
{
    // Sample bmi => expected type
    // Key must be a string later converted to float
    $testMatrix = [
        "15" => BMILevel::TYPE_SEVERE_THINNESS,
        "16.5" => BMILevel::TYPE_MODERATE_THINNESS,
        "18" => BMILevel::TYPE_MILD_THINNESS,
        "24" => BMILevel::TYPE_REGULAR,
        "27" => BMILevel::TYPE_OVERWEIGHT,
        "29" => BMILevel::TYPE_PRE_OBESE,
        "34" => BMILevel::TYPE_OBESE_GRADE_I,
        "39" => BMILevel::TYPE_OBESE_GRADE_II,
        "41" => BMILevel::TYPE_OBESE_GRADE_III,
        "100" => BMILevel::TYPE_OBESE_GRADE_III, // After upper bound limit
    ];

    foreach ($testMatrix as $bmi => $categoryCheck) {
        $type = BMILevel::classification(floatval($bmi));

        $this->assertEquals($type, $categoryCheck);
    }
}

仍然欢迎有关如何增强功能的提示。

测试通常允许您反映现有的(可能是过时的)实现:

const TYPE_SEVERE_THINNESS = -3;
const TYPE_MODERATE_THINNESS = -2;
const TYPE_MILD_THINNESS = -1;
const TYPE_REGULAR = 0;
const TYPE_OVERWEIGHT = 1;
const TYPE_PRE_OBESE = 2;
const TYPE_OBESE_GRADE_I = 3;
const TYPE_OBESE_GRADE_II = 4;
const TYPE_OBESE_GRADE_III = 5;

所以你在这里看到的是一个有序列表(从上到下):

const TYPES = [-3, -2, -1, 0, 1, 2, 3, 4, 5];

或者也许:

const TYPES = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const TYPE_REFERENCE = 4;

和相应的值(不是键,不是转换问题):

const VALUES = [15, 16.5, 18, 24, 27, 29, 34, 39, 41];

您可能想要提供标签:

const LABELS = ["Severe Thinness", "Moderate Thinness", "Mild Thinness", 
                "Regular", "Overweight", "Pre-Obese", "Obese Grade I",
                "Obese Grade II", "Obese Grade III"];

所以很容易想象这大部分不属于代码,而是用来配置代码的数据。然后单元测试可以针对不同的数据集进行测试,这不仅会增加被测系统的稳定性,还会 测试哪些扩展(更改)很容易应用现有代码。

在测试中使用数据提供者通常可以看出代码库本身很可能应该有一些 数据提供者 而不是那么多硬编码。