强类型字符串

Tim*_*lds 22 c# string types design-patterns

那个设定

我有一个原型类TypedString<T>,试图"强类型"(可疑意义)某个类别的字符串.它使用了奇怪的重复模板模式(CRTP)的C#模式.

class TypedString<T>

public abstract class TypedString<T>
    : IComparable<T>
    , IEquatable<T>
    where T : TypedString<T>
{
    public string Value { get; private set; }

    protected virtual StringComparison ComparisonType
    {
        get { return StringComparison.Ordinal; }
    }

    protected TypedString(string value)
    {
        if (value == null)
            throw new ArgumentNullException("value");
        this.Value = Parse(value);
    }

    //May throw FormatException
    protected virtual string Parse(string value)
    {
        return value;
    }

    public int CompareTo(T other)
    {
        return string.Compare(this.Value, other.Value, ComparisonType);
    }

    public bool Equals(T other)
    {
        return string.Equals(this.Value, other.Value, ComparisonType);
    }

    public override bool Equals(object obj)
    {
        return obj is T && Equals(obj as T);
    }

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

    public override string ToString()
    {
        return Value;
    }
}
Run Code Online (Sandbox Code Playgroud)

TypedString<T>在我的项目中定义一堆不同的"字符串类别"时,该类现在可用于消除代码重复.这个类的一个简单用法是定义一个Username类:

class Username (例)

public class Username : TypedString<Username>
{
    public Username(string value)
        : base(value)
    {
    }

    protected override string Parse(string value)
    {
        if (!value.Any())
            throw new FormatException("Username must contain at least one character.");
        if (!value.All(char.IsLetterOrDigit))
            throw new FormatException("Username may only contain letters and digits.");
        return value;
    }
}
Run Code Online (Sandbox Code Playgroud)

这现在让我Username在整个项目中使用该类,从不必检查用户名是否格式正确 - 如果我有一个表达式或类型的变量Username,它保证是正确的(或null).

场景1

string GetUserRootDirectory(Username user)
{
    if (user == null)
        throw new ArgumentNullException("user");
    return Path.Combine(UsersDirectory, user.ToString());
}
Run Code Online (Sandbox Code Playgroud)

我不必担心这里的用户字符串格式化 - 我已经知道它的类型性质是正确的.

情景2

IEnumerable<Username> GetFriends(Username user)
{
    //...
}
Run Code Online (Sandbox Code Playgroud)

在这里,调用者只根据类型知道返回的内容.一个IEnumerable<string>需要读入的方法或文件的细节.更糟糕的是,如果有人要更改GetFriends其引入错误并生成无效用户名字符串的实现,那么该错误可能会无声地传播给方法的调用者并造成各种破坏.这个类型很好的版本阻止了这一点.

场景3

System.Uri是.NET中的一个类的示例,它只包含一个具有大量格式约束的字符串和用于访问其有用部分的辅助属性/方法.所以这是一个证据表明这种方法并不完全疯狂.

问题

我想这种事情以前已经完成了.我已经看到了这种方法的好处,不需要再说服自己了.

我可能会缺少一个缺点吗?
有没有办法可以在以后再次咬我?

Tim*_*ora 6

一般思考

我并没有从根本上反对这种方法(并且知道/使用CRTP的荣誉,这可能非常有用).该方法允许元数据包裹在单个值周围,这可能是一件非常好的事情.它也是可扩展的; 您可以在不破坏接口的情况下向该类型添加其他数据.

我不喜欢你当前的实现似乎很大程度上依赖于基于异常的流程这一事实.这可能非常适合某些事情或真正特殊的情况.但是,如果用户试图选择有效的用户名,他们可能会在此过程中抛出数十个异常.

当然,您可以向界面添加无异常验证.你还必须问自己,你验证规则住(这始终是一个挑战,尤其是在分布式应用程序).

WCF

说到"分发":考虑将这些类型作为WCF数据合同的一部分实施的含义.忽略数据契约通常应该暴露简单DTO的事实,你也有代理类的问题,它将维护你的类型的属性,而不是它的实现.

当然,您可以通过将父程序集放在客户端和服务器上来缓解这种情况.在某些情况下,这是完全合适的.在其他情况下,不那么重要.假设您的某个字符串的验证需要调用数据库.这很可能不适合在客户端/服务器位置.

"场景1"

听起来你正在寻求一致的格式化.这是一个有价值的目标,适用于URI和用户名之类的东西.对于更复杂的字符串,这可能是一个挑战.我已经研究过产品,即使是"简单"的字符串也可以根据上下文以多种不同的方式进行格式化.在这种情况下,专用(可能是可重复使用的)格式化器可能更合适.

同样,非常具体情况.

"场景2"

更糟糕的是,如果有人要更改GetFriends的实现,以致它引入了一个错误并产生无效的用户名字符串,那么该错误可能会无声地传播给该方法的调用者并造成各种破坏.

IEnumerable<Username> GetFriends(Username user) { }
Run Code Online (Sandbox Code Playgroud)

我可以看到这个论点.我想到了一些事情:

  • 一个更好的方法名称: GetUserNamesOfFriends()
  • 单元/集成测试
  • 据推测,这些用户名在创建/修改时会得到验证.如果这是您自己的API,为什么您不相信它给你的东西?

旁注:在处理人/用户时,不可变ID可能更有用(人们喜欢更改用户名).

"情景3"

System.Uri是.NET中一个类的一个例子,它只包含一个具有大量格式约束的字符串和辅助属性/方法来访问它的有用部分.所以这是一个证据表明这种方法并不完全疯狂.

没有争论,BCL中有很多这样的例子.

最后的想法

  • 将值包装到更复杂的类型中没有任何问题,因此可以使用更丰富的元数据来描述/操作它.
  • 在一个地方集中验证是一件好事,但请确保选择正确的地方.
  • 当逻辑驻留在传递的类型中时,跨越序列化边界会带来挑战.
  • 如果您主要关注信任输入,则可以使用一个简单的包装类,让被调用者知道它正在接收已经过验证的数据.无论在何处/如何进行验证都无关紧要.

ASP.Net MVC对字符串使用了类似的范例.如果值为IMvcHtmlString,则将其视为受信任且不再编码.如果没有,则进行编码.