1. Guard Clauses
当业务增长导致代码变得臃肿且复杂时,函数作为最小的可执行单元,其易读性变得尤为重要。函数的清晰性可以通过良好的命名和结构来实现,而Guard Clauses设计模式则是简化复杂性的一种有效手段。通过使用Guard Clauses,我们可以在函数开始处进行预判并立即返回或抛出异常,以确保函数仅在有效条件下执行,避免深度嵌套的if和switch语句。进一步的优化是通过反转流程,即编写主逻辑而将错误情况作为else语句处理,使得代码更加简洁、清晰,减少了深层嵌套和沉长的可能性。
如下是一个简单的函数没有使用Guard Clauses
public void Subscribe(User user, Subscription subscription, Term term)
{
if (user != null)
{
if (subscription != null)
{
if (term == Term.Annually)
{
// subscribe annually
}
else if (term == Term.Monthly)
{
// subscribe monthly
}
else
{
throw new InvalidEnumArgumentException(nameof(term));
}
}
else
{
throw new ArgumentNullException(nameof(subscription));
}
}
else
{
throw new ArgumentNullException(nameof(user));
}
}
我们可以反转一下if语句中的逻辑并将抛出异常的语句放到if中,从而来消除else语句
public void Subscribe2(User user, Subscription subscription, Term term)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (subscription == null)
{
throw new ArgumentNullException(nameof(subscription));
}
if (term == Term.Annually)
{
// subscribe annually
}
else if (term == Term.Monthly)
{
// subscribe monthly
}
else
{
throw new InvalidEnumArgumentException(nameof(term));
}
}
对null和抛出特定类型异常的常见行为的检查显然违反了DRY原则。可以将此代码提取到一个Helper方法中:
public static class Guard
{
public static void AgainstNull(object argument, string argumentName)
{
if (argument == null)
{
throw new ArgumentNullException(argumentName);
}
}
public static void AgainstInvalidTerms(Term term, string argumentName)
{
// note: currently there are only two enum options
if (term != Term.Annually &&
term != Term.Monthly)
{
throw new InvalidEnumArgumentException(argumentName);
}
}
}
现在可以调用这些帮助方法,设置不需要在函数中包含任何if语句,因为如果发生异常,它将从原始函数退出。现在你的代码将变成如下:
public void Subscribe3(User user, Subscription subscription, Term term)
{
Guard.AgainstNull(user, nameof(user));
Guard.AgainstNull(subscription, nameof(subscription));
Guard.AgainstInvalidTerms(term, nameof(term));
if (term == Term.Annually)
{
// subscribe annually
return;
}
// subscribe monthly
}
随着项目的不断演进,您可以继续添加额外的辅助方法覆盖别的应用场景,例如空字符串、负数、无效枚举值等。
2. Ardalis.GuardClauses
Ardalis.GuardClauses是一个第三方的包,用来实现扩展Guard Clauses
2.1 支持如下Guard Clauses
Guard.Against.Null
描述:如果输入为空抛出异常
Guard.Against.NullOrEmpty
描述:输入字符串,guid或者array为null或者empty抛出异常
Guard.Against.NullOrWhiteSpace
描述:输入的字符串为null,empty或者whitespace抛出异常
Guard.Against.OutOfRange
描述:如果 integer/DateTime/enum 超出提供的范围抛出异常
Guard.Against.EnumOutOfRange
描述:如果提供的枚举值超出了定义枚举值的范围则抛出异常
Guard.Against.OutOfSQLDateRange
描述:输入的日期类型超出SQL Server Datetime 范围则抛出异常
Guard.Against.Zero
描述:输入数字为0则抛出异常
Guard.Against.Expression
描述:自定义表达式
Guard.Against.InvalidFormat
描述:使用正则表达式或者函数自定义格式
10.Guard.Against.NotFound
描述:使用id/key查找,如果没有发现对象,抛出异常
2.2 拓展自己的Guard Clauses
你可以通过如下代码来扩展你自己的Guard:
// Using the same namespace will make sure your code picks up your
// extensions no matter where they are in your codebase.
namespace Ardalis.GuardClauses
{
public static class FooGuard
{
public static void Foo(this IGuardClause guardClause,
string input,
[CallerArgumentExpression("input")] string? parameterName = null)
{
if (input?.ToLower() == "foo")
throw new ArgumentException("Should not have been foo!", parameterName);
}
}
}
// Usage
public void SomeMethod(string something)
{
Guard.Against.Foo(something);
Guard.Against.Foo(something, nameof(something)); // optional - provide parameter name
}
github:
https://github.com/bingbing-gui/dotnet-guide/blob/main/DesignPatterns/GuardClause%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md
参考文章:
1.https://deviq.com/design-patterns/guard-clause
2.https://github.com/ardalis/GuardClauses