保护价值与数据的方法?

时间:2018-10-08 19:14:52

标签: c# validation design-patterns

有人知道防止无效的数据而不是无效的 values 的任何技术或方法吗?我知道这似乎是一个奇怪的区别,但请忍受。

最近,我想到了一个更好的方法来防范无效变量值。目前,我们将异常包装在Guard类中,但是建议更进一步,将那些包装在Extensions中。换句话说:

int myVar = 0;

// Basic
if (myVar < 1) throw new InvalidArgumentException("myVar cannot be less than 1");

// Guard -- wraps the above exception
Guard.AgainstValuesLessThan(1, myVar, nameof(myVar), "Value cannot be less than 1");

// Extension -- wraps the above guard
myVar.EnsureValid();

我在这里的问题是,感觉就像乌龟一样,一直下来-最终,我们将决定扩展名不起作用,并用其他东西包装扩展名。此外,对原语的扩展使您很难确定要保护的内容以及原因-想象以下情况:

int customerId = 1;
int employeeId = 1;

// How do I write an extension method that lets me say "Guard against invalid Customer 
// IDs" and "Guard against invalid Employee IDs"?  Both are ints.  This method allows a
// guard, but with no context.
public static void EnsureValidValue(this int actual, int expected, string msg);

employeeId.EnsureValidValue(1, "Employee ID can't be less than 1");
customerId.EnsureValidValue(1, "Customer ID can't be less than 1");

所以我试图在这里找到一种更好的方法,这就是我绊倒的地方。到目前为止,我所遇到的问题是,我的问题从根本上说是我正在考虑将什么作为 data 保护(“我需要防止不良的客户ID”),但是我保护(“我需要确保该整数至少为1”)。

我不确定从这里去哪里。我觉得这可能是“我不知道那个存在,但有帮助!”之一。案例。

有人对看什么有任何建议吗?到目前为止,我有3种想法,我不确定一种想法是否比另一种更好,或者是否有某种方法可以将它们结合起来以达到我的目标:

  • 代码合同
  • 我缺少一些设计模式
  • 其他一些技巧或窍门(例如,属性?InvokerParameterName?)

最后一点:我知道ReSharper提供了一个Annotations库,但并非团队中的每个人都使用ReSharper(包括我在内),所以我不想依赖它。

2 个答案:

答案 0 :(得分:1)

这并不能完全回答您的问题,但这是我在评论中谈论的内容:

public static class AssertValid
{

    public static void TestAssertValid()
    {
        var customerId = 0;
        AssertValid.MinimumFor(customerId, 1, nameof(customerId));

    }
    public static void RangeFor<T>(T variableValue, T min, T max, string varName,
        string message = "Variable {0} outside of range {1} to {2} in function {3}",
        [CallerMemberName] string inFunc = "") where T : IComparable
    {
        if (variableValue.CompareTo(min) < 0 || variableValue.CompareTo(max) > 0)
        {
            var msg = string.Format(message, varName, min, max, inFunc);
            throw new ArgumentOutOfRangeException(varName, variableValue, msg);
        }
    }

    public static void MinimumFor<T>(T variableValue, T min, string varName,
        string message = "Variable {0} less than minimum of {1} in function {2}",
        [CallerMemberName] string inFunc = "") where T : IComparable
    {
        if (variableValue.CompareTo(min) < 0)
        {
            var msg = string.Format(message, varName, min, inFunc);
            throw new ArgumentOutOfRangeException(varName, variableValue, msg);
        }
    }

    public static void MaximumFor<T>(T variableValue, T min, string varName,
        string message = "Variable {0} greater than maximum of {1} in function {2}",
        [CallerMemberName] string inFunc = "") where T : IComparable
    {
        //...
    }

    public static void StringLengthRangeFor(string variableValue, int min, int max, string varName,
        string message = "Length of string variable {0} outside of range {1} to {2} in function {3}",
        [CallerMemberName] string inFunc = "")
    {
        if (variableValue.Length < min || variableValue.Length > max)
        {
            var msg = string.Format(message, varName, min, max, inFunc);
            throw new ArgumentOutOfRangeException(varName, variableValue, msg);
        }
    }

    public static void StringLengthMinFor(string variableValue, int min, int max, string varName,
        string message = "Length of string variable {0} less than {1} characters in function {2}",
        [CallerMemberName] string inFunc = "")
    {
        if (variableValue.Length < min)
        {
            var msg = string.Format(message, varName, min, inFunc);
            throw new ArgumentOutOfRangeException(varName, variableValue, msg);
        }
    }

    public static void StringLengthMaxFor(string variableValue, int max, string varName,
        string message = "Length of string variable {0} greater than {1} characters in function {2}",
        [CallerMemberName] string inFunc = "")
    {
        //...
    }

    public static void StringLengthPatternFor(string variableValue, string regexPattern, string varName,
        string message = "String variable {0} does not match acceptable pattern in function {1}",
        [CallerMemberName] string inFunc = "")
    {
        //... Use ArgumentException
    }

}

然后,如果您有类似的内容(在名为TestAssertValid的函数中):

var customerId = 0;
AssertValid.MinimumFor(customerId, 1, nameof(customerId));

您最终遇到如下异常:

System.ArgumentOutOfRangeException: 'Variable customerId less than minimum of 1 in function TestAssertValid
Parameter name: customerId
Actual value was 0.'

您可能想要做的另一件事是将其放入一个可实例化的类中,所有方法均为实例方法。您创建该类的一个实例,对该实例执行所有声明,最后,您声明一切都很好。如果不是,则将所有故障吐出(一个例外)。

这样,您的所有测试都在引发异常之前完成。

答案 1 :(得分:0)

从可维护性的角度来看,我认为某些OOP /设计模式将是最好的方法。我同意您的看法,如果不是所有开发人员都需要使用ReSharper,请尝试将其嵌入代码中。

拥有Guard子句的方式很简单,并且易于重用;我不会改变这一点。如果我对您的理解正确,那么您就在防范业务规则,不一定是基本价值观。您希望封装“不能具有小于1的ID”的业务规则,这绝对是一个不错的规则。

建议您考虑以下几种选择:

  1. 将您的逻辑放入业务对象的构造函数中。遵循的一个很好的原则是,对象应该负责维护自己的状态。在构造函数中传递有效的ID,并让构造函数包含guard子句和错误消息,例如下面的代码段。

public Employee(int id) { Guard.AgainstValuesLessThan(1, id, nameof(id), "Employee ID cannot be less than 1"); }

如果在代码的其他地方使用该对象,则可以肯定地知道它处于有效状态,因为您已成功构造了该对象。这也解决了记住在调用者函数/类中调用Guard子句的问题。

  1. 使用装饰器模式。这一点有点高级,需要在项目中进行更多设置,但是从本质上讲,您可以编写一个类来为您执行验证。您可以连接一个类似于SimpleInjector的DI库,以拦截实现某个接口(在这种情况下,类似于IEmployeeID)的请求,并在继续执行任何代码之前执行验证。这对于其他业务规则非常有帮助,并且可以解决诸如“该员工ID甚至存在于数据库中?”之类的问题。如果不存在,则可以取消其余代码的任何执行。这样,您可以将验证逻辑划分到一个单独的类中。

我倾向于首先选择选项1,因为它可以帮助您专注于创建具有有效状态的对象,这是一个很好的遵循原则。如果您需要更多示例,或者我想念您的问题,请让我知道,我可以给出更好的答案:)