【问题标题】:Is abstracting data type (sometimes) a good idea?抽象数据类型(有时)是个好主意吗?
【发布时间】:2008-10-06 11:43:39
【问题描述】:

很多时候你有一个接口接受具有不同领域逻辑含义的相似类型参数:

public static class Database
{
   public static bool HasAccess(string userId, string documentId) { return true; }
}

现在很容易让某人键入 documentId 而不是 userId,反之亦然。可以通过抽象参数的数据类型来防止这种情况发生:

public class UserId
{
   public string Value { get; internal set; }
   public static implicit operator string(UserId id) { return id.Value; }
}

public class DocumentId
{
   public string Value { get; internal set; }
   public static implicit operator string(DocumentId id) { return id.Value; }
}

public static class Database
{
    public static bool HasAccess(UserId userId, DocumentId documentId) { return true; }
}

这样,如果你乱序输入参数,你会得到一个很好的编译器警告:

UserId e = new UserId() { Value = "a" };
DocumentId d = new DocumentId() { Value = "b" };

Database.HasAccess(d, e);

您还可以在不影响系统其余部分的情况下更改抽象类型,但这不太可能。抽象类型是获得更多类型安全性的好主意吗?

该问题与 C# 解决方案有关,但欢迎使用其他语言进行简短描述。

编辑:从字符串中删除了隐式转换并将手指指向 C# 标记。

【问题讨论】:

  • 但是 Database.HasAccess("a","b") 仍然有效......我怀疑大多数调用者会比你的包装器更有可能使用字符串,从而取消了目的。
  • 同意。我可以删除隐式字符串以进行类型转换。

标签: c# design-patterns


【解决方案1】:

我认为您回答了自己的问题 - 更好的数据完整性和验证,更好的系统

【讨论】:

    【解决方案2】:

    有趣,但我怀疑在许多情况下(特别是序列化/RPC API)这只会增加混乱/开销。另外 - 一个小的实现细节,但考虑到这种方法,我会让包装器完全不可变,而不仅仅是“内部集”不可变。

    TBH - 我可能宁愿在大部分情况下使用单元测试......有时简单就是美丽。另一个问题是,由于你有隐式运算符,它不会阻止你做更多的事情:

    string user = "fred";
    SomeMethodThatWantsADocument(user);
    

    那应该编译;隐式运算符会撤销你所有的好工作......

    【讨论】:

    • 有没有办法让它在 c++ 中表现得像 workmad3 回答的 typedef?
    • 不是AFAIK;您可以使用“使用”别名,但那只是一个别名(即不应用额外检查),并且是每个文件(来源)。
    【解决方案3】:

    这就是 typedef 在 C++ 中变得有用的地方。您可以将 UserID 和 DocumentID 作为 typedeffed 类型,因此在没有强制转换的情况下不可互换,但只需要向编译器快速说明“这应该是与其他类型不同的单独类型,即使它实际上只是键入 X'。

    【讨论】:

      【解决方案4】:

      在这种情况下,我觉得不值得。

      你已经添加了 12 行,分布在两个额外的类中。在某些语言中,您正在考虑为此管理两个新文件。 (在 C# 中不确定)。你引入了很多额外的认知负荷。每当您浏览班级列表时,这些班级就会出现;它们出现在您自动生成的文档中;它们在那里是代码库的新手在尝试学习时看到的东西,它们位于编译器的依赖关系图中等。程序员必须知道类型并在调用 HasAccess 时创建两个新对象.

      为了什么?为了防止您在检查某人是否有权访问数据库时不小心混淆了用户名和文档 ID。在正常系统中,该检查可能应该写两到三遍。 (如果你写了很多,你的数据库访问代码可能没有得到足够的重用)

      所以,我会说这是过度的航天。我的经验法则是类或类型应该封装变体行为,而不是被动数据的变体使用。

      【讨论】:

        【解决方案5】:

        是的,有时这是个好主意。但如果你太沉迷于此,你就会成为一名建筑宇航员。

        关于类型安全参数 - 它确实增加了类型安全,但许多语言在没有它的情况下也能正常工作。

        在我看来,最好的方法是把它作为一个 String 开始,然后当你发现自己重用接口时,在那时重构为更抽象的类型。

        预测未来太难了,不能浪费时间去尝试。

        【讨论】:

          【解决方案6】:

          对于您的单元测试无论如何都应该防止的事情,似乎有很多开销,至少在这种情况下是这样。

          【讨论】:

            【解决方案7】:

            您不问也不回答的是最能确定新类型是否重要的​​问题:

            1. 该系统的预计实际寿命是多少?如果答案是 2 年以上,那么您应该对数据库和用户 ID 至少有一个抽象级别。换句话说,您的数据库应该是抽象的,而您的用户和凭据应该是抽象的。然后根据抽象定义实现数据库和用户 ID。这样,如果需求发生变化,您的更改将在最需要它的地方进行。

            2. 拥有 userid 数据类型的收益和损失是什么?这个问题应该从可用性、表现力和类型安全的角度来回答。如果在可用性和表现力方面有明显的提升,那么创建的类或额外行的数量在很大程度上是无关紧要的——万岁,你赢了。让我给你一个明显损失的例子——我使用了一个类层次结构,它包含一个抽象基类和几个具体的子类型。他们没有为子类和适当的访问器提供构造函数,而是创建了一个工厂方法,该方法将 XML 字符串或流作为参数并从中构造适当的具体类。可用性的损失如此之大,以至于让这个库很痛苦——甚至他们的示例代码都散发着丢失的味道。虽然我可以构建他们提供的所有东西,但感觉令人发指并生成运行时而不是典型问题的编译时错误。

            【讨论】:

              【解决方案8】:

              虽然在一天结束时,您可能并不在意,但抽象程度越高,维护就越困难(尤其是对其他人而言)。如果在六个月内您必须开始挖掘此代码以查找或修复错误,甚至添加新功能,那么您将花费更长的时间来记住您做了什么以及为什么。如果其他人正在这样做,请乘以该时间。编写新代码时,优雅的代码总是很好,但我总是喜欢权衡维护者未来的需求。

              【讨论】:

              • 这就是我问这个问题的原因 - 我确实关心维护,也许太过分了。对我来说,抽象让事情更容易理解而不是更难。当您在代码中找到 UserId 变量时,您会立即知道它的含义,或者可以使用现代 IDE 定位 UserId 类以了解更多信息。
              • 实际上我认为“抽象”在这里可能意味着不同的东西。抽象是关于处理问题的“概括”......当简单地采用一般事物(字符串)并添加子类型来约束它时,您不会添加“抽象”。恰恰相反。
              【解决方案9】:

              这对我来说就像一个 YAGNI 问题。如果您只是因为它可能有用而这样做,那么这通常不是额外复杂性的充分理由。此外,正如其他人所指出的,这是单元测试应该捕捉到的东西。

              我试图记住的另一件事是抽象是否旨在保护程序员免受自己的伤害。是的,理论上很容易触发两个字符串参数。但严肃地说,几十年来,关注参数顺序一直是大多数语言编程的一个基本方面。这不是应该经常发生的事情,而且肯定是测试应该抓住的事情。如果这类事情是您组织中的常见问题,我会说您有更大的问题要担心。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2010-09-30
                • 1970-01-01
                • 1970-01-01
                • 2014-10-15
                • 2017-06-21
                • 1970-01-01
                • 2021-03-17
                • 1970-01-01
                相关资源
                最近更新 更多