25

那个设定

我有一个原型类TypedString<T>,它试图“强类型”(可疑的含义)某个类别的字符串。它使用 C#-analogue 的奇怪重复模板模式 (CRTP)

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;
    }
}

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;
    }
}

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

方案 1

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

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

方案 2

IEnumerable<Username> GetFriends(Username user)
{
    //...
}

在这里,调用者仅根据类型就知道它得到了什么作为返回。需要IEnumerable<string>阅读方法或文档的详细信息。更糟糕的是,如果有人要更改其实现,GetFriends从而引入错误并产生无效的用户名字符串,则该错误可能会默默地传播给方法的调用者并造成各种破坏。这个打字很好的版本可以防止这种情况。

方案 3

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

问题

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

我可能会遗漏一个缺点吗?
有没有办法这以后会回来咬我?

4

4 回答 4

7

一般想法

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

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

当然,您可以向接口添加无异常验证。您还必须问自己希望验证规则存在于何处(这始终是一个挑战,尤其是在分布式应用程序中)。

WCF

说到“分发”:考虑将此类类型作为 WCF 数据契约的一部分实现的含义。忽略数据协定通常应该公开简单的 DTO 的事实,您还会遇到代理类的问题,它会维护您的类型的属性,但不会维护它的实现。

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

“情景 1”

听起来您正在寻求一致的格式。这是一个有价值的目标,并且非常适合 URI 和用户名之类的东西。对于更复杂的字符串,这可能是一个挑战。我曾开发过一些产品,即使是“简单”的字符串也可以根据上下文以多种不同的方式进行格式化。在这种情况下,专用的(也许是可重用的)格式化程序可能更合适。

同样,非常针对具体情况。

“情景 2”

更糟糕的是,如果有人要更改 GetFriends 的实现,使其引入错误并生成无效的用户名字符串,则该错误可能会默默地传播给方法的调用者并造成各种破坏。

IEnumerable<Username> GetFriends(Username user) { }

我可以看到这个论点。我想到了几件事:

  • 更好的方法名称:GetUserNamesOfFriends()
  • 单元/集成测试
  • 大概这些用户名在创建/修改时被验证。如果这是你自己的 API,你为什么不相信它给你的东西?

旁注:在与人/用户打交道时,不可变的 ID 可能更有用(人们喜欢更改用户名)。

“情景 3”

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

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

最后的想法

  • 将值包装成更复杂的类型并没有错,以便可以使用更丰富的元数据来描述/操作它。
  • 将验证集中在一个地方是一件好事,但请确保您选择了正确的地方。
  • 当逻辑位于被传递的类型中时,跨越序列化边界可能会带来挑战。
  • 如果您主要专注于信任输入,则可以使用一个简单的包装类,让被调用者知道它正在接收已验证的数据。验证发生在何处/如何发生并不重要。

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

于 2013-06-04T02:23:49.590 回答
3

以下是我能想到的两个缺点:

1) 维护开发人员可能会感到意外。他们也可能只是决定使用 CLR 类型,然后您的代码库被拆分为string username在某些地方和Username username其他地方使用的代码。

2) 您的代码可能会因调用new Username(str)和而变得混乱username.Value。这现在可能看起来不多,但是当您第 20 次键入时username.StartsWith("a"),您必须等待 IntelliSense 告诉您有问题,然后考虑并纠正它,username.Value.StartsWith("a")您可能会生气。

我相信您真正想要的是Ada 所说的“受约束的子类型”,但我自己从未使用过 Ada。在 C# 中,您能做的最好的事情就是包装器,这不太方便。

于 2013-06-04T01:22:33.683 回答
2

您已经为可以从字符串中解析的对象表示定义了一个基类。使基类中的所有成员都成为虚拟的,除此之外它看起来很好。您可以考虑稍后管理序列化、区分大小写等。

这种对象表示在基类库中使用,例如System.Uri

Uri uri = new Uri("ftp://myUrl/%2E%2E/%2E%2E");
Console.WriteLine(uri.AbsoluteUri);
Console.WriteLine(uri.PathAndQuery);

使用这个基类很容易实现对部件的轻松访问(如 System.Uri)、强类型成员、验证等。我看到的唯一缺点是 C# 中不允许多重继承,但您可能不需要无论如何继承任何其他类。

于 2013-06-04T01:11:24.400 回答
0

我会推荐另一种设计。

定义一个描述解析规则(字符串语法)的简单接口:

internal interface IParseRule
{
    bool Parse(string input, out string errorMessage);
}

定义用户名的解析规则(以及您拥有的其他规则):

internal class UserName : IParseRule
{
    public bool Parse(string input, out string errorMessage)
    {
        // TODO: Do your checks here
        if (string.IsNullOrWhiteSpace(input))
        {
            errorMessage = "User name cannot be empty or consist of white space only.";
            return false;
        }
        else
        {
            errorMessage = null;
            return true;
        }
    }
}

然后添加几个利用接口的扩展方法:

internal static class ParseRule
{
    public static bool IsValid<TRule>(this string input, bool throwError = false) where TRule : IParseRule, new()
    {
        string errorMessage;
        IParseRule rule = new TRule();

        if (rule.Parse(input, out errorMessage))
        {
            return true;
        }
        else if (throwError)
        {
            throw new FormatException(errorMessage);
        }
        else
        {
            return false;
        }
    }

    public static void CheckArg<TRule>(this string input, string paramName) where TRule : IParseRule, new()
    {
        string errorMessage;
        IParseRule rule = new TRule();

        if (!rule.Parse(input, out errorMessage))
        {
            throw new ArgumentException(errorMessage, paramName);
        }
    }

    [Conditional("DEBUG")]
    public static void DebugAssert<TRule>(this string input) where TRule : IParseRule, new()
    {
        string errorMessage;
        IParseRule rule = new TRule();
        Debug.Assert(rule.Parse(input, out errorMessage), "Malformed input: " + errorMessage);
    }
}

您现在可以编写干净的代码来验证字符串的语法:

    public void PublicApiMethod(string name)
    {
        name.CheckArg<UserName>("name");

        // TODO: Do stuff...
    }

    internal void InternalMethod(string name)
    {
        name.DebugAssert<UserName>();

        // TODO: Do stuff...
    }

    internal bool ValidateInput(string name, string email)
    {
        return name.IsValid<UserName>() && email.IsValid<Email>();
    }
于 2013-06-04T00:46:09.253 回答