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:这个名字起的太有.
                存档

  • 相关阅读:
    Codeforces 177G2 Fibonacci Strings KMP 矩阵
    Codeforces Gym100187C Very Spacious Office 贪心 堆
    Codeforces 980F Cactus to Tree 仙人掌 Tarjan 树形dp 单调队列
    AtCoder SoundHound Inc. Programming Contest 2018 E + Graph (soundhound2018_summer_qual_e)
    BZOJ3622 已经没有什么好害怕的了 动态规划 容斥原理 组合数学
    NOIP2016提高组Day1T2 天天爱跑步 树链剖分 LCA 倍增 差分
    Codeforces 555C Case of Chocolate 其他
    NOIP2017提高组Day2T3 列队 洛谷P3960 线段树
    NOIP2017提高组Day2T2 宝藏 洛谷P3959 状压dp
    NOIP2017提高组Day1T3 逛公园 洛谷P3953 Tarjan 强连通缩点 SPFA 动态规划 最短路 拓扑序
  • 原文地址:https://www.cnblogs.com/dayouluo/p/92290.html
Copyright © 2011-2022 走看看