代码的复杂度会影响我们对代码的理解。
一般而言,我们认为代码的最小单元就是一个函数或者方法,我们写出的代码应该能让人迅速理解,所以这要求我们的代码行数不要太多、命名清晰规范、功能单一(一个方法只做一件事)。导致代码复杂性增加的一个原因是条件判断,这时代码中往往会出现if或者Switch语句,如果没有组织好你的程序结构,这两种语句很容易将你的代码从简洁易懂的变成冗长,模糊,甚至低效的。
所以有一个办法是通过使用守卫语句(Guard Clause)来避免这些问题。
1.守卫语句
守卫语句只是一个立即退出函数的检查,检查可能是正常返回也可能是抛出异常。如果你习惯于编写一个函数来确保要运行的函数的成员都是合法有效的,那么你在写主函数时没需要用else语句来处理错误的情况,这会反转你当前的工作流程,好处是会使你的代码更加简单,并且缩颈深度更少。
先看不使用保护语句的例子:
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));
}
}
显然可以重构上面的代码,消除else字句的需要。我们首选处理user 、subscription为null的情况,然后再处理正常情况:
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原则,可以 将代码提取到辅助方法中。
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);
}
}
}
那么最后代买就演变成:
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
}
看起来是不是清晰多了。随着时间的推移,你在后续开发中遇到新的守卫情况(比如检查除数是否为0),可以继续添加Guard辅助方法。
2.Ardalis.GuardClauses
C#里面已经有这样一个包,帮我们实现了很多守卫情况,详情可见Ardalis.GuardClauses。
使用方法:
public void ProcessOrder(Order order)
{
Guard.Against.Null(order, nameof(order));
// process order here
}
// OR
public class Order
{
private string _name;
private int _quantity;
private long _max;
private decimal _unitPrice;
private DateTime _dateCreated;
public Order(string name, int quantity, long max, decimal unitPrice, DateTime dateCreated)
{
_name = Guard.Against.NullOrWhiteSpace(name, nameof(name));
_quantity = Guard.Against.NegativeOrZero(quantity, nameof(quantity));
_max = Guard.Against.Zero(max, nameof(max));
_unitPrice = Guard.Against.Negative(unitPrice, nameof(unitPrice));
_dateCreated = Guard.Against.OutOfSQLDateRange(dateCreated, nameof(dateCreated));
}
}
目前支持的守卫语句有:
- Guard.Against.Null (throws if input is null)
- Guard.Against.NullOrEmpty (throws if string, guid or array input is null or empty)
- Guard.Against.NullOrWhiteSpace (throws if string input is null, empty or whitespace)
- Guard.Against.OutOfRange (throws if integer/DateTime/enum input is outside a provided range)
- Guard.Against.EnumOutOfRange (throws if a enum value is outside a provided Enum range)
- Guard.Against.OutOfSQLDateRange (throws if DateTime input is outside the valid range of SQL Server DateTime values)
- Guard.Against.Zero (throws if number input is zero)
显然这不会满足个性化的开发需求,你可以自己扩展这个包,跟扩展其它类一样:
// 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, string parameterName)
{
if (input?.ToLower() == "foo")
throw new ArgumentException("Should not have been foo!", parameterName);
}
}
}
// Usage
public void SomeMethod(string something)
{
Guard.Against.Foo(something, nameof(something));
}
实际使用中,也不是任何判断都要用守卫语句,守卫语句并不影响程序逻辑,性能等,关键在于提高代码质量,增强可读性,要用的恰到好处。