如何更好实现和使用易重用抽象接口

电子说

1.2w人已加入

描述

 

>>> 1.5.4 实现接口

 

为了描述事物的完整性和相对封闭性,“封装”就提上了日程,细节从此不需要再去关注。而封装的传统定义是数据隐藏,如果还是这样看待封装,则具有很大的局限性。应该将封装视为任何形式的隐藏,即发现变化将其封装。封装不仅可以隐藏数据,而且可以隐藏实现和隐藏设计等所有的细节。

 

如果以更宽泛的方式看待封装,其优点是能够带来一种更好的分解程序的方法,于是封装层自然而然地就成为了设计需要遵循的接口。封装不会妨碍人们认识程序内部具体是如何实现的,只是为了防止用户写出依赖内部实现的代码。进而强迫用户在调用程序时,仅仅依赖于接口而不是内部实现,使抽象的概念接口和实现分离,将大大降低软件维护成本。

 

C语言中的*.c文件就是接口功能的具体实现,即用户不可见的内部实现,简称实现。一个接口可以有多个实现,它在发布后还可以改变、升级,因为它的改变不会对调用程序产生影响。大多数时候,*.c和*.h是成对出现的,一般来说,将某个子模块的声明放在*.h文件中,而将具体的实现放在对应的*.c文件中。*.c文件可以通过引用一个或多个*.h文件,达到共用各种声明的目的,但是*.h文件不可以引用*.c文件。

 

其实软件包就是一个用来描述定义一个库的软件,其中*.h文件作为库的接口,而实现这个库可能有一个或多个*.c文件,每个*.c文件包含1个或多个函数定义,软件包就是由*.h文件和*.c文件所组成的。这是一种良好的风格,适用于任何大型程序和小型程序。

 

假设开发一个由多个文件组成的大型程序pgm,这样就需要在每个*.c文件的顶部都放上这样一行:

#include "pgm.h"                // 用户自己编写的库文件

 

由此可见,通过共性分析使设计具有比较强的内聚,其价值就是实现紧凑的设计。从而使调用者无需关注实现的细节,实际上是函数的实现与使用它们的函数解耦了,swap()接口的实现程序清单 1.17。

 

程序清单 1.17 swap数据交换接口的实现(swap.c)

1     #include "swap.h" 

2     void swap(int *p1, int *p2)

3     { 

4            int temp;

5

6            temp = *p1;  *p1 = *p2;  *p2 = temp;

7     }

 

当p1和p2分别指向变量a和b时,则p1和p2存储的值就是&a和&b,即可用*p1和*p2表示a和b的值。如果写成以下这种形式:

temp = p1; 

 

则交换的不是a的值,而是a的地址(p1的值就是a的地址)。而函数要交换的是a和b的值,不是它们的地址。因此需要使用*运算符和指针,该函数才能访问存储在这些位置的值并改变它们。即指针允许将局部变量的地址传给函数,然后在函数中修改局部变量。

 

由此可见,当将问题的“共性和可变性”分离开来,经过简化后发现,稳定不变的相同的处理部分(temp = *p1; *p1 = *p2; *p2 = temp;)都包含在抽象的模块中,可变性分析所发现的变化的变量a和b由外部传递进来的参数应对。从软件设计学角度来看,共性和可变性分析原理自然而然地成为了面向过程编程的理论基石。

 

注意,编写代码必须遵循结构化编程规则,即每个函数、函数中的每个代码块都应该只有一个入口、一个出口。实际上,只有在大函数中,这些规则才会有明显的好处。刚开始写代码时,都会冗长而复杂。有太多的缩进和嵌套循环,有过长的参数列表,甚至还会有重复的代码。需要不断打磨这些代码,分解函数、修改名称、消除重复,并保证测试通过。

 

有时我们并不关心指针所指向的变量的类型,此时可以使用并不指定具体数据类型的泛型指针void *。通常只允许相同类型的指针之间进行转换,但泛型指针能够转换为任何类型的指针,反之亦然。比如,C标准库中的memcpy()函数它将一段数据从内存中的一个地方复制到另一个地方。由于memcpy()可能用于复制任何类型的数据,因此将它的指针参数设定为void指针是非常合理的。比如,此前的swap()函数,可以将它的参数改为void指针,则swap()就变成了一个可以交换任何类型数据的通用交换函数,详见程序清单 1.18。

 

程序清单 1.18 swap()函数(void_data_swap.c)

1     #include

2     #include

3

4     int swap(void *x, void *y, int size) 

5     { 

6            void *temp;

7

8            if((temp = malloc(size)) == NULL)

9                   return -1;

10          memcpy(temp, x, size);   memcpy(x, y, size);   memcpy(y, temp, size);

11          free(temp); 

12          return 0; 

13   }

 

>>> 1.5.5 使用接口

 

只要传入待交换的变量的地址,即可确定如何通过接口调用它们,详见程序清单 1.19。

 

程序清单 1.19  swap数据交换函数范例程序

1    #include

2     #include "swap.h"

3

4     int main(int argc, char *argv[])

5     {

6            int a = 1, b = 2;

7

8            printf("%d, %d\n", a, b);

9            swap(&a, &b);

10          printf("%d, %d\n", a, b);

11          return 0;

12   }

 

由此可见,抽象的接口隐藏了它的内部细节,用户不再依赖具体的实现代码,而是依赖于抽象接口。抽象的接口几乎没有细节,没有什么需要变化的,使抽象和细节彼此隔离,因此抽象的接口非常容易被重用,其深刻地揭示了抽象的生命力。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分