我们为什么要使用空接口

举报
福州司马懿 发表于 2021/11/19 04:15:50 2021/11/19
【摘要】 转自 http://www.cnblogs.com/shanyou/archive/2005/10/22/259674.html FxCop设计规则中的第三条提供了对空接口的检查.下面是它的描述: ...

转自 http://www.cnblogs.com/shanyou/archive/2005/10/22/259674.html

FxCop设计规则中的第三条提供了对空接口的检查.下面是它的描述:

一个接口提供了一组行为和使用契约(usage contract),任何一个类型都可以实现这个Interface, 而不需要考虑这个类型的继承层次。一个类型通过实现接口的成员而实现这个接口。一个空的接口没有定义任何成员,因此,也就没有任何契约能够被实现。

如果你的设计包含一个空的接口,并且希望一些类型实现这个接口,你很可能希望使用这个接口作为一个标记来标示一组类型。如果你只需要区分这些类型在运行时,一个更佳的解决方式是使用自定义属性(attribute)。使用有或没有一个属性或通过属性的字段(Property)去标示一组类型。如果你希望这种标示能够被使用在编译时,就只好使用空接口了。

这说明在大多数情况下,空接口都说明在设计上存在错误。这里有一个例子:

interface ThingBase {}; 
interface Thing1 : ThingBase 
{ 
    // Operations here...
}; 
interface Thing2 : ThingBase { 
    // Operations here...
};
  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

考察这个定义,我们可以观察到两个事实:
• Thing1 和Thing2 有共同的基类,因此是相关的。

• 不管Thing1和Thing2有什么共同之处,都可以在ThingBase接口中找到。
当然,只要看一看ThingBase,我们就会发现Thing1 和Thing2 根本没有共享任何操作,因为ThingBase 是空的。 假如我们是在使用面向对象模型,这种做法就显然很奇怪:在面向对象模型中,与某个对象通信的唯一途径是向它发送消息。但要发送消息,我们需要有操作。假如ThingBase没有操作,我们就无法向它发送消息,而Thing1 和Thing2 也就是不相关的,因为它们没有共同的操作。但看到Thing1 和Thing2 有共同的基类,我们就会得出这样的结论:它们是相关的,否则共同的基类就根本不会存在。到了这里,大多数程序员都会开始挠头,想知道到底在发生什么事情。
使用这样的设计的一个常见理由是,要多态地处理Thing1 和Thing2。
例如,我们可以继续先前的定义:

interface ThingUser 
{ 
    void putThing(ThingBase thing);
};
  
 
  • 1
  • 2
  • 3
  • 4

现在使用共同的基类的目的就清楚了:我们既想要把Thing1 代理、也想要把Thing2 代理传给putThing。这能否证明使用空的基接口是正当的?
要回答这个问题,我们需要思考一下在putThing 的实现中发生的事情。显然, putThing 不可能调用ThingBase 上的操作,因为在那里没有操作。这意味, putThing 必须要能做以下两件事情之一:

  1. putThing 能够记住事物的值。
  2. putThing 能够试着向下转换到Thing1 或Thing2,然后调用操作。

putThing 的伪码实现看起来可能像是这样:

void putThing(ThingBase thing) 
{
    if (is_a(Thing1, thing)) 
    {
        // Do something with Thing1...
    } else if (is_a(Thing2, thing)) {
        // Do something with Thing2...
    } else {
        // Might be a ThingBase?
        // ...
    }
}
  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

这个实现试着依次把它的参数向下转换成每种可能的值,直到它找 到参数实际的运行时类型。当然,任何一本像样的面向对象课本都会告 诉你,这是在滥用继承,并且会带来维护问题。如果你发现自己在编写像putThing 这样的操作,依赖于人为的基接口,问问你自己,你是否真的需要采用这种做法。例如,这样的设计可能更加适宜:

interface Thing1 { 
    // Operations here...
};
interface Thing2 {
    // Operations here...
};
interface ThingUser {
    void putThing1(Thing1 thing);
    void putThing2(Thing2 thing);
};
  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

在这种设计中, Thing1 和Thing2 是不相关的,而ThingUser 为每种类 型的代理提供了单独的操作。这些操作的实现不需要使用任何向下转换,而且在我们的面向对象世界里,一切都安然无恙。
下面是空的基接口的另一种常见用法:

interface PersistentObject {};
interface Thing1 : PersistentObject 
{
    // Operations here...
};
interface Thing2 : PersistentObject 
{
    // Operations here...
};
  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

显然,这种设计把持久功能放在PersistentObject 基接口中,并且要求想要拥有持久状态的对象继承PersistentObject。表面上,这是合理的:毕竟,这样使用继承是一种沿用已久的设计模式,那么,它可能有什么问题?我们发现,这种设计有这样一些问题:
• 上面的继承层次用来给 Thing1 和Thing2 增加行为。但在严格的OO 模型中,行为只能通过发送消息来调用。这引发了这样一个问题:PersistentObject 实际上该怎样着手完成它的工作;推测起来,它对Thing1 and Thing2 的实现(也就是,内部状态)有所了解,所以它可以把该状态写入数据库。但如果是这样, PersistentObject、Thing1,以及Thing2 就不能再在不同的地址空间中实现了,因为如果是那样, PersistentObject 就不再能知道Thing1 和Thing2 的状态。
换一种做法, Thing1 和Thing2 可以使用PersistentObject 提供的某种功能, 使它们的内部状态持久。但PersistentObject 没有任何操作,那么Thing1 和Thing2 实际上又该怎样去完成这件事情呢?再一次,唯一可行的做法是,在同一个地址空间中实现PersistentObject、Thing1,以及Thing2,并让它们在幕后共享实现状态,也就是说,它们不能在不同的地址空间中实现。
• 上面的继承层次把世界分成两半,一个含有持久对象,另一个含有非持久对象。这种做法有着深远的影响:

• 假定你有一个应用,它已经实现了一些非持久对象。随着时间推移,需求发生变化,你发现现在你想让部分对象持久。采用上面的设计,你无法做到这一点,除非你改变你的对象的类型,因为它们现在必须继承PersistentObject。这当然是一个极其糟糕的消息:你不仅要改变你的服务器中的对象的实现,还要找到并更新所有正在使用你的对象的客户,因为它们突然有了一种全新的类型。更糟糕的是,你无法让它们向后保持兼容:或者让所有客户随着服务器发生改变,或者一个客户都不改变。要想让某些客户“不升级”,这是不可能的。

• 这种设计不能扩展到支持多种特性。设想一下,我们有另外一些行为,对象可以继承它们,比如序列化、容错、持久,以及用搜索引擎进行搜索的能力。我们很快就会陷入多重继承的泥淖。更糟糕的是,每种可能的特性组合都会创建一种完全独立的类型层次。这意味着,你不再能编写出一些操作,一般化地对一些对象类型进行操作。例如,你不能把持久对象传到需要非持久对象的地方, 即使对象的接收者并不在乎对象的持久方面。这很快就会造成碎片化的、难以维护的类型系统。不久,你会发现,你不是在重写应用,就是获得了某种难以使用也难以维护的东西。

但愿前面的讨论成为一个警告:空接口几乎总是表明,你的应用通过所定义的接口之外的机制共享了实现状态。如果你发现自己在编写空的接口定义,你至少应该后退一步,思考一下手上的问题;其他设计可能会更加适宜,更能清晰地表达你的意图。如果无论如何你都要使用空接口,那么要注意,你几乎肯定会失去这样的能力:改变对象模型在物理的服务器进程上的分布方式,因为你无法把共享了隐藏状态的接口分置在不同的地址空间中。

评论列表

1楼 2005-10-22 10:46 microhf
这么说吧:
public interface IIdentifier { }
正如楼主所说目的就是“作为一个标记来标示一组类型”,仅此而已。如果硬要弄这样的东西

public interface IIdentifier2:IIdentifier { } 
  
 
  • 1

类说话,其实是很脑缺水,硬是自己的误用把自己搞混,要造两个标识,不就这样了

public interface IIdentifier { } 
public interface IIdentifier2 { } 
  
 
  • 1
  • 2

有什么问题呢?
至于后面楼主说的,恰是在对interface的误用中找问题。
interface和继承没有什么关系,它是实现面向对象的“多态”的一种方式,自然没有你说的那些继承和多重继承的问题。
interface是一组行为的一种契约,这种契约与对于不同的实现者(class)之间可以是没有什么关系的。对于继承来说所有的子类之间都是存在某种关系的,继承最大的问题就是误用(尤其多重继承),override看似很酷,很多时候它使对象之间关系混乱和复杂。
支持(0)反对(0)

2楼 2005-10-22 15:52 kain
在控件设计时也比较有用
支持(0)反对(0)

3楼 2005-10-23 18:46 装配脑袋
我认为空接口在运行时没有任何意义,属于不良设计。当且仅当需要编译时类型信息和编译时判定,才需要空接口。
支持(1)反对(0)

4楼 2005-10-23 23:35 microhf
同意 装配脑袋 说的
支持(0)反对(0)

5楼 2005-10-24 11:04 Klesh Wong
INamingContainer 接口
标识在 Page 对象的控件层次结构内创建新 ID 命名空间的容器控件。这仅是一个标记接口。
——-摘自MSDN

是否考虑过以上这种需求。因为需要在页面中保证控件的ID唯一性。那么会要求所有控件的子控件有唯一的ID,此是否为之契约?然而你要向IDE说明你已经做到这一点,能保证自己的子控件都有唯一的ID,是不是需要实现契约?但实现这种契约事实上是不需要任何方法调用的。所以这应该算是空接口的合理用法与存在的合理性吧。
支持(0)反对(0)

6楼[楼主] 2005-10-24 19:04 张善友
小残说的非常正确,还有IBatisNet的DataAccess框架也是一个空接口IDao.
支持(0)反对(0)

7楼 2005-10-25 09:34 砂砂
小残说的也是,我就是一直没想通INamingContainer 接口是怎么工作的:(
支持(0)反对(0)

#8楼 2006-11-01 18:59 yyww[匿名]
我看空接口也没用,就算要打标记可以通过attribute来进行
支持(0)反对(0)

9楼 2008-11-30 00:34 Jianqiang Bao
筛选文章时看到此文,我也说几句。
空接口作用大大的有。
1.IssueVision中的观察者就是空接口实现。只是为了把若干Observer打上标记
2.Memento中为了将自身方法只暴露给指定的类,一定要加一个空接口的“壳”
支持(0)反对(0)

10楼 2008-12-22 16:54 不入流程序员
iserror
支持(0)反对(0)

11楼 2013-09-05 10:00 鹤冲天
支持下!
给出这个地址,方便查阅:CA1040:避免使用空接口
https://msdn.microsoft.com/zh-cn/library/ms182128(v=vs.110).aspx
支持(0)反对(0)

文章来源: blog.csdn.net,作者:福州-司马懿,版权归原作者所有,如需转载,请联系作者。

原文链接:blog.csdn.net/chy555chy/article/details/52223309

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。