使用struct强制验证内置类型


9

通常,域对象具有可以用内置类型表示的属性,但是其有效值是可以用该类型表示的值的子集。

在这些情况下,可以使用内置类型存储值,但是必须确保始终在输入点验证值,否则我们最终可能会使用无效值。

解决此问题的一种方法是将值存储为自定义struct,该自定义具有private readonly内置类型的单个后备字段,并且其构造函数会验证提供的值。然后,我们始终可以确保仅通过使用此struct类型来使用经过验证的值。

我们还可以提供与底层内置类型之间的强制转换运算符,以便值可以作为底层类型无缝地进入和退出。

以一个例子为例,其中我们需要表示域对象的名称,并且有效值是长度在1到255个字符(含)之间的任何字符串。我们可以使用以下结构来表示:

public struct ValidatedName : IEquatable<ValidatedName>
{
    private readonly string _value;

    private ValidatedName(string name)
    {
        _value = name;
    }

    public static bool IsValid(string name)
    {
        return !String.IsNullOrEmpty(name) && name.Length <= 255;
    }

    public bool Equals(ValidatedName other)
    {
        return _value == other._value;
    }

    public override bool Equals(object obj)
    {
        if (obj is ValidatedName)
        {
            return Equals((ValidatedName)obj);
        }
        return false;
    }

    public static implicit operator string(ValidatedName x)
    {
        return x.ToString();
    }

    public static explicit operator ValidatedName(string x)
    {
        if (IsValid(x))
        {
            return new ValidatedName(x);
        }
        throw new InvalidCastException();
    }

    public static bool operator ==(ValidatedName x, ValidatedName y)
    {
        return x.Equals(y);
    }

    public static bool operator !=(ValidatedName x, ValidatedName y)
    {
        return !x.Equals(y);
    }

    public override int GetHashCode()
    {
        return _value.GetHashCode();
    }

    public override string ToString()
    {
        return _value;
    }
}

该示例显示了TO- string投的implicit,因为这可能永远不会失败,但在从- string铸造的explicit,因为这将引发无效值,但当然这些都可能是两种implicitexplicit

还要注意,只能通过from的强制转换来初始化此结构string,但是可以使用IsValid static方法预先测试这种强制转换是否会失败。

这似乎是强制验证可以用简单类型表示的域值的一种好模式,但是我看不到它经常使用或建议使用,而且我对原因很感兴趣。

所以我的问题是:您认为使用此模式的优点和缺点是什么?为什么?

如果您认为这是一个糟糕的模式,我想了解为什么以及您认为是最佳的选择。

注意:我最初是在Stack Overflow上这个问题的,但由于主要基于观点(具有讽刺意味的是,它本身具有主观性)而被搁置了-希望它可以在这里获得更多的成功。

上面是原始文本,下面是一些想法,部分是对搁置之前在那里收到的答复的回应:

  • 答案提出的主要观点之一是上述模式所需的样板代码数量,尤其是在需要许多此类代码时。但是,为了捍卫这种模式,可以使用模板在很大程度上将其自动化,并且实际上对我来说似乎还不错,但这只是我的观点。
  • 从概念的角度来看,使用强类型语言(例如C#)仅将强类型原理应用于复合值,而不是将其扩展为可以由a实例表示的值,这似乎并不奇怪。内置类型?

您可以制作一个模板版本,该版本采用bool(T)lambda
棘轮怪胎

Answers:


4

这在ML风格的语言(例如Standard ML / OCaml / F#/ Haskell)中相当普遍,在其中创建包装器类型要容易得多。它为您提供两个好处:

  • 它允许一段代码来强制字符串已经经过验证,而不必自己进行验证。
  • 它使您可以将验证代码集中在一个地方。如果ValidatedNameever包含无效值,则说明该IsValid方法中有错误。

如果IsValid正确使用该方法,则可以保证任何接收到a的函数ValidatedName实际上都将接收经过验证的名称。

如果需要进行字符串操作,则可以添加一个公共方法,该方法接受一个接受String(的值ValidatedName)并返回String(新值)的函数,并验证应用该函数的结果。这消除了获取基础String值并重新包装它的样板。

包装值的一个相关用途是跟踪其来源。例如,基于C的OS API有时以整数形式提供资源句柄。您可以包装OS API来使用Handle结构,而仅提供对构造函数的访问,以访问代码的那部分。如果产生Handles 的代码正确,那么将仅使用有效的句柄。


1

您认为使用此模式有什么优点和缺点,为什么?

  • 它是自包含的。验证位太多,卷须到达不同的位置。
  • 它有助于自我记录。看到一个方法可以ValidatedString使调用的语义更加清晰。
  • 它有助于将验证限制在一个位置,而无需在公共方法之间重复。

不好

  • 铸造技巧被隐藏了。它不是惯用的C#,因此在阅读代码时可能会造成混乱。
  • 它抛出。拥有不符合验证条件的字符串并不是一个例外的情况。做IsValid演员之前是一个小unweildy。
  • 它无法告诉您为什么有些无效。
  • 默认值ValidatedString无效/已验证。

我见过这样的事情更多的时候UserAuthenticatedUser这样的东西,其中对象实际上改变。尽管在C#中似乎不合适,但它可能是一种很好的方法。


1
谢谢,我认为您的第四个“骗子”是迄今为止最令人信服的参数-使用默认值或类型的数组可能会为您提供无效值(当然取决于零/空字符串是否是有效值)。(我认为)这是以无效值结束的仅有两种方法。但是,如果我们不使用这种模式,这两件事仍然会为我们提供无效的值,但是我想至少我们知道需要对它们进行验证。因此,这可能会使基础类型的默认值对我们的类型无效的方法无效。
gmoody1979'1

所有缺点都是实现问题,而不是概念问题。另外,我发现“例外应该是例外”是一个模糊且定义不清的概念。最实用的方法是同时提供基于异常和基于非异常的方法,并让调用者选择。
2015年

@Doval我同意,除非另有评论。模式的重点是要确定是否拥有ValidatedName,它必须是有效的。如果基础类型的默认值也不是域类型的有效值,则此方法将失效。当然,这取决于域,但基于字符串的类型(而不是数字类型)更可能是这种情况(我想过)。在基础类型的默认值也适合作为域类型的默认值时,该模式最有效。
gmoody1979

@Doval-我普遍同意。这个概念本身很好,但是它正在有效地尝试将改进类型转换为不支持它们的语言。总是会有实施问题。
Telastyn 2015年

话虽如此,我想您可以检查“出站”强制转换的默认值以及该结构方法中任何其他必要位置的默认值,如果未初始化,则抛出该默认值,但这会变得混乱。
gmoody1979

0

您的方法相当繁重。我通常将域实体定义为:

public class Institution
{
    private Institution() { }

    public Institution(int organizationId, string name)
    {
        OrganizationId = organizationId;            
        Name = name;
        ReplicationKey = Guid.NewGuid();

        new InstitutionValidator().ValidateAndThrow(this);
    }

    public int Id { get; private set; }
    public string Name { get; private set; }        
    public virtual ICollection<Department> Departments { get; private set; }

    ... other properties    

    public Department AddDepartment(string name)
    {
        var department = new Department(Id, name);
        if (Departments == null) Departments = new List<Department>();
        Departments.Add(department);            
        return department;
    }

    ... other domain operations
}

在实体的构造函数中,使用FluentValidation.NET触发验证,以确保您不能创建状态无效的实体。请注意,属性都是只读的-您只能通过构造函数或专用域操作进行设置。

该实体的验证是一个单独的类:

public class InstitutionValidator : AbstractValidator<Institution>
{
    public InstitutionValidator()
    {
        RuleFor(institution => institution.Name).NotNull().Length(1, 100).WithLocalizedName(() =>   Prim.Mgp.Infrastructure.Resources.GlobalResources.InstitutionName);       
        RuleFor(institution => institution.OrganizationId).GreaterThan(0);
        RuleFor(institution => institution.ReplicationKey).NotNull().NotEqual(Guid.Empty);
    }  
}

这些验证器也可以轻松重用,并且您编写的样板代码更少。另一个优点是可读性强。


下选民会在意解释为什么我的答案被选空吗?
4点

问题是关于约束值类型的结构,您切换到一个类而不解释WHY。(不是贬低者,只是提出建议。)
DougM 2015年

我解释了为什么我找到一个更好的选择,这是他的问题之一。谢谢回复。
4

0

我喜欢这种对值类型的方法。这个概念很棒,但是我对实现有一些建议/抱怨。

投射:在这种情况下,我不喜欢使用投射。显式的from字符串转换不是问题,但是(ValidatedName)nameValuenew和new 之间没有太大区别ValidatedName(nameValue)。因此,这似乎是不必要的。隐式字符串转换是最糟糕的问题。我认为获取实际的字符串值应该更加明确,因为它可能会意外地分配给字符串,并且编译器不会警告您可能的“精度损失”。这种精度损失应该是明确的。

ToString:我更喜欢将ToString重载用于调试目的。而且我不认为返回原始值是个好主意。这与隐式到字符串转换的问题相同。获取内部值应该是显式操作。我相信您正在尝试使结构的行为像外部代码的普通字符串一样,但是我认为这样做会使您失去从实现这种类型中获得的一些价值。

EqualsGetHashCode:默认情况下,结构使用结构相等。因此,您EqualsGetHashCode正在复制此默认行为。您可以删除它们,这几乎是相同的事情。


转换:从语义上来说,这更像是将字符串转换为ValidatedName,而不是创建新的ValidatedName:我们将现有字符串标识为ValidatedName。因此,对我来说,演员表在语义上似乎更正确。同意打字(键盘上的手指)几乎没有区别。我不同意对字符串的
强制转换

ToString:我不同意。对我来说,ToString是一种完全有效的方法,可以在调试方案之外使用,前提是它符合要求。同样在一种类型是另一种类型的子集的情况下,我认为使从子集到超集的能力转换尽可能容易是有意义的,因此,如果用户需要,他们几乎可以将其视为的超集类型,即字符串...
gmoody1979

Equals和GetHashCode:是,结构使用结构相等,但是在这种情况下,它是在比较字符串引用而不是字符串的值。因此,我们确实需要覆盖等于。我同意,如果基础类型是值类型,则不必这样做。根据我对值类型的默认GetHashCode实现(这是相当有限的)的理解,这将提供相同的值,但性能更高。我真的应该测试是否是这种情况,但这是问题要点的一个附带问题。谢谢您的回答:-)。
gmoody1979

@ gmoody1979默认情况下,在每个字段上使用Equals比较结构。字符串不应该是一个问题。与GetHashCode相同。至于结构是字符串的子集。我喜欢将类型视为安全网。我不想使用ValidatedName,然后不小心滑倒使用字符串。如果编译器使我明确指定我现在想使用未经检查的数据,我将更愿意。
欣快感,2015年

对不起,对等点很好。尽管在给定默认行为的情况下改写应该会更好,但是默认行为需要使用反射进行比较。强制转换:是的,可以将其设为显式强制转换。
gmoody1979
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.