软件测试关于面向对象的一些思考[2] 软件测试工具
关键字:面向对象 思考
比如,我实现一个 GUI 系统(或是一个 3d 世界)。需要实现一个功能——判断鼠标点选到了什么物件。这里,每个物件提供了一个方法,可以判断当前鼠标的位置有没有捕获(点到)它。
这时最简单的时候方法是:把所有可以被点选的物件都放在一个容器中,每次遍历这个容器,查看是哪一个物件捕获了鼠标。
我们并不需要可被点选的物件都是同类,只需要要求从容器中可以以统一方法访问每个元素的是否捕获住鼠标的这个判定方法。
也就是说,把对象置入容器时,只需要让置入的东西有这一个判定方法即可。了解 COM 的同学应该明白我要说什么了。对,这就是 QueryInterface 的用途。com 的 query interface 就是说,从一个对象里取到一个特定可以做某件事情的接口。通常接下来的代码会把它放在一个容器里,方便别处的代码可以干这些事情。
面向对象的本质就是让对象有多态性,把不同对象以同一特性来归组,统一处理。至于所谓继承、虚表、等等概念,只是实现的细节。
说到这里,再说一下 COM 。COM 允许 接口继承 ,但不允许接口多继承。这一点是从二进制一致性上来考虑的。
为什么没提 实现继承 的事情?因为实现继承不属于面向对象的必要因素。而且,现在来看,实现继承对软件质量来说,是有负面影响的。因为如果你改写基类的虚方法,就意味着有可能破坏基类的行为(继承角度看,基类对象是你这个对象的一部分)。往往基类的实现早于派生类,并不能了解到派生类的需求变化。这样,在不了解基类设计的前提下,冒然的实现继承都是有风险的。这不利于软件的模块化分离和组件复用。
但是接口继承又有什么意义呢?以我愚见,绝大多数情况下,同样对设计没有意义。但具体到 COM 设计本身,让每个接口都继承于 IUnknown 却是有意义的。这个意义来至于基础设施的缺乏。我指的是 GC 。在没有 GC 的环境中,AddRef 和 Release 相当于让每个对象自己来实现 RC (引用计数)的自动化管理。对于非虚拟机的原生代码,考虑到 COM 不依赖具体语言,这几乎是唯一的手段。另外 COM 还支持 apartment 的概念,甚至允许 COM 对象处于不同的机器间,这也使得 GC 实现困难。
QueryInterface 存在于每个 COM 接口中却有那么一点格格不入。它之所以存在,是因为 COM 接口指针承担了双重责任,既指出了一个抽象概念,又引用了对象的实体。但从一个具体算法来看,它只需要对一组相同的抽象概念做操作即可。但它做完操作后,很可能(但不是必须)需要把对象放入另一个不同的集合中,供其它算法操作。这个时候,就需要 QueryInterface 将其转换为另外一个接口。
但是,从概念上讲,让两个不相关的接口相互转换是不合逻辑的。本质上,其实在不相关的接口间转换做的事情等价于:从一个接口中取得对对象的引用,然后调用这个对象的方法,取到新的接口。
如果去掉了 AddRef Release (依赖 GC )以及 QueryInterface (只在需要时增加一个接口获得对象的引用),IUnknown 就什么都不剩了。那么接口继承也完全不必存在。
回头再来看程序语言。
C++ 提供了对面向对象的支持,但 C++ 所用的方法(虚表、继承、多重继承、虚继承、等等)只是一种在 C 已有的模型上,追加的一种高效的实现方式而已。它不一定是最高效的方式(虽然很少能做到更高效),也不是最灵活的方式(可以考察 Ruby )。我想,只用 C++ 写程序的人最容易犯的错误就是认为 C++ 对面向对象的支持的实现本身就是面向对象的本质。如果真的理解了面向对象,在特定需求下可以做出特定的结构来实现它。语言就已经是次要的东西了。
了解你的需求,区分我需要什么和我可以做到什么,对于设计是很重要的。好的设计在于减无可减。
你需要面向对象吗?你需要 GC 吗?你需要所有的类都有一个共同的基类吗?你需要接口可以继承吗?你为什么需要这些?