标记界面的目的是什么?


Answers:


77

根据“ Mitch Wheat”的回答,这有点切线。

通常,每当我看到人们引用框架设计准则时,我总是喜欢提到:

通常,大多数时候您通常应该忽略框架设计准则。

这不是因为框架设计准则存在任何问题。我认为.NET框架是一个很棒的类库。框架设计指南中有很多奇妙的地方。

但是,设计指南不适用于大多数程序员编写的大多数代码。他们的目的是要创建一个供数百万开发人员使用的大型框架,而不是使库编写更加高效。

其中的许多建议可以指导您进行以下操作:

  1. 可能不是实现某些东西的最直接方法
  2. 可能导致额外的代码重复
  3. 可能会有额外的运行时开销

.net框架很大,真的很大。它是如此之大,以至于假设任何人都具有关于它的各个方面的详细知识绝对是不合理的。实际上,假设大多数程序员经常遇到他们以前从未使用过的框架部分,要安全得多。

在这种情况下,API设计人员的主要目标是:

  1. 使事情与框架的其余部分保持一致
  2. 消除API表面积中不必要的复杂性

框架设计指南要求开发人员创建实现这些目标的代码。

这意味着要做一些事情,例如避免继承层次,即使这意味着重复代码,或者将所有抛出异常的代码推送到“入口点”,而不是使用共享的帮助程序(这样堆栈跟踪在调试器中更有意义),还有很多事情其他类似的东西。

这些准则建议使用属性而不是标记器接口的主要原因是因为删除标记器接口使类库的继承结构更容易接近。与具有15种类型和2层层次结构的类图相比,具有30种类型和6层继承层次结构的类图非常艰巨。

如果确实有成千上万的开发人员在使用您的API,或者您的代码库很大(例如超过10万个LOC),那么遵循这些准则将大有帮助。

如果500万开发人员花15分钟学习API,而不是花60分钟学习API,那么结果是净节省了428个人年。那是很多时间。

但是,大多数项目并不涉及数百万开发人员或10万多个LOC。在一个典型的项目中,假设有4个开发人员,大约有5万个位置,这组假设有很大不同。团队中的开发人员将对代码的工作原理有更好的了解。这意味着优化以快速生成高质量代码,减少错误数量和进行更改所需的精力更加有意义。

花1周的时间开发与.net框架一致的代码,而花费8个小时编写易于更改且错误少的代码可能会导致:

  1. 后期项目
  2. 较低的奖金
  3. 错误数量增加
  4. 在办公室花费的时间更多,而在海滩上喝玛格丽塔酒的时间更少。

没有4,999,999个其他开发人员来承担成本通常是不值得的。

例如,测试标记接口可归结为单个“ is”表达式,从而导致查找属性的代码更少。

所以我的建议是:

  1. 如果您要开发旨在广泛使用的类库(或UI小部件),请认真遵循框架准则。
  2. 如果您的项目中的LOC超过10万,请考虑采用其中一些
  3. 否则,请完全忽略它们。

12
我个人认为,我以后将需要使用的任何编写为库的代码。我真的不在乎消耗是否广泛-遵循准则可以提高一致性,并在多年后需要查看代码并理解时减少意外...
Reed Copsey 2010年

16
我并不是说指导方针不好。我的意思是,它们应该有所不同,具体取决于您的代码库的大小以及您拥有的用户数量。许多设计指南都是基于诸如binaryg二进制可比性之类的,它对于少数项目所使用的“内部”库并不像BCL这样重要。其他准则,例如与可用性相关的准则,几乎总是很重要的。在道德上不要对指南过于虔诚,特别是在小型项目上。
Scott Wisniewski 2010年

6
+1-尚未完全回答OP的问题-MI的目的-但是仍然很有帮助。
bzarah 2011年

5
@ScottWisniewski:我认为您缺少要点。框架准则仅不适用于大型项目,而适用于中小型项目。当您始终尝试将它们应用到Hello-World程序时,它们会变得过多。例如,无论应用程序大小如何,将接口限制为5种方法始终是一个很好的经验法则。您还想念的另一件事是,今天的小型应用程序可能会成为明天的大型应用程序。因此,最好在构架时考虑到适用于大型应用程序的良好原则,以便在需要扩展时不必重新编写大量代码。
Phil

2
我不太明白遵循(大多数)设计准则将如何导致一个耗时1周的8小时项目。例如:命名virtual protected模板方法DoSomethingCore而不是命名DoSomething并不需要太多的工作,您可以清楚地知道这是模板方法... IMNSHO,编写应用程序而不考虑API(But.. I'm not a framework developer, I don't care about my API!)的人员正是那些编写大量重复的(以及未记录且通常不可读的代码),反之亦然。
Laoujin 2015年

44

标记接口用于在运行时将类的功能标记为实现特定接口。

界面设计.NET类型设计准则-界面设计劝阻赞成在C#中使用属性的使用的标记接口,但作为@Jay Bazuzi指出,更容易检查比属性标记的接口:o is I

所以代替这个:

public interface IFooAssignable {} 

public class FooAssignableAttribute : IFooAssignable 
{
    ...
}

.NET准则建议您执行以下操作:

public class FooAssignableAttribute : Attribute 
{
    ...
}

[FooAssignable]
public class Foo 
{    
   ...
} 

26
另外,我们可以将泛型与标记接口完全结合使用,但不能与属性一起使用。
Jordão酒店

18
尽管我喜欢属性以及从声明的角度看它们的外观,但它们在运行时并不是一等公民,需要大量的相对较低水平的管道来使用。
Jesse C. Slicer 2010年

4
@Jordão-正是我的想法。举例来说,如果我想抽象数据库访问代码(比如说Linq to Sql),那么拥有一个通用的接口将使它变得更加容易。实际上,我认为不可能用属性编写这种抽象,因为您不能转换为属性,也不能在泛型中使用它们。我想您可以使用一个空的基类,其他所有类都派生自该空的基类,但这与拥有一个空的接口多少有些相同。另外,如果您以后意识到需要共享功能,则该机制已经到位。
tandrewnichols 2012年

23

由于其他所有回答都表明“应避免使用它们”,因此对原因进行解释将很有用。

首先,为什么使用标记器接口:它们的存在是为了允许正在使用实现该接口的对象的代码检查它们是否实现了所述接口,如果使用,则将其视为不同。

这种方法的问题是它破坏了封装。现在,对象本身可以间接控制如何在外部使用它。此外,它了解将要使用的系统。通过应用标记接口,类定义表明它希望在检查标记是否存在的地方使用。它具有使用环境的隐式知识,并正在尝试定义应如何使用它。这与封装的概念背道而驰,因为它知道完全存在于其自身范围之外的系统部分的实现知识。

在实践上,这降低了可移植性和可重用性。如果该类在其他应用程序中重复使用,则该接口也需要复制,并且在新环境中可能没有任何意义,从而使其完全冗余。

这样,“标记”是有关类的元数据。该元数据不被类本身使用,仅对(某些!)外部客户端代码有意义,因此它可以某种方式处理对象。因为元数据仅对客户端代码有意义,所以元数据应在客户端代码中,而不在类API中。

一个“标记接口”和普通接口之间的区别是,方法的接口告诉外面的世界怎么可以使用,而空的接口意味着它告诉外面的世界如何应该被使用。


1
任何接口的主要目的是区分承诺遵守与该接口相关联的契约的类和不遵守该契约的类。虽然接口还负责提供履行合同所需的任何成员的调用签名,但由合同而不是成员来决定是否应由特定的类来实现特定的接口。如果合同IConstructableFromString<T>规定某类T只有IConstructableFromString<T>在具有静态成员的情况下才可以实现...
supercat

... public static T ProduceFromString(String params);,接口的伴随类可以提供一种方法public static T ProduceFromString<T>(String params) where T:IConstructableFromString<T>; 如果客户端代码具有类似的方法T[] MakeManyThings<T>() where T:IConstructableFromString<T>,则可以定义可以与客户端代码一起使用的新类型,而不必修改客户端代码来处理它们。如果元数据在客户端代码中,则无法创建供现有客户端使用的新类型。
2015年

但是与T使用它的类之间的协定是IConstructableFromString<T>您在接口中具有描述某些行为的方法的地方,因此它不是标记接口。
Tom B

该类必须具有的静态方法不是接口的一部分。接口中的静态成员由接口本身实现;接口无法在实现类中引用静态成员。
supercat

一种方法可以使用反射来确定泛型类型是否碰巧具有特定的静态方法,如果存在则执行该方法,但是ProduceFromString上面示例中搜索和执行该静态方法的实际过程不会涉及以任何方式使用该接口,但该接口将用作标记,以指示应期望哪些类实现必要的功能。
超级猫

8

当语言不支持区分的联合类型时,标记接口有时可能是必不可少的。

假设您想定义一个方法,该方法期望一个参数的类型必须恰好是A,B或C中的一个。在许多功能优先的语言(如F#)中,可以将此类明确定义为:

type Arg = 
    | AArg of A 
    | BArg of B 
    | CArg of C

但是,在诸如C#这样的面向对象的语言中,这是不可能的。在这里实现类似目的的唯一方法是定义接口IArg并使用它来“标记” A,B和C。

当然,您可以通过简单地接受类型“ object”作为参数来避免使用标记接口,但是这样会失去表达能力和某种程度的类型安全性。

区分工会类型非常有用,并且在功能语言中已经存在了至少30年。奇怪的是,直到今天,所有主流的OO语言都忽略了此功能-尽管它实际上与函数式编程无关,但属于类型系统。


值得注意的是,因为a Foo<T>将为每种类型提供一组单独的静态字段T,所以不难让泛型类包含包含用于处理a的委托的静态字段T,并用处理该类所包含的每种类型的函数预先填充这些字段应该一起工作。对类型使用通用接口约束T将在编译器时检查所提供的类型至少声称是有效的,即使它不能确保它实际上是有效的。
超级猫

6

标记器接口只是一个空的接口。类出于某种原因会将此接口实现为要使用的元数据。在C#中,出于与使用其他语言的标记接口相同的原因,您通常会使用属性来标记类。


4

标记器接口允许以将应用于所有后代类的方式标记类。“纯”标记接口不会定义或继承任何东西;标记接口的一种更有用的类型可能是“继承”另一个接口但未定义新成员的接口。例如,如果有一个接口“ IReadableFoo”,则可能还会定义一个接口“ IImmutableFoo”,该接口的行为类似于“ Foo”,但会向使用它的任何人保证不会改变其值。接受IImmutableFoo的例程将能够像使用IReadableFoo一样使用它,但是该例程将仅接受声明为实现IImmutableFoo的类。

我想不出“纯”标记界面的全部用途。我唯一想到的就是EqualityComparer(of T).Default是否会为实现IDoNotUseEqualityComparer的任何类型返回Object.Equals,即使该类型也实现了IEqualityComparer。这将允许一个具有未密封的不可变类型而不违反Liskov替换原理:如果该类型密封了所有与相等性测试有关的方法,则派生类型可以添加其他字段并使它们可变,但是此类字段的突变不会使用任何基本类型的方法均不可见。拥有一个未密封的不可变类并避免使用任何EqualityComparer可能并不可怕。Default或信任派生类不实现IEqualityComparer,


4

这两种扩展方法将解决Scott断言的大多数问题,即标记属性优于属性接口:

public static bool HasAttribute<T>(this ICustomAttributeProvider self)
    where T : Attribute
{
    return self.GetCustomAttributes(true).Any(o => o is T);
}

public static bool HasAttribute<T>(this object self)
    where T : Attribute
{
    return self != null && self.GetType().HasAttribute<T>()
}

现在您有了:

if (o.HasAttribute<FooAssignableAttribute>())
{
    //...
}

与:

if (o is IFooAssignable)
{
    //...
}

正如Scott所言,我看不出第一种模式与第二种模式相比,构建API所花费的时间要长5倍。


1
仍然没有泛型。
伊恩·坎普

0

标记是空接口。标记存在或不存在。

Foo类别:IConfidential

在这里,我们将Foo标记为机密。不需要实际的其他属性或属性。


0

标记接口是一个总的空白接口,没有主体/数据成员/实现。
一个类在需要时实现标记接口,只是为了“ 标记 ”;表示它告诉JVM特定的类是出于克隆目的,因此允许其克隆。这个特殊的类是序列化其对象,因此请允许其对象进行序列化。


0

标记接口实际上只是一种用OO语言编写的程序。除标记接口外,接口定义了实现者和使用者之间的契约,因为标记接口仅定义了自身。因此,标记接口马上就成为接口的基本目的失败了。

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.