我将如何设计一个接口,以使其清楚哪些属性可以更改其值,哪些属性将保持不变?


12

我在有关.NET属性的设计问题。

interface IX
{
    Guid Id { get; }
    bool IsInvalidated { get; }
    void Invalidate();
}

问题:

此接口具有两个只读属性,IdIsInvalidated。但是,它们本身是只读的事实本身并不能保证它们的值将保持不变。

可以说,我的意图是要清楚地表明……

  • Id 表示一个常量值(因此可以安全地缓存),而
  • IsInvalidated可能会在IX对象的生存期内更改其值(因此不应缓存)。

我如何修改interface IX以使该合同足够明确?

我自己尝试的三种解决方案:

  1. 该界面已经过精心设计。调用方法的存在Invalidate()使程序员可以推断出类似名称的属性的值IsInvalidated可能会受到它的影响。

    仅在方法和属性的命名类似的情况下,此参数才成立。

  2. 通过事件增强此接口IsInvalidatedChanged

    bool IsInvalidated { get; }
    event EventHandler IsInvalidatedChanged;

    …Changed事件的存在IsInvalidated表明该属性可能会更改其值,而事件的类似事件的缺失Id则表明该属性不会更改其值。

    我喜欢这种解决方案,但其中很多其他的东西可能根本就不会使用。

  3. IsInvalidated用以下方法替换属性IsInvalidated()

    bool IsInvalidated();

    这可能太微妙了。可以暗示每次都会重新计算一个值-如果它是一个常数,则不需要。MSDN主题“在属性和方法之间选择”对此有这样的说明:

    在以下情况下,请使用方法而不是属性。[…]每次调用操作都会返回不同的结果,即使参数没有更改。

我希望得到什么样的答案?

  • 我对解决该问题的完全不同的解决方案最感兴趣,并给出了它们如何击败我的上述尝试的解释。

  • 如果我的尝试在逻辑上有缺陷或具有尚未提及的重大缺点,以致仅剩一种解决方案(或没有解决方案),我想听听我哪里做错了。

    如果缺陷很小,并且在考虑了多个解决方案后仍然存在,请发表评论。

  • 至少,我希望获得一些反馈,以了解哪种是您首选的解决方案,以及出于何种原因。


并不IsInvalidated需要private set
詹姆斯,

2
@James:不一定,既不在接口声明中也不在实现中:class Foo : IFoo { private bool isInvalidated; public bool IsInvalidated { get { return isInvalidated; } } public void Invalidate() { isInvalidated = true; } }
stakx

接口中的@James属性与自动属性不同,即使它们使用相同的语法。
svick

我想知道:接口是否“有权力”施加这种行为?不应将此行为委托给每个实现吗?我认为,如果要强制执行该级别的操作,则应考虑创建一个抽象类,以便子类型可以从中继承,以实现给定的接口。(顺便说一句,我的理由合理吗?)
heltonbiker

@heltonbiker不幸的是,大多数语言(我知道)不允许表达对类型的此类约束,但绝对可以。这是规格问题,而不是实现问题。在我看来,所缺少的是直接用语言表达类不变式后置条件的可能性。理想情况下,例如,我们永远不必担心InvalidStateExceptions,但我不确定这在理论上是否可行。不过会很好。
proskor

Answers:


2

我更喜欢解决方案3而不是1和2。

解决方案1的问题是:如果没有Invalidate方法会怎样?假定一个接口具有IsValid返回的属性DateTime.Now > MyExpirationDate;您可能不需要SetInvalid在这里使用显式方法。如果方法影响多个属性怎么办?假定具有IsOpenIsConnected属性的连接类型–两者都受该Close方法影响。

解决方案2:如果事件的唯一目的是告知开发人员类似名称的属性可能在每次调用时返回不同的值,那么我强烈建议您这样做。您应该使界面简短明了;此外,并非所有实现都可以为您触发该事件。我IsValid将从上面重用我的示例:您必须实现一个Timer并在到达时触发一个事件MyExpirationDate。毕竟,如果该事件是您的公共界面的一部分,则该界面的用户将希望该事件能够正常工作。

话虽这么说,但这些解决方案还不错。方法或事件的存在将表明相似命名的属性可能在每次调用时返回不同的值。我要说的是,仅凭它们不足以始终传达这种含义。

解决方案3是我想要的。如aviv所述,这可能仅对C#开发人员有效。作为C#开发人员,对我来说,IsInvalidated不是属性这一事实立即传达了“不仅仅是一个简单的访问器,这里正在发生某些事情”的含义。但是,这并不适用于所有人,而且正如MainMa所指出的那样,.NET框架本身在这里并不一致。

如果您想使用解决方案3,建议您将其声明为约定,并让整个团队遵循。我相信文档也很重要。在我看来,它实际上是很简单的提示,在不断变化的价值:“ 如果该值为返回true 仍然有效 ”,“ 指示连接是否已经被打开 ”“对,如果这个对象返回true 有效的 ”。尽管在文档中更加明确,但完全没有害处。

所以我的建议是:

声明解决方案3的约定,并始终遵循它。在属性文档中明确说明属性是否具有更改的值。使用简单属性的开发人员将正确地假定它们是不变的(即仅在修改对象状态时才进行更改)。开发商遇到一个方法,像它听起来可能是一个属性(Count()Length()IsOpen())知道,成才的事情和(希望)阅读方法的文档,了解究竟是什么方法呢,它的行为方式。


这个问题收到了很好的答案,很遗憾我只能接受其中一个。我选择了您的答案,是因为它很好地总结了其他答案中提出的一些观点,并且因为它或多或少是我决定要做的。感谢所有发布答案的人!
stakx

8

有第四种解决方案:依靠文档

您怎么知道string该类是不可变的?您只是知道这一点,因为您已经阅读了MSDN文档。StringBuilder另一方面,是可变的,因为文档再次说明了这一点。

/// <summary>
/// Represents an index of an element stored in the database.
/// </summary>
private interface IX
{
    /// <summary>
    /// Gets the immutable globally unique identifier of the index, the identifier being
    /// assigned by the database when the index is created.
    /// </summary>
    Guid Id { get; }

    /// <summary>
    /// Gets a value indicating whether the index is invalidated, which means that the
    /// indexed element is no longer valid and should be reloaded from the database. The
    /// index can be invalidated by calling the <see cref="Invalidate()" /> method.
    /// </summary>
    bool IsInvalidated { get; }

    /// <summary>
    /// Invalidates the index, without forcing the indexed element to be reloaded from the
    /// database.
    /// </summary>
    void Invalidate();
}

您的第三个解决方案还不错,但是.NET Framework并不遵循它。例如:

StringBuilder.Length
DateTime.Now

是属性,但不要期望它们每次都保持不变。

在.NET Framework本身中始终使用的是:

IEnumerable<T>.Count()

是一种方法,而:

IList<T>.Length

是财产。在第一种情况下,处理工作可能需要其他工作,例如,查询数据库。这项额外的工作可能需要一些时间。就属性而言,预计将花费很短的时间:的确,长度可以在列表的生存期内更改,但实际上不需要花费任何时间即可返回它。


对于类,仅查看代码即可清楚地表明,属性值在对象的生存期内不会改变。例如,

public class Product
{
    private readonly int price;

    public Product(int price)
    {
        this.price = price;
    }

    public int Price
    {
        get
        {
            return this.price;
        }
    }
}

很明显:价格将保持不变。

遗憾的是,您不能对接口应用相同的模式,并且鉴于C#在这种情况下的表现力不足,应使用文档(包括XML文档和UML图)来缩小差距。


即使是“无额外工作”指南,也并非绝对一致地使用。例如,Lazy<T>.Value可能需要很长时间才能计算出来(第一次访问时)。
svick

1
在我看来,.NET框架是否遵循解决方案3的约定并不重要。我希望开发人员足够聪明,可以知道他们使用的是内部类型还是框架类型。不了解的开发人员不会阅读文档,因此解决方案4也无济于事。如果解决方案3是按照公司/团队惯例实施的,那么我相信其他API是否也遵循它都没关系(当然,当然会非常感激)。
enzi

4

我的2美分:

选项3:作为非C#-er,这没有什么头绪。但是,从您带来的报价来看,精通C#的程序员应该清楚这意味着某些东西。不过,您仍然应该添加有关此文档。

选项2:除非您计划将事件添加到可能更改的所有事物中,否则不要去那里。

其他选项:

  • 重命名IXMutable接口,因此很明显它可以更改其状态。您的示例中唯一不变的值是id,即使在可变对象中,通常也假定该不变值。
  • 不提。接口不像我们希望的那样擅长描述行为。在某种程度上,这是因为该语言实际上并不能让您描述诸如“该方法将始终为特定对象返回相同的值”之类的内容,或“使用参数x <0调用此方法将引发异常”之类的事情。
  • 除了有一种机制可以扩展语言并提供有关构造的更精确信息- 注释/属性。他们有时需要做一些工作,但是允许您指定关于方法的任意复杂的事情。找到或发明一个注释,指出“该方法/属性的值可以在对象的整个生命周期中改变”,然后将其添加到方法中。您可能需要花一些技巧来获取实现方法以“继承”它,并且用户可能需要阅读该注释的文档,但这将是一个使您能够准确地说出您想说的内容而不是提示的工具。在它。

3

另一个选择是拆分接口:假设在调用的Invalidate()每次调用后IsInvalidated应返回相同的值true,似乎没有理由调用与被调用IsInvalidated相同的部分Invalidate()

因此,我建议您要么检查是否无效,要么造成它,但似乎两者都不合理。因此,有意义的是提供一个包含对Invalidate()第一部分进行操作的接口,以及提供另一个用于检查(IsInvalidated)接口的接口。从第一个接口的签名很明显,它将导致实例失效,因此剩下的问题(对于第二个接口)的确很笼统:如何指定给定类型是否为不可变的

interface Invalidatable
{
    bool IsInvalidated { get; }
}

我知道,这不能直接回答问题,但至少可以将其简化为如何标记某些类型不可变的问题,这是一个常见且可以理解的问题。例如,假设所有类型都是可变的,除非另有定义,否则您可以得出结论,第二个接口(IsInvalidated)是可变的,因此可以IsInvalidated不时更改值。另一方面,假设第一个接口看起来像这样(伪语法):

[Immutable]
interface IX
{
    Guid Id { get; }
    void Invalidate();
}

由于标记为不可变,因此您将知道 ID不会更改。当然,调用invalidate将导致实例状态更改,但是通过此接口将无法观察到此更改。


不错的主意!但是,我认为[Immutable]只要对接口的标记仅包含引起突变的方法,就标记接口是错误的。我将Invalidate()方法移至Invalidatable界面。
stakx

1
@stakx我不同意。:)我认为您混淆了不变性纯度(没有副作用)的概念。实际上,从提出的接口IX的角度来看,根本没有可观察到的突变。每当调用时Id,每次都会获得相同的值。同样适用于Invalidate()(您一无所获,因为它的返回类型是void)。因此,类型是不可变的。当然,您会有副作用,但是您将无法通过界面观察到它们IX,因此它们不会对的客户端产生任何影响IX
proskor

好,我们可以同意这IX是一成不变的。而且它们是副作用;它们只是分布在两个分离的接口之间,因此客户端不需要关心(对于IX),或者很明显的副作用(对于Invalidatable)。但是,为什么不移动InvalidateInvalidatable?这将使它变得IX不可变纯净,并且Invalidatable不会变得更加可变,也不会变得更加不纯正(因为这两者已经存在)。
stakx

(我知道在现实世界中,接口拆分的性质可能更多地取决于实际用例,而不是技术问题,但是我试图在这里完全理解您的推理。)
stakx

1
@stakx首先,您询问了进一步的可能性以某种方式使界面的语义更加明确。我的想法是将问题简化为不变性,这需要拆分接口。但是,不仅是使一个接口不变所需的分裂,它也会使重要的意义,因为没有理由调用都Invalidate()IsInvalidated由接口相同消费者。如果调用Invalidate(),您应该知道它已失效,那么为什么要检查它呢?因此,您可以提供两个不同的接口以实现不同的目的。
proskor
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.