完整版:资深程序员都了解的代码复用法则 Code Reusability

分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友 微信微信
查看查看318 回复回复6 收藏收藏 分享淘帖 转播转播 分享分享 微信
查看: 318|回复: 6
收起左侧

完整版:资深程序员都了解的代码复用法则 Code Reusability

[复制链接]
young 发表于 2016-7-16 16:08:35 | 显示全部楼层 |阅读模式
快来登录
获取优质的苹果资讯内容
收藏热门的iOS等技术干货
拷贝下载Swift Demo源代码
订阅梳理好了的知识点专辑

编写代码最重要一条,是怎样复用其他程序员的代码和思路来解决问题。

通过修改他人的代码来解决复杂问题是种错误的做法,不仅成功的机率很低,就算成功也不会提供什么经验。按照这种方式进行编程,无法成长为一名真正的程序员,在软件开发领域,前景也是非常有限。

一旦问题达到了一定规模,期望程序员从头开发一个解决方案不太现实,这会导致程序员大量时间浪费在低效率工作中,并且极大地依赖程序员精通各个方面的知识。另外,这种做法也容易导致程序充满缺陷或难以维护。

完整版:资深程序员都了解的代码复用法则 Code Reusability 1

完整版:资深程序员都了解的代码复用法则 Code Reusability - 敏捷大拇指 - 完整版:资深程序员都了解的代码复用法则 Code Reusability 1





1、良好的复用和不良的复用

良好的复用帮助我们编写更好的程序,并且提高程序的编写速度。不良的复用可能短时间内帮助我们借用其他程序员的思维,但最终会导致不良的开发。下面表格对它们之间的区别进行了总结。

左边一列显示了良好复用的属性,右边一列显示了不良复用的属性。在考虑是否对代码进行复用时,要考虑它很可能会产生左边一列的属性还是右边一列的属性。

完整版:资深程序员都了解的代码复用法则 Code Reusability 2

完整版:资深程序员都了解的代码复用法则 Code Reusability - 敏捷大拇指 - 完整版:资深程序员都了解的代码复用法则 Code Reusability 2

表:良好的复用和不良的复用

值得注意的是,良好的复用和不良的复用之间的区别,并不是我们复用了什么代码或者我们怎样复习它们,而是在于我们所借用的代码和概念之间的关系

按照编程的术语,良好的复用是指我们通过阅读某个人对一个基本概念的描述后编写代码或者利用自己以前所编写的代码

注意表格的最后一行,不良的复用常常会导致失败,因为有可能是一位程序员在复用自己实际上并不理解的代码。在有些情况下,被借用的代码一开始能够工作,但是当程序员试图对借用的代码进行修改或者扩展时,由于缺乏深入的理解,很难用有组织的方式完成这样的任务。程序员必须通过不断的尝试和失败来进行试验,这样就违背了我们的基本问题解决规则的第一条也是最重要一条:先规划再开发




2、可复用组件的 5 种类型

知道了我们打算采用的复用类型之后,现在可以对代码的不同复用方法进行分类了。

在本文中,组件表示由一位程序员所创建的、可以被其他人复用以帮助解决编程问题的任何东西。

组件可以在任何地方出现,它可以是抽象的,也可以是具体的。它可以是一个思路,也可以是一段完整实现的代码。如果我们把解决一个编程问题看成是处理一个手工项目,我们所学会的解决问题的技巧就像工具一样,而组件就像是专用的零件。下面的每种组件都是复用程序员的以前工作的不同方式。



2.1、代码块 Code Block

代码块就是将一块代码从一个程序清单复制到另一个程序清单。按照更通俗的说法,我们可以称为复制粘贴工作。这是最低级形式的组件用法,常常代表了不良的复用,会出现不良复用可能导致的所有问题。当然,如果被复制的代码是自己所编写的,那就不会有实际的危害,但最好还是把一段现有的代码包装成为一个类库或其他结构,允许它以一种更清晰和更容易维护的方式被复用。



2.2、算法 Algorithm

算法是一种编程配方,它是完成一个目标的特定方法,是以日常语言或流程图的形式表达的。算法是一种高级形式的复用,一般具有良好的复用属性。算法在本质上只是思路。



2.3、模式 Pattern

在编程中,模式(或设计模式)表示具有一种特定编程技巧的模板,比如 singleton。这个概念与算法有关,但又存在区别。算法就像解决特定问题的配方,而模式是在特定的编程情况下所使用的基本技巧。模式所解决的问题一般是在代码本身的结构内部。

和算法一样,模式也是高级形式的组件复用。



2.4、抽象数据类型 Abstract Data Type

抽象数据类型是由它的操作而不是由这些操作的实现方法所定义的类型。堆栈类型就是一个很好的例子。抽象数据类型与模式的相似之处在于它们定义了操作的效果,但并没有特别地定义这些操作的实现方式。但是,和算法一样,这些操作存在一些众所周知的实现技巧。



2.5、库 Library

在编程中,库表示一些相关代码片段的集合。库一般包含了已编译形式的代码以及所需的源代码声明。库可以包含独立的函数、类、类型声明以及任何可以出现在代码中的东西。在C++中,最显而易见的例子就是标准库。

一般而言,库的使用是良好的代码复用。代码被包含在库中,因为它提供了各种程序一般需要使用的功能。库的代码可以帮助程序避免“重新发明轮子”。然而,作为程序开发人员,当我们使用库代码时,必须从中学到些什么,而不是单纯走捷径。




3、创建可复用组件的方法

组件非常有用,因此程序员应该尽可能地利用组件。

优秀的程序员必须养成习惯向他的工具箱中不断添加组件。

对组件的收集可以通过两种不同的方式进行:程序员可以明确分配时间学习新组件,把它作为一项基本任务,或者可以搜索一个组件来解决一个特定的问题。我们把第一种方法称为探索式学习,把第二种方法称为根据需要学习



3.1、探索式学习组件

我们首先从一个探索式学习的例子开始。假设我们想学习关于设计模式的更多知识,对于频繁使用的设计模式,人们已经达成了广泛的共识。因此我们能接触大量的资源。

通过简单地查找一些设计模式并对它们进行研究,我们就可以从中受益。如果实现了其中一些模式,就可以得到更多的收获。

我们在典型的模式列表中可以找到的一种模式叫策略。这是一种思路,允许一种算法(或算法的一部分)在运行时才被选择。在策略模式的最基本形式中,它允许更改函数或方法的操作方式,但不允许对结果进行更改。

例如,一个对类的数据进行排序(或者参与了排序过程)的类方法可能允许对排序的方法做出选择(如选择快速排序或插入排序)。不管选择什么排序方式,其结果(排序后的数据)是相同的,但是允许客户选择排序方法可能使代码的执行效率更高。

客户对于具有很高重复率的数据可以避免选择快速排序。根据策略的形式,客户的选择会对结果产生影响。例如,有一个表示一手牌的类,排序策略可能会决定 A 被认为是最大(比 K 大)还是最小(比 2 小)。


3.1.1、开始把学习融入到可复用实践中

通过上文我们知道什么是策略模式,但还没有运用于创建自己的实现。在工具商店浏览工具和实际购买并使用工具之间还是存在显著区别的。因此,我们现在从货架上取出这个设计模式,并把它投入到使用中。

尝试新技巧的最快方法是把它融入到已经编写完成的代码中。让我们设计一个可以用这种模式解决的问题,它建立在我们已经编写完成的代码基础之上。

班长


在一所特定的学校中,每个班级具有一名指定的“班长”,如果教师离开了教室,就由这名学生负责维持课堂秩序。最初,这个称号授予班里学习成绩最好的学生。但是,现在有些教师觉得班长应该是具有最深资历的学生,也就是学生 ID 最小的那个学生,因为学生 ID 是按照进入班级的先后顺序依次分配的。还有一部分教师觉得指定班长这个传统是件非常愚蠢的事情。为了表示抗议,他们简单地选择按照字母顺序排列的班级花名册中所出现的第 1 个学生。我们的任务是修改学生集合类,添加一个方法从集合中提取班长,同时又能适应不同教师的选择标准。


正如我们所见,这个问题将要采用策略形式的模式。我们需要让这个方法根据不同的选择标准返回不同的班长。

为了在 C++ 中实现这一点,需要使用函数指针。我们已经简单地从 qsort 函数中了解过这个概念。qsort 函数接受一个函数指针,它所指向的函数对需要进行排序的数组中的两个数据项进行比较。

在这个例子中,我们完成类似的任务。我们将创建一组比较函数,接受 2 个 studentRecord 对象为参数并分别根据成绩、学生 ID 值或姓名确定第 1 个学生是否“好于”第 2 个学生。

首先,我们需要为比较函数定义一个类型:

完整版:资深程序员都了解的代码复用法则 Code Reusability 3

完整版:资深程序员都了解的代码复用法则 Code Reusability - 敏捷大拇指 - 完整版:资深程序员都了解的代码复用法则 Code Reusability 3


这个声明创建了一个称为 firstStudentPolicy 的类型,它是个函数指针,它所指向的函数返回一个 bool 值并接受两个 studentRecord 类型的参数。

*firstStudentPolicy 号两边的括号 ❶ 是必要的,这是为了防止这个声明被解释为返回一个 BOOL 类型的指针的函数。有了这个声明之后,我们就可以创建 3 个策略函数了:

完整版:资深程序员都了解的代码复用法则 Code Reusability 4

完整版:资深程序员都了解的代码复用法则 Code Reusability - 敏捷大拇指 - 完整版:资深程序员都了解的代码复用法则 Code Reusability 4


前两个函数非常简单:

  • higherGrade 在第 1 条记录的成绩值大于第 2 条记录时返回 true;
  • lowerStudentNumber 在第 1 条记录的学生 ID 值小于第 2 条记录时返回 true。
  • 第 3 个函数 nameComesFirst 在本质上与前两个函数相同,但它需要使用 strcmp 库函数。这个函数接受 2 个“C 风格”的字符串,即以 null 结尾的字符数组而不是 string 对象。因此我们必须对两条学生记录的姓名字符串使用 c_str() 方法。strcmp 函数在第 1 个字符串按照字母顺序出现在第 2 个字符串之前时返回一个负数,因此我们检查它的返回值以判断它是否小于 0 ❸。


现在,我们就可以修改 studentCollection 类本身了:

完整版:资深程序员都了解的代码复用法则 Code Reusability 5

完整版:资深程序员都了解的代码复用法则 Code Reusability - 敏捷大拇指 - 完整版:资深程序员都了解的代码复用法则 Code Reusability 5


这个类声明增加了一些新的成员:一个私有数据成员 _currentPolicy,它存储了指向其中一个策略函数的指针、一个用于修改策略的 setFirstStudentPolicy 方法,以及根据当前策略返回班长的 firstStudent 方法本身。

setFirstStudentPolicy 的代码非常简单:

完整版:资深程序员都了解的代码复用法则 Code Reusability 6

完整版:资深程序员都了解的代码复用法则 Code Reusability - 敏捷大拇指 - 完整版:资深程序员都了解的代码复用法则 Code Reusability 6


我们还需要修改默认构造函数对当前策略进行初始化:

完整版:资深程序员都了解的代码复用法则 Code Reusability 7

完整版:资深程序员都了解的代码复用法则 Code Reusability - 敏捷大拇指 - 完整版:资深程序员都了解的代码复用法则 Code Reusability 7


现在,我们可以编写 firstStudent 方法:

完整版:资深程序员都了解的代码复用法则 Code Reusability 8

完整版:资深程序员都了解的代码复用法则 Code Reusability - 敏捷大拇指 - 完整版:资深程序员都了解的代码复用法则 Code Reusability 8


这个方法首先检查特殊情况。如果没有需要检查的链表或者不存在策略❶ ,就返回一条哑记录。否则,就使用本书中广泛使用的基本搜索技巧,对这个链表进行遍历并寻找最适当地匹配当前策略的学生。

我们把链表开始位置的那条记录赋值给 first ❷,使循环变量从链表的第 2 条记录开始 ❸,然后执行遍历。

在遍历循环中,对当前策略函数的调用 ❹ 告诉我们目前所查看的学生根据当前标准是否“好于”到现在为止所找到的最佳学生。当这个循环结束时,我们就返回“班长” ❺。


3.1.2、班长解决方案的分析

使用策略模式解决了一个问题之后,我们很可能想确认这种技巧可以适用的其他场合,而不是一次了解了这个技巧之就将其束之高阁。我们还可以对示例问题进行分析,形成对这个技巧的价值的认识,明白什么时候使用它比较合适,什么时候使用它则是个错误,或至少它带来的价值应多于麻烦。对于这个特定的模式,读者可能会看到它弱化了封装和信息隐藏。

例如,如果客户代码提供了策略函数,它就需要访问通常属于类内部的类型,在这个例子中也就是 studentRecord 类型。这意味着如果我们修改了这个类型,客户代码就有可能失败。把这个模式应用于其他项目之前,必须在这个顾虑与它可能带来的好处之间进行权衡。通过对自己的代码进行检查,可以对这个关键问题获得深入的体会。

至于进一步的实践,我们可以检查已完成项目的库,搜索可以使用这种技巧进行重构的代码。记住,很多“现实世界”的编程涉及到对现有的代码进行补充或修改,因此这是进行这类修改的一种非常好的实践,还能发展自己运用某种特定组件的技能。而且,良好的代码复用的一个优点是我们可以从中进行学习,而实践能够最大限度地提升学习的效果。



3.2、根据需要寻找可复用组件

前一节描述了“漫游式学习”的过程。虽然这种类型的学习旅程对于程序员而言是极具价值的,但有时候我们必须直接针对一个特定的目标学习。

如果我们正在着手处理一个特定的问题,特别是当这项工作面临极大的时间压力时,我们会猜测某个组件可能会为我们提供极大的帮助。我们不想通过随机漫游编程世界来碰到自己所需要的东西,而是想尽可能快地找到直接适用于自己所面临问题的组件。

但是,这听起来似乎有些挑战,当我们并不准确地知道自己所寻找的是什么时,怎样才能找到自己所需要的东西呢?思考下面这个示例问题:


3.2.1、高效的遍历

一个编程项目将使用我们的 studentCollection 类。客户代码需要做到能够遍历集合中的所有学生。显然,为了维护信息隐藏,客户代码不能直接访问这个链表,但要求高效地对其进行遍历。

由于这段描述中的关键词是高效,让我们精确地分析它在这个例子中的含义。我们假设 studentCollection 类的一个特定对象具有 100 条学生记录。如果我们直接访问这个链表,可以编写一个迭代 100 次的循环。这是所有的链表遍历中最高效的做法。任何要求我们迭代超过 100 次的循环都可以认为其结果是不够高效的。

如果没有高效这个需求,我们可以在这个类中添加一个简单的 recordAt 方法来解决这个问题。这个方法返回集合中特定位置的学生记录,第1条记录的位置编号为 1:

完整版:资深程序员都了解的代码复用法则 Code Reusability 9

完整版:资深程序员都了解的代码复用法则 Code Reusability - 敏捷大拇指 - 完整版:资深程序员都了解的代码复用法则 Code Reusability 9


在这个方法中,我们使用了一个循环 ❶ 对链表进行遍历,直到找到了所需的位置或者到达了链表的尾部。当这个循环结束时,如果已经到达了链表的尾部,我们就创建并返回一条哑记录 ❷。如果是在指定的位置就返回这条记录 ❸。问题在于我们执行遍历只是为了寻找一条学生记录。这并不一定是完整的遍历,因为当我们到达所需的位置时就会终止循环,但它终归还是进行了遍历。假设客户代码试图求学生成绩的平均值:

完整版:资深程序员都了解的代码复用法则 Code Reusability 10

完整版:资深程序员都了解的代码复用法则 Code Reusability - 敏捷大拇指 - 完整版:资深程序员都了解的代码复用法则 Code Reusability 10


对于这段代码,假设 sc 是个以前所声明并生成的 studentCollection 对象,recNum 是个表示记录数量的整数。假设 recNum 变量值为 100。当我们初步扫视这段代码时,可能觉得计算平均成绩只需要迭代这个循环 100 次,但由于每次调用 recordAt 函数本身就要执行一次不完整遍历,因此这段代码总共涉及 100 次遍历,每次遍历平均需要进行 50 次迭代。因此,它的结果并不是非常高效的 100 个步骤,而是大约需要 5000 个步骤,这是极为低效的。


3.2.2、什么时候搜索可复用组件

现在,我们触及到真正的问题。让客户访问集合成员对其进行遍历是非常容易的,但高效地提供这种访问却是非常困难的。当然,我们可以尝试只用自己的能力来解决这个问题。但是,如果我们可以使用一个组件,就能够很快实现一个解决方案。

为了寻找一个适用于我们的解决方案的未知组件,第 1 个步骤是假设这个组件实际上存在。换句话说,如果我们不开始搜索,就肯定无法找到这样一个组件。因此,为了最大限度地获得组件的优点,需要使自己处于能够让组件发挥作用的场合。发现自己陷在问题的某个方面而无法自拔时,可以尝试下面这些方法:

  • 以通用的方式重新陈述这个问题。
  • 向自己提问:这是否可能成为一个常见的问题?


第 1 个步骤非常重要,因为我们把问题陈述为“允许客户代码高效地计算一个类所封装的记录链表中的平均学生成绩”,它听上去特定于我们所面临的情形。但是,如果我们把这个问题陈述为“允许客户代码高效地遍历一个链表,并且不需要提供对链表指针的直接访问”,我们就开始理解这可能成为一个常见的问题。

显然,我们可以想象,由于程序常常需要在类中存储链表和其他线性访问的数据结构,因此其他程序员肯定已经想出了允许高效地访问数据结构中的每个数据项的办法。


3.2.3、寻找可复用组件</