我为何需要使用空接口?

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

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

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏大数据文摘

将Python和R整合进一个数据分析流程

2998
来自专栏Python小屋

Python代码优化之in关键字

如果经常需要测试一个序列中是否包含某个元素,最好使用字典或集合,尽量不使用列表。 import random import time x_list = list...

2738
来自专栏程序员互动联盟

抓包工具Wireshark过滤规则实践第一篇

引子 现在从网上看到的一些wireshark过滤规则的介绍,都是比较老一点的,新版本的语法好像有所变化,所以在这里写一篇基于最新的1.12版本的wireshar...

4349
来自专栏LuckQI

Java核心技术讲解学习

1252
来自专栏java一日一条

Java 理论与实践: 正确使用 Volatile 变量

Java 语言中的 Volatile 变量可以被看作是一种 “程度较轻的 synchronized”;与 synchronized 块相比,volatile 变...

722
来自专栏小程序·云开发专栏

小程序·云开发的云函数路由高级玩法

在掘金开发者大会上,在推荐实践那里,我有提到一种云函数的用法,我们可以将相同的一些操作。

8.7K12
来自专栏决胜机器学习

有趣的算法(十) ——归并排序思想解决大量用户数据清洗

有趣的算法(十)——归并排序思想解决用户数据清洗 (原创内容,转载请注明来源,谢谢) 一、问题阐述 近期工作中接触到一个很有趣的算法,在此进行分享。 当前有...

3569
来自专栏python3

python 装饰器构造优酷视频登录程序

912
来自专栏程序员叨叨叨

听说你们家的NotifyDataSetChanged不起作用了

前几天,公司项目准备上线,就在前一晚,出现了一个BUG:主页界面刷新无效。千钧一发之际,用了一个笨方法,每次刷新的时候重新setAdapter一下算是实现了基本...

892
来自专栏CSDN技术头条

Java性能调优的11个实用技巧

大多数开发人员认为性能优化是个比较复杂的问题,需要大量的经验和知识。是的,这并不没有错。诚然,优化应用程序以获得最好的性能并不是一件容易的事情,但这并不意味着你...

1927

扫码关注云+社区

领取腾讯云代金券