面向接口编程(B)
约束的理解是一个很辩证的问题,我觉得我们需要一个好的例子才能把这个事情论述清楚。我很喜欢鸟儿在天上飞这个例子,因为它和大自然紧紧的联系了起来。
{
fly() { /* some code make birds fly on the sky */ }
}
class Dove : Bird
{
fly() { /* some code make a dove fly on the sky */ }
}
class Penguin : Bird
{
fly() { /* fly, fly, Oh, could I fly? */ }
}
在这段代码中,有一个基类,class Bird,鸟类,然后派生类是Dove(鸽子),和Penguin(企鹅)。简而言之,这三个类描述了这样一个世界,“但凡是个鸟就能飞 ”。哦,世界还是我们所认识的世界嘛?不,企鹅都能飞了?!
问题的关键就在于:“是鸟就一定能飞嘛?”
显然不是,这个世界除了企鹅外,还有许多鸟类没有飞行的能力。也就是说鸟类(Bird)与飞行(fly())并没有一种必然的关系的。或许,我们不该把 fly()方法设计到Bird类中去。于是,我们采用一个叫做IFly的接口,根据can-do的对象关系。我们可以称这个接口的实现类为能飞的事物。
{
}
interface IFly
{
fly() { /* some code make something been in the sky */ }
}
class Dove : Bird, IFly
{
fly() { /* some code make a dove been in the sky */ }
}
class Penguin : Bird /* don't implements IFly */
{
}
这就是我们重新刻画的世界。
并且,在这之后,我们发现这样的世界(设计)很好,我们可以很简单地让一些不是鸟类的动物也飞了起来。
{
fly() { /* a butterfly flies */ }
}
嗯,这次演化,和Shape类演化到IDraw接口,具有相似的原因。
一些Java,或者C#的教科书,往往会这样写:“Java不支持多继承,但是支持实现多个接口... ...”,我以为这样的话本身没有问题,但是许多初学者会误以为实现多个接口也能完成多继承的功能。而从设计的角度来说,用聚合来代替继承才是一种选择, 这个是我们前面说过的,而实现多接口并不是完成这种对象关系表达的合理途径。因为接口的语义往往是can-do,而且需要强调的是,这是一种约束的语义。 而继承表达的是is-a的关系。继承的对象关系使得派生类传承了一种(行为)能力。而,接口没有。
我们想想,是IFly使得一只鸟儿飞上了天嘛?不,它没有赋予鸟儿这种行为能力。而真正完成飞行能力的是实现代码本身,而并不是“实现接口”这一举动。
想飞?这很容易。
许多动态的教本语言,比如Ruby,Python,JavaScript,它们只要拥有了fly这样的方法,就可以被调用了,于是,无论有没有实现IFly接口,对象都起飞了。所以说,实现接口并没有赋予对象以能力,而是约束对象,或者说要求对象具有某种能力。
一些语言,如果只支持单继承,比如Ruby,如果想聚合某种功能,可以通过Mixins来完成这种功能的扩展。
end
module Action
def fly
end
end
class Dove < Bird
include Action
end
dove = Dove . new
dove . fly
接口与应用
前面我们所讲的东西足够理论了,但是实际的软件中,大量用到面向接口编程,而没有我们这么多推演和辩证的过程。因为接口有许多重要的性质。
例如,COM的出现,可以解决DLL Hell的问题。结果,COM的开发者总是要定义出许多的COM接口。而它们的实现反而并不重要了。
接口的使用,会有许多种原因,有时候仅仅就是为了做到“接口和实现分离”。而这给软件工程带来了许多新话题。而相当多的设计模式都是以接口编程为基础的, 经典的设计模式的实现,往往就是一个精彩的接口展示。而又不仅仅是模式本身,还有些原则,比如DIP,往往是采用接口编程的方式。当然了DIP还有很多争 论,我们就不讨论它本身的问题了。
这一节,实际上是从反面论证了接口的约束性。