标记界面的目的是什么?
标记界面的目的是什么?
根据“米奇小麦”的反应,这是一个切线。
一般来说,任何时候我看到人们引用框架devise的指导方针,我总是想提一下:
大多数情况下,您通常应该忽略框架devise指南。
这不是因为框架devise指南的任何问题。 我认为.NET框架是一个梦幻般的类库。 框架devise指南中有许多梦幻般的东西。
但是,devise准则并不适用于大多数程序员编写的大部分代码。 他们的目的是创build一个由数百万开发人员使用的大型框架,而不是让图书馆的写作更有效率。
其中的很多build议可以指导您做以下事情:
- 可能不是最直接的实现方式
- 可能会导致额外的代码重复
- 可能会有额外的运行时间
.net框架很大,真的很大。 这是如此之大,以至于认为任何人对其各个方面都有详细的了解是绝对不合理的。 事实上,假定大多数程序员经常遇到他们以前从未使用过的框架的一部分会更安全。
在这种情况下,APIdevise者的主要目标是:
- 保持与框架的其他部分保持一致
- 消除API表面区域中的不必要的复杂性
框架devise准则推动开发人员创build实现这些目标的代码。
这意味着要避免使用inheritance层,即使这意味着重复代码,或者将所有的exception抛出代码推送到“入口点”而不是使用共享助手(这样堆栈跟踪在debugging器中更有意义)其他类似的东西。
这些准则build议使用属性而不是标记接口的主要原因是因为删除标记接口使得类库的inheritance结构更加平易近人。 与具有15个types和2个层次结构的类图相比,具有30个types和6个inheritance层次的类图是非常艰巨的。
如果真的有数以百万计的开发人员使用您的API,或者您的代码库真的很大(比如超过100K LOC),那么按照这些指导方针可以提供很多帮助。
如果500万开发人员花15分钟学习API,而不是花60分钟学习,那么结果是净节省了428万年。 这是很多时间。
但是,大多数项目不涉及数百万开发人员,或者100K + LOC。 在一个典型的项目中,如果有4个开发人员和5万个左右的开发人员,那么这个假设是非常不同的。 团队中的开发人员将更好地理解代码的工作原理。 这意味着对于快速生成高质量代码进行优化以及减less错误数量和进行更改所需的工作量更有意义。
花费1周时间开发与.net框架一致的代码,8小时编写易于更改且缺陷less的代码可能导致:
- 后期项目
- 降低奖金
- 增加错误数量
- 在办公室度过更多的时间,而在海滩喝玛格丽塔酒的时间则更less。
没有4,999,999其他开发人员来吸收成本通常是不值得的。
例如,testing标记接口归结为一个“is”expression式,并且导致查找属性的代码更less。
所以我的build议是:
- 如果您正在开发用于广泛传播消费的类库(或UI小部件),请遵循框架指导原则。
- 如果您的项目中有超过10万个LOC,请考虑采用其中的一些
- 否则完全忽略它们。
标记接口用于将类的能力标记为在运行时实现特定的接口。
界面devise和.NETtypesdevise指南 – 界面devise不鼓励在C#中使用标记接口来支持使用属性,但正如@Jay Bazuzi所指出的,检查标记接口比属性更容易: o is I
所以,而不是这个:
public interface IFooAssignable {} public class FooAssignableAttribute : IFooAssignable { ... }
.NET的指导方针build议你这样做:
public class FooAssignableAttribute : Attribute { ... } [FooAssignable] public class Foo { ... }
既然所有其他答案都表示“应该避免”,那么解释为什么是有用的。
首先,为什么使用标记接口:它们的存在是为了允许使用实现它的对象的代码检查它们是否实现了所述接口,如果它实现了不同的对象。
这种方法的问题是它打破封装。 目标本身现在间接地控制着它如何在外部使用。 此外,它具有将要使用的系统的知识。通过应用标记接口,类定义表明它希望被用于检查标记存在的地方。 它隐含了它所使用的环境的知识,并试图定义它应该如何使用。 这与封装的想法是背道而驰的,因为它具有完全存在于其范围之外的部分系统的实现的知识。
在实践中,这降低了可移植性和可重用性。 如果类在不同的应用程序中被重复使用,接口也需要被复制,并且在新环境中可能没有任何意义,这使得它完全是多余的。
因此,“标记”是关于类的元数据。 这个元数据不是由类本身使用的,只对外部客户端代码有意义,所以它可以以某种方式处理对象。 因为它只对客户端代码有意义,所以元数据应该在客户端代码中,而不是在类API中。
“标记接口”和普通接口的不同之处在于,具有方法的接口告诉外界如何使用它,而空接口意味着告诉外界应该如何使用它。
标记接口只是一个空的接口。 一个类将实现这个接口作为元数据被用于某种原因。 在C#中,你会更常用的属性来标记一个类,因为你使用其他语言的标记接口的原因相同。
当语言不支持歧视联盟types时,标记接口有时可能是一个必要的罪恶。
假设你想要定义一个方法,这个方法的types必须是A,B或C中的一个。在许多function优先的语言(比如F# )中,这样的types可以完全定义为:
type Arg = | AArg of A | BArg of B | CArg of C
但是,在C#等OO第一语言中,这是不可能的。 在这里实现类似的唯一方法是定义接口IArg并用它“标记”A,B和C.
当然,通过简单地接受types“对象”作为参数,你可以避免使用标记接口,但是你会失去performance力和某种程度的types安全性。
歧视的工会types是非常有用的,已经存在于function语言至less30年。 奇怪的是,直到今天,所有主stream的面向对象语言都忽略了这个特性 – 尽pipe它本质上与函数式编程没有任何关系,但属于types系统。
一个标记接口允许一个类被标记,将被应用于所有的后代类。 “纯”标记接口不会定义或inheritance任何东西; 一个更有用的标记接口types可能是“inheritance”另一个接口但没有定义新成员的接口。 例如,如果有一个接口“IReadableFoo”,可能还会定义一个接口“IImmutableFoo”,它的行为就像一个“Foo”,但是可以保证使用它的任何人都不会改变它的值。 接受IImmutableFoo的例程将能够像使用IReadableFoo一样使用它,但例程只接受声明为实现IImmutableFoo的类。
我不能想象“纯”标记接口的很多用途。 唯一我能想到的是,如果EqualityComparer(T).Default将返回Object.Equals的任何实现IDoNotUseEqualityComparer的types,即使types也实现了IEqualityComparer。 这将允许在不违反Liskovreplace原则的情况下拥有不可变的不可变types:如果types封装了与平等testing有关的所有方法,则派生types可以添加额外字段并使它们可变,但是这样的字段的变异不会被修改,使用任何基本types的方法都不可见。 有一个未被封装的不可变类,或者避免使用EqualityComparer.Default或者信任派生类不要实现IEqualityComparer,但是实现了IEqualityComparer的派生类可以看作是一个可变类,类对象。
这两个扩展方法将解决大多数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) { //... }
正如斯科特所说,我没有看到如何build立一个API的第一个模式比第二个模式要长5倍。
标记是空的界面。 标记是在那里或不是。
Foo级:IConfidential
这里我们把Foo标记为保密。 没有实际的附加属性或属性要求。