zoukankan      html  css  js  c++  java
  • COM沉思录(八)

    COM沉思录(八)

                接口的不变性
                
                将“接口与实现分离”,让调用者不能了解库提供者的任何细节,而仅仅知道库所提供的公开接口,可以进一步降低重用的难度。而这些接口则是两者之间进行通信的协议,也可以称之为“契约”或“合同”。
                
                只要双方都按照“合同”所规定的方式去做,“合同”之外的东西互相不需要了解。那么无论一方何时做出何种改变,只要仍然符合“合同”的所有规定,那么对另一方则毫无影响。
                
                但是,一旦“合同”发生了变化,也就意味着双方之前的约定被破坏。除非双方能够协商建立新的“合同”,并都按照新的合同去操作,否则,双方的合作将会被破坏。
                
                既然接口就是双方(库和其调用者)合作的“合同”,所以接口必须具备“不变性”,即一旦接口被发布,将永不能变。
                由于接口是函数声明的集合,所以接口的“变”有三种,集合中的函数被增加,减少,或改变。这个改变是指函数的参数以及返回值的改变,不包括函数名的改变(改变一个函数的名字,等同于先减少一个函数,再增加另外一个函数)。
                
                不能减少和改变一个接口中的函数很容易可以理解,为什么增加一个函数也不可以?既然没有改变和减少原来的函数,原来的调用者似乎并不会受到影响。
                
                但现实往往是残酷的。且不考虑增加一个接口函数就意味着向“合同”里增加新的“条款”一样已经改变了合同双方最初的约定等人文因素,即使从纯技术的因素考虑,也会带来致命的伤害。让我们来看看为什么?
                
                因为接口是允许继承的,即一个接口可以从继承另外一个接口的函数集合,例如:
                struct interface_A
                {
                     virtual void func_A1(int) = 0;
                     virtual int func_A2(void) = 0;
                };
                
                struct interface_B: public interface_A
                {
                      virtual void func_B1(void) = 0;
                };
                
                编译后,interface_A和interface_B的二进制结构为:
                
                        vptr ---------> |-----------------|
                                        |    func_A1      |
                                        |-----------------|
                                        |    func_A2      |
                                        |-----------------|
                
                        vptr ---------> |-----------------|
                                        |    func_A1      |
                                        |-----------------|
                                        |    func_A2      |
                                        |-----------------|
                                        |    func_B1      |
                                        |-----------------|
                
                如果现在我们为interface_A增加一个新的函数func_A3,则interface_B的二进制结构变为:
                        vptr ---------> |-----------------|
                                        |    func_A1      |
                                        |-----------------|
                                        |    func_A2      |
                                        |-----------------|
                                        |    func_A3      |
                                        |-----------------|
                                        |    func_B1      |
                                        |-----------------|
                
                如果一个对象(组件)O实现了interface_B,而调用者使用了这个对象,则对于原来的版本,调用者调用接口B的函数func_B1时,编译出来的结果是访问vtbl的第3项。但对于新的版本,由于调用者没有重新编译,其仍然以vtbl的第三个入口来调用func_B1,但此时,对象O的接口interface_B的vtbl中的第三项已经改变为func_A3,于是错误的结果产生了。
                
                关于这个问题,Don
                Box给出了一个不充分,甚至是错误的解释。他认为新的调用者(即针对修改后的接口编译的调用者)在碰巧调用旧版本的接口时,由于找不到新的接口函数会引起崩溃。仔细考虑一下,在同一台机器上,既然库已经升级为新版本,怎么可能还会调用到旧的版本?
                
                但是,世间万物总是不完美的,接口也不能幸免。有时候,接口确实需要被改变。此时,至少有两种途径可以做到这一点:
                
                1)如果仅仅是扩充接口的功能,即增加新的接口函数,可以通过继承原有的接口,形成新的接口。
                2)干脆重新设计新的接口,让对象(组件)继承多个不相关的接口。


                 darwin_yuan @ 14:56 | 阅读全文 | 评论(0) | 引用Trackback(0) | 编辑 

                2004-03-26  14:50
                COM沉思录(七)

                ——继承
                之前,我们把一个接口的二进制结构定义为一个vptr以及vptr指向的vtbl。一个实现了这个接口的对象的第一个属性就是vptr,于是我们可以很容易的定位并访问一个对象的接口。
                如果我们限制组件只能实现一个接口,则大大降低了我们这种技术的方便性。我们必须允许一个组件可以实现多个接口。
                既然一个接口包含一个vptr和一个vtbl,那么一个组件实现了多少个接口,组件里就会包含多少个vptr和vtbl。这些vptr依次放在组件对象结构的最前面,它们的顺序依赖于被声明的顺序。比如:
                class Component: public interface_A, interface B, interface_C
                {
                public:
                .... // 接口函数的声明及实现
                private:
                 int a;
                };
                则在组件Component对象的前面依次为interface_A, interface_B,
    interface_C的vptr,如下所示:
                    |---------------------|
                    |  interface_A_vptr   |---> interface_A_vtbl
                    |---------------------|
                    |  interface_B_vptr   |---> interface_B_vtbl
                    |---------------------|
                    |  interface_C_vptr   |---> interface_C_vtbl
                    |---------------------|
                    |      int a          |
                    |---------------------|
                而调用者想访问哪个接口,就使用哪个vptr。一切都很明了。
                另外,也必须允许接口继承接口,这在面向对象的理论和实践中是一种自然的行为,不必过多的叙述。但这种继承必须是单继承。因为多重继承会影响一个接口的二进制格式。
                我们之前已经把一个接口的二进制格式设计为一个vptr和一个vtbl,如果一个接口继承自一个接口,并不会改变这种布局。比如:
                struct interface_A
                {
                 virtual int func_A1(void) = 0;
                 virtual void func_A2(int) = 0;
                };
                struct interface_C: public interface_A
                {
                 virtual void func_C1(void) = 0;
                };
                则interface_C的结构为:
                   interface_B_vptr -----> |-------------------|
                                           |    func_A1        |
                                           |-------------------|
                                           |    func_A2        |
                                           |-------------------|
                                           |    func_C1        |
                                           |-------------------|
                这没有问题。但如果是多继承,则一个接口会包含多个vptr及多个vtbl。比如:
                struct interface_B
                {
                 virtual int func_B1(int) = 0;
                };
                struct interface_C: public interface_A, interface_B
                {
                 virtual void func_C1(void) = 0;
                };
                则interface_C的二进制结构为:
                   interface_A_vtpr -----> |-----------------|
                   interface_B_vtpr --|    |    func_A1      |
                                      |    |-----------------|
                                      |    |    func_A2      |
                                      |    |-----------------|
                                      |    |    func_C1      |
                                      |    |-----------------|
                                      |
                                      |--->|-----------------|
                                           |   func_B1       |
                                           |-----------------|
                这显然与我们制定的接口二进制格式不符。另外,让接口进行多继承是没有必要的。我们完全可以通过让组件继承多个接口来实现与之等价的功能,同时却不破坏一个接口的二进制定义。
                对于这个例子,我们可以通过如下方法实现:
                struct interface_C: public interface_A
                {
                 virtual void func_C1(void) = 0;
                };
                class Component: public interface_C, interface_B
                {
                public:
                 // 接口函数声明及实现。
                private:
                 // 组件属性
                };
                仔细考虑一下,你就会看出,两种方法得到了组件二进制结构是完全相同的(此时,你或许会质疑我之前的讨论),但两者之间在语义及操作上有着本质的不同,对于前者,组件实现的接口只有interface_C,那么就应该只存在一个vptr和一个vtbl,但组件却存在两个vptr和两个vtbl。而后者实现了两个接口,当然应该存在两套vptr和vtbl。


                 darwin_yuan @ 14:50 | 阅读全文 | 评论(0) | 引用Trackback(0) | 编辑 

                2004-03-24  14:35
                COM沉思录(六)

                ——二进制接口
                
                将“接口与实现分离”,进一步降低了重用的难度。但现实生活中仍然有很多的问题在阻碍重用。其中最重要是各种开发语言的互操作问题。
                
                我们梦中存在的一种景象是,用任何一种语言开发出来的库,可以被任意其它语言轻松调用。
                
                这几乎无法做到,一个问题就可以把我们这个梦击的粉碎——编译型语言如何调用解释性语言所写的库?有人会说,这可以做到:编译型程序启动那个解释性语言的解释器,由它来解释由这种解释性语言写的库就行了。
                
                除非我疯了,或者有人拿刀架在我的脖子上,否则我绝对不会采取这个方案。一个库的提供者,如果它提供这套库的目标是想做到让所有的语言都可以轻松调用它,却选取解释性语言来写,那它一定是大脑出了问题。
                
                难道就这样放弃我们的梦想?别着急,冷静,冷静,再冷静。管理学中有一个重要的理论:最优目标是很难达到的,现实中我们往往追寻的是次优目标。我们不妨换个思路,如果我们的库是用编译型语言写的,那么是不是所有的语言都可以相对容易的调用?
                
                一个重要的事实是,所有现代主流的的解释型语言都提供了对编译型语言,如C或C++库的调用接口,并且事实上它们在调用这些库的时候,不需要借助任何东西,调入内存,执行它就是了,这样性能反而更高。
                
                啦……啦……,我们生活在一个幸福的年代,我们是一群幸运儿~~
                
                Come down!
                即使是编译型语言,由于编译器的实现千差万别,想实现跨语言调用,甚至同一种语言内部的互相调用,并不如想像的那么轻松。假如库是由C++写的,由于Name
                Mangling,C语言如何通过符号名调用它?即使由C++调用,由于不同编译器的Name
                Mangling的方案不同,也无法通过符号名调用。
                
                事情似乎又回到了列宁的问题上:怎么办?
                
                解决问题的思路当然是对症下药。既然存在Name
                Mangling,那我们不使用符号名来对应调用关系就行了,而是指定一个大家约定好的二进制格式,双方都可以根据这种格式找到相应的调用接口。
                
                不错的思路,但还需要加一个约束,那就是这种二进制格式必须让各种编译型语言可以轻易的生成。而所有语言都可以轻松的识别。
                
                让我们的头脑风暴继续狂野的进行下去——
                
                在面向过程的语言中,所谓接口,其实一个函数集合。而在面向对象的语言中,一个函数集合可以被定义为一个仅仅包含函数成员的的类。比如:
                struct interface{
                   void function_1(int);
                   int function_2(void);
                   float function_3(int, float);
                };
                
                由于我们不能以符号——在这里就是函数名来进行双方的约定,那就使用函数指针。并且由于接口背后的实现可以被替换,也就是说这些函数指针在不同的实现中可以指向不同的函数,但函数原型却是一致的。所以,在C中,我们按如下方式定义一套接口:
                struct interface
                {
                   void (*function_1)(int);
                   int (*function_2)(void);
                   float (*function_3)(int, float);
                };
                
                而在C++中,我们把函数定义为virtual的,就能让函数变为函数指针。
                
                struct interface
                {
                   virtual void function_1(int) = 0;
                   virtual int function_2(void) = 0;
                   virtual float function_3(int, float) = 0;
                };
                
                将两者编译,C语言写成的结构将生成一个和定义吻合的函数指针表,但C++除了生成一张函数指针表外,还生成一个指向这张表格的指针。这张表在C++中被称作vtbl,这个指针称作vptr。
                
                到底应该使用哪种方式?考虑一下我们需要的是什么。
                
                按照“将接口与实现分离”的原则,调用者知道的仅仅是接口,但调用者通过这些接口仍然要操作数据,这些数据可能是全局变量,但更常见的应该是具体的对象,而这些对象是由库提供的接口函数来创建的。
                
                好吧,我们进一步的说,按照《Object-Oriented Software
                Construction》所描述的样子,软件重用应该像硬件重用那样,提供的是一个一个元件,这些元件实现了一个或多个接口,然后把这些元件通过接口组合起来,形成系统。而在软件中,一个元件就是一个对象或者结构,这个对象有自己的属性,同时提供对外的接口。
                
                回到原来的推理上,我们通过调用库,得到一个对象,然后调用这些接口从这个对象获取我们需要的功能。
                
                既然一个元件即有自己的属性,还有自己的接口,那么它的布局应该是怎样的?
                
                稍加思考我们就知道,C++的方式更符合我们的需要。因为按照C的方式,每一个对象实例都会包含所有的函数指针,这无疑是没有必要的。而C++让所有的对象实例共享同一个vtbl,每个对象实例仅仅包含一个vptr就够了。
                
                我们的组件实现一个接口时,在C++中就是在实现类中继承这个接口类,这个实现类中可以有自己的私有属性,以及内部接口(私有的或受保护的),比如:
                
                class component: public interface
                {
                public:
                   virtual void function_1(int);
                   virtual int function_2(void);
                   virtual float function_3(int, float);
                protected:
                    void self_func(void);
                private:
                    int na;
                    char* nb;
                    long nc;
                };
                
                编译出来的布局为:
                
                    |-------------------|             vtbl
                    |     vptr          |------>|----------------|
                    |-------------------|       |  p_function_1  |-->
                    |     int na;       |       |----------------|
                    |-------------------|       |  p_function_2  |-->
                    |     char* nb      |       |----------------|
                    |-------------------|       |  p_function_3  |-->
                    |     long nc       |       |----------------|
                    |-------------------|
                
                这样,当调用者得到一个对象时,它就能很容易的找到这个对象的vtbl,然后根据索引,而不是链接的符号名来调用vtbl中的接口函数。
                
                我们把这个对象中的接口部分拿出来,就是如下的结构。
                    |------------------|                   vtbl
                    |     vptr         |------>|-----------------|
                    |------------------|       |   p_function_1  |-->
                                               |-----------------|
                                               |   p_function_2  |-->
                                               |-----------------|
                                               |   p_function_3  |-->
                                               |-----------------|
                
                对于这样一个接构,用C可以很轻松的实现。我们之前用C声明的结构体就是vtbl,我们只需要在对象结构的最前面加上一个vptr就行了,如下:
                struct object
                {
                   struct interface* vptr;
                   int na;
                   char* nb;
                   long nc;
                };
                
                对于C++对象模型非常了解的人一定会提出这样两个问题:
                1)不同编译器对vptr实现的位置不同。
                2)对于vtbl,由于RTTI,其前面会有一个Type_info。
                
                是这样,但它们都不影响我们使用C++生成这样的结构。
                
                对于第一个问题,有些编译器把vptr放在对象其它数据的前面,有些放在后面,比如下面的类
                
                struct T
                {
                   virtual void func1(void) = 0;
                   virtual void func2(int) = 0;
                private:
                    int na;
                    long nb;
                };
                
                由两种编译器编译出来的结构如下。
                      |-----------------|      |----------------|
                      |      vptr       |      |     int na     |
                      |-----------------|      |----------------|
                      |     int na      |      |     long nb    |
                      |-----------------|      |----------------|
                      |     long nb     |      |     vptr       |
                      |-----------------|      |----------------|
                
                但这个这是基于没有继承关系的类而言的,如果struct T被继承,比如:
                
                class Derived: public struct T
                {
                public:
                   virtual void func1(void);
                   virtual void func2(int);
                private:
                   char nc;
                };
                
                则两种编译器编译出的结构如下:
                      |------------------|      |------------------|
                      |       vptr       |      |     int na       |
                      |------------------|      |------------------|
                      |     int na       |      |     long nb      |
                      |------------------|      |------------------|
                      |    long nb       |      |     vptr         |
                      |------------------|      |------------------|
                      |    char nc       |      |    char nc       |
                      |------------------|      |------------------|
                
                仔细观察这两个结构,你就会知道,基类对象的所有属性都被排放在继承类属性之前。而如果我们把基类属性都除去,则vptr就成为唯一从基类继承下来的东西,那么它自然就成为继承类的第一个元素。
                
                在我们的方案中,接口不包含任何属性,所有的实现类都是从接口类继承下来的,所以vptr当然被放在实现类对象的最前面。
                
                对于第二个问题,RTTI是后来加入C++的特征,对于任意一个C++编译器,为了实现和早期的C++程序的链接,它们都提供了相应的编译选项,除去这个特征,比如g++提供了-nortti。
                
                我们就这样得到了一个简单却绝对有效的,任何通用目的的编译型语言都可以毫不费力的生成的,任何非特殊目的的编程语言,无论是编译型还是解释型的(C,
                C++, Java, Ada,甚至Basic)都可以很容易识别的二进制结构。


                 darwin_yuan @ 14:35 | 阅读全文 | 评论(0) | 引用Trackback(0) | 编辑 

                2004-03-23  13:25
                COM沉思录(五)


                ——接口与实现分离
                
                动态模型让重用变得更容易,但还远远不够。
                
                因为动态模型仅仅从物理上将可重用库和库的调用者分开。而两者逻辑上可能存在千丝万缕的联系。一旦库发生了调用者可见的变化,调用者必须做出相应的改变。
                
                先看一个例子。假如库A由一个调用者可见的类。
                
                class T
                {
                public:
                     void do_something(int v) { a = v; }
                private:
                   int a;
                };
                
                调用者在自己的程序中使用了这个结构体,比如直接声明这个类的一个实例。然后开始调用这个类的公开方法。如下:
                
                void func(int v)
                {
                  T t;
                  t.do_something(v);  
                }
                
                然后,编译调用者,并指定其和库进行动态链接。
                
                显然,调用者的执行进入func的时候,由于其了解class T的细节,所以它知道需要在stack中为class
                T的实例t,分配4个字节的空间(如果int的宽度为4
                bytes的话),然后调用类函数T::do_something,这个函数的相关代码在库里,它会对a进行操作,如下所示:
                
                          Library                     Client(Caller)
                     |----------------|  Calling  |------------------|   
                     |   Function     |<----------|                  |
                     |  do_something  | Operation |  |------------|  |
                     |                |-----------|->|A T Instance|  |
                     |----------------|           |  |------------|  |
                                                  |------------------|
                
                这不会有问题,因为无论是库函数还是调用者,大家对T的认知是一致的。
                
                但随后,由于某种原因,在库的升级版本中,将T的定义改变了,如下:
                
                class T
                {
                public:
                     void do_something(int v)   { b=a; a = v; }
                private:
                   int a;
                   int b;
                };
                
                此时,将库重新编译为动态链接库,然后将旧的版本替换掉,然后启动没有被重新编译的调用者,紧接着,系统在短暂的运行之后崩溃了。为什么会这样?
                
                这是因为调用者对T的认知仍然停留着4个字节的阶段,而在升级之后的库看来,T应该是8个字节,类成员函数T::do_something会按照新的认知来操作T的实例t,于是发生越界操作。系统当然会崩溃掉。
                
                那么,如果在调用者不直接声明T的实例,而是动态申请呢?比如调用者函数func修改为:
                void func(int v)
                {
                  T* t = new T;
                  t->do_something(v);  
                }
                
                结果是一样的,因为new事实上调用的是调用者的operator new(size_t size);请注意这个函数的参数size。T *t
                = new T其实最终被转化为T *t = operator
                new(sizeof(T))。而这个sizeof(T)在第一次被编译是为4,后来既然没有重新编译调用者,那么它依然为4。
                
                由这个例子可以很清楚的得知,如果调用者了解库所提供的某个数据结构的实现细节,那么它就对这个库产生了编译依赖,当库里任何它的调用者所依赖的实现细节发生改变的时候,调用者也必须进行重新编译。
                
                怎样才能避免这一点?很简单,那就是将“接口与实现分离”——库应该对调用者坚决隐藏所有实现细节,仅仅公开外部调用接口。如果想引用一个库提供的对象的时候,也应该由库来创建,然后调用者通过一个指向对象的指针来引用它。而对这个对象的操作,也完全有库所提供的公开接口来进行。既然对象的创建和对对象的操作都是由库完成的,那么无论库进行怎样的改变,它对实现细节的认知肯定是一致的。


                 darwin_yuan @ 13:25 | 阅读全文 | 评论(0) | 引用Trackback(0) | 编辑 

                2004-03-22  13:22
                COM沉思录(四)


                ——动态链接
                
                Microsoft,这只软件业最大的超级恐龙,首先是一个操作系统提供商。它希望它的操作系统所提供的服务——比如库,不仅仅由固定的某种或某几种语言来调用。这是一个重用问题。
                
                但对于传统的编译链接模型,如果不加任何约束,不可能轻易做到这一点。
                
                在动态链接出现之前,软件一直使用静态链接模型,但静态链接存在很多问题。
                
                首先,使用静态链接模型无疑会造成资源的无谓浪费。有多少个系统调用某个库,那么这个库就会有多少份拷贝,这首先占用了静态资源(磁盘),当这些系统运行时,每个系统同样要为这些库在内存中分配空间,这就浪费了宝贵的动态资源(内存)。而这种浪费根本就是不必要的。
                
                其次,库生产厂商提供库,其它软件厂商在自己的产品中调用这些库,这是一种自然的重用行为。但静态链接模型无疑提高了重用的成本。
                
                任何一段代码都不可能是完美的,恒久不变的。随着需求的变化,本来完全运行良好的代码会变得不合需要。更不用说代码本身就存在BUG。所以,升级在软件领域(在其它领域也一样)是一个再普遍不过的行为。
                
                如果仅仅是库升级了,而调用这个库的程序本身没有任何改变,在静态链接模型下,必须对整个系统进行重新链接,并把链接之后的产品重新发布,这是一件麻烦事。
                
                这还不算什么,如果在一个指定的平台上,有多个系统调用同一套库;当这个库做出升级后,所有调用它的程序如果想使用新特性,或者想稳定工作(如果老版本的库有Bug),都必须重新编译。而这些系统很可能由不同的厂商提供,嗯,真是麻烦透顶。
                
                于是,动态链接模型出现了。动态链接模型将库和它的调用者分开,在磁盘上只保存库的一份拷贝,在运行时也是如此,首先节约了资源。另外,如果库本身只是修改了某些代码逻辑,而没有修改任何对外是可见的数据结构,调用者则无须做任何事情,平台拥有者将旧版本的库替换掉就是了。
                
                由此可见,动态链接技术降低了重用成本,使重用变得更容易。


                 darwin_yuan @ 13:22 | 阅读全文 | 评论(0) | 引用Trackback(0) | 编辑 

                日历

                  2004 年 12 月 
                SunMonTueWenThuFriSat
                   1234
                567891011
                12131415161718
                19202122232425
                262728293031


          

                用户登录
                用户名:
                密   码:
                          

                搜索

              


                最近更新
          为goto正名
          相等性判断的自动化
          属性也应是接口
          Delegate Class
          Package vs. Namespace
          Function Signature:新思维
          Visibility: 两种观点
          Java在Interface方面的缺陷
          也谈对象的生命
          Unlink操作


                最新评论
          soloist:To darwin_yuan:<.
          soloist:一、楼主所描述的.
          dreamhead:1 对于只有get和.
          tinyfool:属性当然是接口,.
          javasea:这不是java特有的.
          fox: 随便问一句,您?
          fox:很高兴看到沉寂很.
          fox:好久没看到您的更.
          mochow:好久没更新了,什.
          dreamhead:这个名字起的太有.
                存档

  • 相关阅读:
    01背包回溯法
    网络嗅探器
    侦听局域网内密码
    Winsock协议目录
    LSP(分层服务提供者)
    n后问题回溯法
    批处理作业调度回溯法
    图m着色问题
    SPI概述
    符号三角形问题回溯法
  • 原文地址:https://www.cnblogs.com/dayouluo/p/92290.html
Copyright © 2011-2022 走看看