心寄笔端 附庸风雅

甘草的技术博客

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
面向接口编程(A)
前面的章节对于本篇来说,只是基础和铺垫,而且讲的很简单,因为那些很容易理解。我们从这个章节开始,用大量的代码的配合,来阐述面向接口编程。

接口的演化形式
现在我们回顾一下继承相关的知识。我们现在给出一组新的继承体系。它们是和图形相关的,我们可以假设这样的一种需求,就是我们要实现一个画图板(例如Windows的画图板),至少要能在上面绘制几个圆形和矩形。于是,我们很自然底定义了如下的classes。
class Shape    //abstract in fact
{
    
public virtual void draw();
}

class Circle : Shape
{
    
public override void draw()
    {
        
/* draw a circle on some device */
    }
}

class Rect : Shape
{
    
public override void draw()
    {
        
/* draw a Rect on some device */
    }
}
首先要说的是,我们确实是无法给出Shape类的方法draw()的实现。因为它是一个抽象的类型(不要和语言中的抽象类或abstract关键字混淆,但是它们确实有莫大的联系,但是我所谓的抽象是一种真实的抽象),我们不知道一个所谓的“图形”应该如何被画出来。这就像你让我画一个图形出来一样,我感到很为难。我画个圆圈或者是多边形,五角星,似乎都不是一个具有抽象意义的图形。

我们再回顾一下多态,下面的代码更好的诠释了多态的作用。
Array<Shape> shapes = { new Circle(), new Rect(), new Circle(), new Circle(), new Rect() };

void drawShapes()
{
    foreach(Shape shape in shapes)
    {
        shape.draw();
    }
}
这是一段伪码,但是很自然地表现除了多态的从容,foreach从容器中循环枚举出Shape类的各种不同的派生类对象,它们都多态性地调用了它们各自类型的draw()方法。而这个过程又绝没有很明显的显露出某一个具体的派生类的类型的参与。于是,我们还可以很自然地再派生出圆角矩形,或者是五角星,都让它们继承自Shape类即可,它们也可以很自然地被放进shapes容器中,而且又不会修改循环处的分毫代码,这就是我们所追求的可扩展性和“新增代码不会影响已有的代码”。

嗯,这一切都很完美,不是嘛? 然而,这真的很完美嘛?

表格(Table)是一个Shape(对象)嘛?文本(Text)是一个Shape嘛?如果我们要有若干个图层(Layer),每一个图层是一个Shape嘛?如果是,这些出现在画图板上的元素,它们很好的诠释了is-a的信念嘛?

没有,因为一个Table不是一个Shape,Text也不是Shape,但是它们也都可以被画在上面,于是我们需要进行一次重要的演化。

我们在C++的语法教材中不强调接口的概念,而是用纯抽象类来表达这一个含义,但是我更愿意用Java的interface关键字作为表达。但是我们要清楚,无论是接口,还是基类,抽象基类,多态性都是存在于这样的语法关系中的。

interface IDraw {
    
void draw();
}

class Circle implements IDraw {
    
void draw() {
        
/* draw a circle on some device */
    }
}

class Rect implements IDraw {
    
void draw() {
        
/* draw a Rect on some device */
    }
}
这次演化,似乎是没有本质改变的,特别是对于C++的编译器来说,编译出来的代码都可能没有丝毫的不同。也许很多人开始叹气了,认为这种形式上的变化根本是无所谓的,甚至就是在浪费时间。
但是我想说的是,对于设计来说,这种变化是本质的变化。因为对象的关系变化了,之前,我们说一个Circle对象也是一个Shape,它满足is-a的经典关系,但是现在,这种关系被打破了。

Can-do & Constraint
我们在前面的章节中,已经提到了has-a和is-a的对象关系了,现在,我们的重点是can-do的关系,这种关系表示约束(Constraint)。
于是,在上面的代码中,我们说Circle类实现了IDraw接口,或者说Circle对象能够完成IDraw接口所要求的行为。
void draw(IDraw d)
{
    d.draw();
}

而这个函数就更直观地表达出约束的概念了,“画可画之对象”,准确地说,这个函数更直接地完成 IDraw接口的约束语义表达:只有实现了约束的对象才可以被传入,并调用其draw()方法(注意,而并不是有draw()方法的对象都可以被传入和调用,相关问题可以对比C++09的concept)。

Array<IDraw> drawList = { new Circle(), new Rect(), new Circle(), new Table(), new Rect(), new TextArea() };

void draw()
{
    
foreach(IDraw d in drawList )
    {
        d.draw();
    }
}
我用这段伪码,明确地表示了这样的一个新情况,Circle被画到画图板中去了,Table和TextArea也可以被画到上面去了。

依然是存有怀疑,难道Table派生自Shape就有问题嘛?难道一定要弄出一个IDraw,让它看起来有道理才是合理的?
不是的。首先,如果IDraw接口没有取代Shape类,我们也要承认这样几个事情,Shape类还是带着IDraw接口的含义,尽管我们不把它抽象出来。

而且,设计没有对错之分,是不是适应需求才是最实际的评判标准。我们当前的例子还很简单,我们只涉及到了元素的绘制(draw),但是也许还有其他的问题 (新需求总是很多的),比如ALPHA混合,图层的遮挡,元素的选择,蚂蚁线的绘制,等等。例如,当我们选择了一个Rect的时候,他的四周有蚂蚁线,而我们选择了一个TextAread的时候,它的四周是带有8个调节点的边框。可是,我们不打算让蚂蚁线和边框参与其他元素的遮挡计算和ALPHA混合计算,而且它们也不参与序列化,于是,它们既不是Shape,也不应该实现IDraw接口。经验告诉我们,Shape类是不足以成为所有元素的基类的,IDraw接口也不是万能的。具体的解决方案要看需求,如何应对这些需求,我们会在后面的内容中有所涉及。





posted on 2008-07-01 15:45  甘草  阅读(1114)  评论(0编辑  收藏  举报
Baidu
Google
心寄笔端
TEST
以后我会加上Power By的,先别介意