这是我有一个函数:
public int GetIntSetting(SET setting, int nullState)
{
// Get the setting value or nullState if it was not set
var str = GetStringSetting(setting, nullState.ToString());
// Check if it's an integer
var isInt = int.TryParse(GetStringSetting(setting, nullState.ToString()), out int parsed);
// If integer then return else update settings and return nullState
if (isInt )
{
return parsed;
}
else
{
UpdateIntSetting(setting, nullState);
return nullState;
}
}
我想做到这一点,这样该功能就不会有任何惊喜。 希望就如何检查输入以及我应该使用此类方法考虑的任何其他内容提供任何建议?
这不是一个C#
问题。 这是关于函数是什么以及它应该如何操作的基础知识。 从技术上讲,"函数"应该将所有输入参数视为不可变,并返回一些值。 "方法"可能会有副作用。 在这里,您使用的是C#
"方法",但您可以将其视为一个函数,这往往会使代码更清晰。
编写函数时要记住的事项:
- 无副作用
- 将其简化为语义上有意义的最简单的指令集
- 尽可能减少分支和嵌套循环(这些通常是可以进行进一步重构以从当前函数中提取另一个函数的指标) 用
- 接口替换具体的依赖项类(这远远超出了我今天发布的范围,但它有助于编写测试并可以减轻维护期间的麻烦)。
方法/功能应该做什么? 尝试将其简化为母语中的几句话,然后尝试将其编写为几行代码。 看起来您的方法正在执行以下操作..."如果setting
未设置为整数值,则将其设置为nullValue
,并返回其值。 我希望最终方法不超过几行(我在这里故意说方法,因为有一个副作用 - 如果"未设置",则更改setting
的值)。
你最终可能会得到一种方法,但我会强调函数的重要性。 通过编写纯函数(无副作用)并降低单个函数的复杂性,可以显著减少所需的单元测试用例覆盖率。 话虽如此,这也是您问题的核心,您仍然希望尝试识别您的方法/函数的所有边缘情况。 例如,假设我有以下人为的功能...
bool IsPositive(int val) { return val > 0; }
有明显的> 0
和< 0
的情况,以及(尽管不是那么微妙的)边缘情况== 0
。 目前,val = 0
将被视为负面行为,这可能是也可能不是期望的行为。 此函数中正在计算单个表达式,并且没有分支或循环,因此复杂性尽可能低,我们可以很容易地推断出这将如何表现。
特别是在 C# 方面。您可以将所有内容包装在一个通用的try/catch
块中,但那是..."最佳实践"的反义词是什么?哦,是的,垃圾。 如果您认为可能会遇到某种完全超出您控制的异常(即非托管资源 - 数据库连接、http 客户端连接等),那么您可以专门针对该异常。 然后,您将使用try/catch
但仅捕获该特定异常。 这当然意味着您已经确定了函数中可能的故障点。
确定故障点的能力可以来自理解您正在与之交互的数据结构,或者更常见的是,只是经验(试错、调试)。 这再次超越了 C#,并回到了一般编程技能。像任何技能一样,发展需要时间,有些人天生比其他技能更擅长。
现在让我们看一下您的代码...
public int GetIntSetting(SET setting, int nullState)
什么是SET
? 现在,我们将忽略这样一个事实,即SET
的行为可能封装在某些现有的.NET类中。
{
// Get the setting value or nullState if it was not set
var str = GetStringSetting(setting, nullState.ToString());
此时,您已检索设置或nullState.ToString()
,并将其保存在str
中。 因此,在下一行中,您可以使用它而不是再次评估它。 但是,这里您只使用该值一次,因此我认为为变量分配内存是没有意义的,只是在下一行中取消引用它。
// Check if it's an integer
var isInt = int.TryParse(GetStringSetting(setting, nullState.ToString()), out int parsed);
同样,没有理由将int.TryParse()
结果保存到变量中,可以将其移动到if
表达式中。 但是,你不需要TryParse
,因为如果没有设置setting
它将使用nullState.ToString()
,我们知道它必须是整数作为字符串,将被成功解析。 然后诀窍是弄清楚GetStringSetting()
实际做了什么(代码气味)。
// If integer then return else update settings and return nullState
if (isInt )
{
return parsed;
}
else
{
UpdateIntSetting(setting, nullState);
return nullState;
}
从局部上讲,您将在if
块中返回,无需else
.
因此,如果设置是整数值,则返回它,否则将其更改为nullState
并返回nullState
。 但我相信GetStringSetting()
的第二个参数是返回的值,如果第一个参数"未设置",无论这意味着什么(null?null还是空? 这意味着如果未设置该设置,您将已经在使用nullState
(GetStringSetting(setting, nullState.ToString())
)
。但也许该设置可以设置为非数字字符串(复杂的边缘情况)。 如果发生这种情况,设置仍将是非数字字符串,但该方法的其余部分将使用nullState
进行操作。
如果不了解其他代码,就很难说出你的边缘情况是什么,这往往是一种代码异味 - 需要其他代码来理解方法/函数的行为方式(有时,在实践中,这是无法避免的)。
这种方法有潜在的副作用。 如果未设置setting
或不是整数值,则其值将更改为nullState
。 我认为我能对给出的信息做的最好的事情,因为我对SET
类一无所知,就是假设如果parsed
等于nullState
,那么设置需要更新。 这可能会导致不需要的后续更新(因此可以使用更多信息进行优化)。
public int GetIntSetting(ISetting setting, int nullState)
{
int.Parse(GetStringSetting(setting, nullState.ToString()), out int parsed);
if(parsed == nullState)
{
UpdateIntSetting(setting, nullState);
}
return parsed;
}
您可以反转if
语句的表达式,并立即返回parsed != nullState
(VS 或像 resharper 这样的工具可能会提出这个建议),但在这种情况下复杂性不会改变,你最终会得到两个 return 语句而不是一个。
就个人而言,我可能会制作这些(GetIntSetting()
/GetStringSetting()
)扩展方法,或者在我将创建的ISetting
接口上公开它们。
现在我们已经重构了这一点,很容易看出我们唯一可能的故障点只是......
GetStringSetting()
因为我们在第一行调用它,但这是一个未知数。- 又
UpdateIntSetting()
,一个未知数
这很好。 我们将对两个未知方法重复该过程,我们可以开始传递我们为此编写的单元测试用例。
最后,我们只剩下一个方法,因为如上所述,它具有改变setting
值的潜在副作用。 我将不得不查看代码库的其余部分,但是如果返回的值nullState
,则将其留给调用方该怎么做可能是有意义的。 我知道预先知道将更新隐藏在此方法中可能是有意义的,但它使这个单独的部分更加复杂,并且复杂性会迅速积累。
更新
我猜GetStringSetting()
做类似的事情,因为如果未设置,它会更新设置。 而且由于GetIntSetting()
依赖于它,并且nullValue.ToString()
作为默认值传递,我们知道在我们调用它之后setting
如果未设置,则会使用nullValue
进行更新,然后我们可以从GetIntSetting()
中删除更新。 然后代码变成...
public int GetIntSetting(ISetting setting, int nullState)
=> int.Parse(GetStringSetting(setting, nullState.ToString()), out int parsed);
它仍然是一种方法,因为它依赖于一种方法,并且仍然存在潜在的副作用,但它降低了GetIntSetting()
的复杂性。 这一切都基于GetStringSetting()
正在执行更新的假设。
我们可以为此编写测试用例,但是这个工作单元不应该抛出任何异常(GetStringSetting()
,或者它称之为可能的东西)。 如果这些是扩展方法或在ISetting
接口上,则可以模拟GetStringSetting()
以确保GetIntSetting()
不会因集成测试而失败。