首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >我为何需要使用空接口?

我为何需要使用空接口?

作者头像
张善友
发布2018-01-30 16:30:05
4470
发布2018-01-30 16:30:05
举报
文章被收录于专栏:张善友的专栏张善友的专栏

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

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

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

interface ThingBase {}; 
interface Thing1 : ThingBase 
{ 
// Operations here... 
}; 
interface Thing2 : ThingBase { 
// Operations here...
}; 

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

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

 interface ThingUser 
{ 
void putThing(ThingBase thing); 
}; 

现在使用共同的基类的目的就清楚了:我们既想要把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? 
// ... 
} 
} 

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

interface Thing1 { 
// Operations here... 
}; 
interface Thing2 { 
// Operations here... 
}; 
interface ThingUser { 
void putThing1(Thing1 thing); 
void putThing2(Thing2 thing); 
}; 

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

interface PersistentObject {}; 
interface Thing1 : PersistentObject 
{ 
// Operations here... 
}; 
interface Thing2 : PersistentObject 
{ 
// Operations here... 
}; 

显然,这种设计把持久功能放在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。这当然是一个极其糟糕的消息:你不仅要改变你的服务器中的对象的实现,还要找到并更新所有正在使用你的对象的客户,因为它们突然有了一种全新的类型。更糟糕的是,你无法让它们向后保持兼容:或者让所有客户随着服务器发生改变,或者一个客户都不改变。要想让某些客户“不升级”,这是不可能的。

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

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2005-10-22 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档