面向对象理论(8)-Granularity

来源:互联网 发布:淘宝平台入驻流程 编辑:程序博客网 时间:2024/05/22 13:10

对象的粒度
粒度的变化
对象关系的演化我们依然用Button作为例子来说明对象关系中粒度的变化。我们在ImageButton的OnDraw方法中加入了许多的代码,这些代码完成了一个按钮的绘制工作,可能要绘制按钮的3D边框,或者表明的渐变色,以及上面的文本,比如OK或者Cancel,此外还有一个Icon,这里我们就认定它为一个 Image。

class ImageButton : public Button
{
public:
    
void OnDraw()
    {
        
/* code for draw button frame */
        
// ...
        /* code for draw button text */
        
// ...
        /* code for draw button image */
        
// ...
    }
};

这段代码展示了一个ImageButton是如何显示自己的。但是事情还没有结束呢,现在我们要设计一个新的控件,一个ImageLabel,也就是不仅仅有文本的标签,它也想要一张图片来装点自己。

class ImageLabel : public Label
{
public:
    
void OnDraw()
    {
        
/* code for draw Label frame */
        
// ...
        /* code for draw Label text */
        
// ...
        /* code for draw Label image */
        
// ...
    }
};


OK,我们观察每一处变化,并假定Draw一个Image是一个复杂的过程实现。但是无论是在Button上画一个Image,还是在Lable上画一个Image,它们都(应该)是相同的逻辑(比如以Win32GDI的程序为例,在Button的DC画,与在Label的DC上画,它们的逻辑几乎是完全是一回事情)。那么,为什么不把这样的绘制行为进行一下封装呢?

class ImageDrawing
{
public:
    
void DrawImage()
    {
        
/* draw an image */
    }
};

然后,是两段很重要的代码:

class ImageButton : public Button, protected ImageDrawing
{
public:
    
void OnDraw()
    {
         
/* code for painting button and frame */
         
// ...
         DrawImage(); //call ImageDrawing::DrawImage()
    } 
};

class ImageLabel : public Label, protected ImageDrawing
{
public:
    
void OnDraw()
    {
         
/* code for painting lebel text and frame */
         
// ...
         DrawImage(); //call ImageDrawing::DrawImage()
    } 
};

于是,我们把绘制Image的逻辑封装到了ImageDrawing类中,而用继承的方式,把这种功能集成了进来。需要有两点说明:第一是这段代码限于在C++内,Java和C#很难用类似的代码形式表达这种关系,但是可以求助于聚合。第二,所以封装为类,而不是函数(方法),是因为类对象更容易维护状态。

保护继承和Implement-with
在上面的代码中,我们重点描述了对象关系的演化,而演化的结果就是这种关系,implement-with,以...实现(用...实现)。那么这种关系更易于用C++的代码进行描述,或者采用Ruby的Mixins等语法特性。那么这是在讲述了has-a,is-a,can-do对象关系后的又一种关系。

To be, or not to be: that is the question.

----Shakespeare
为什么要用保护继承呢?那么我们来看下面的代码。下面的代码用最直接的方式表达了is-a的关系,但是...
Button* button = new ImageButton();   //OK
ImageDrawing* d = new ImageButton();  //Compile Error.

OK,这就是我们想要的结果,我们不想接受那样的事实,一个ImageButton也是一个“图片绘制”?我觉得用自然的语言表达得如此尴尬。

然而,我们还可以将ImageDrawing类的构造函数标记为Protected的,这样,这个类就真的只能用于保护继承了。于是,这种关系又一次被稳固了。

对象的正交关系
对象之间的关系,在功能上可能存在正交性,而我们要尽量地把这种正交性体现出来,于是我们就要重新界定对象的粒度。我们借用Java的Thread类,和Runnable接口作为例子的原型,记住,只是以其为原型,我所讲述的并非是Java的Thread class。

class Thread
{
    
void start(Runnable run);
}

interface Runnable
{
    
void run();
}

我再次请大家注意的是,我的Thread类并没用实现Runnable类,因为我认为那并不必要,事实上,我们也不鼓励Java的开发人员派生Thread,而是鼓励他们实现Runnable接口。

(new Thread(new Runnable() {
               
public void run() {}
            })).start();

这样的使用方式(除了代码风格外),才是我们鼓励的。但是由于我们只讲必要性,而不涉及到具体使用的方便性,所以我的Thread类没有实现Runnable接口。

那么我们思考线程对象和Runnable实现类的真实含义,一个线程对象,也许维护了OS的线程内核对象(可能是模拟的,比如Ruby的FakeThread)。但是线程对象本身可以加载什么方法,或者说在一个线程上做什么事情,那是Runnable实现类要如何描述的问题,这两个对象有着关系,但是在功能上确实正交的。于是,我们把它们分成两个对象的粒度。
到此,如果我们有一个Timer类,或者一个叫做Fiber的类。我们只是想把我们要做的事情加载到Timer事件中,或者是Fiber纤程上,我们就会更好地意识到Runnable接口和Thread类的正交性。

抽象粒度
我们考虑流的概念,这是一个抽象的概念,比如文件流,Socket的通信,管道中的数据,都被抽象为了流。但是我们先想想MFC的CFile类,这个类主要完成了哪些事情?打开和关闭文件,读写文件。但是我们发现,文件被打开后,对其进行读写的操作,可以正交于打开和关闭这些操作,更关键的是,如果我们把Socket,管道,串口设备等都看作是文件的时候。我们依然要读写它们。而这个操作尽管在实现上有着千差万别,但是对象的方法,也就是其表现行为是一致的(准确说是相似的,一致只是一种理想的情况),都是Read和Write。于是我们就有把这种操作抽象出来的理由了,那么今天的流的概念,就源于此处的抽象。

File file = new File(fileName);
FileInputStream fis 
= new FileInputStream(file);


迭代器也是一种抽象的概念,它的对象用于遍历容器,并可以通过它进行对容器中的元素的各种操作。而这种抽象,也隔离了具体容器的实现,无论是链表,数据,平衡二叉树,我们都可以用一致的迭代器接口访问那些处于容器中的元素。
这些是最经典的抽象了,在实际的开发中,我们也需要建立一个抽象的概念,然后去实现它,并从中发现对象的抽象粒度之美。
对象的粒度是一个难以界定的量,在今天的面向对象开发中,提倡单一职责的原则,尽量要对象的粒度变小。比如《C++设计新思维》就提倡这种观点。但是,书中没有刻意地让一个类保护继承自若干个Policy Classes,而且书上对这种做法进行了“辩论”,而我以为,这无伤大雅。




原创粉丝点击