《软件框架设计的艺术》第一部分 理论与理由

========第一部分 理论与理由=========

  发明,设计与编写API的过程,既可以看作是艺术创作,也可以当作是科学实践;

  可以把一个API架构看作是一个努力改变实践的艺术家,也可以看作是一个架设桥梁的工程师;

  保持API的一致性是非常难得,肯定要求设计组的各个成员都有着很好的默契,合作无间;还需要方法论知道大家如何来实现最终目标。

  综上所述,为了避免术语上的混淆,本部分会建立一个术语库,并分析API设计的常见内容,首先要简历一个基本的术语库,描述整个API出现的原由,并概述设计过程的主要目标。

  本部分,主要围绕三个问题?

  (1)为什么要有API?

  (2)什么是API?

  (3)如何设计API?

第一章  软件开发的艺术

  软件开发的历史很短,从有人编写和执行第一个计算机程序至今,也不足百年,尽管历史并不悠久,但其影响力不亚于任务一种开创性的发明。有人把计算机科学历史发展史与人类认识真实世界的历史放在一起加以比较,这非常有意思,我们也借此来理解为什么软件开发需要优秀的API。

(1)理性主义、经验主义、无绪

  在文艺复兴时期,现代科学产生了两大重量级理论,表现在哲学方法则为理性主义和经验主义。

  理性主义的起源来自于伽利略的自由落体实验;在英吉利海峡的另一边又诞生了经验主义;

  但是站在今天来看来,如果孤立地基于两种极端的方式来看待问题,或者观察问题,就会出现严重的偏离事实。

  从理性到无绪

  为国际受众设计API与创建书籍都是比较困难得事情。个人喜好和文化差异都会影响我们处理问题的方式,理性主义更喜欢谈论理论,了解现实对象背后的联系,而后再创建实例将理论应用到现实世界中来,而经验主义者则正好相反,先想尽各种办法获取实践经验,最终才会对事物的联系作出判断。

  从针对性无绪(selective cluelessness)的角度来解释如何设计API。我们将API看作一种可以将无绪极大化又能得到可靠结果的完美工具,正确理解无绪的真正含义是十分必要的。

(2)软件的演变过程

  在20世纪40年代和50年代初期,编写代码是非常困难的事,不得不学习机器语言,同时还要知道寄存器的大小和数量,有时候,情况不妙,还要拿起螺丝刀亲自上场,去链接计算单元的信号线。人们的精力不在于思考一个算法,而放在将算法写成可执行的程序上,这是一种枯燥有机械的工作。

  所以前期出现了FORTRAN语言,可算是程序员的福音,所以让程序员只关注数学公式的计算,而不需要考虑其他的内容。不需要关注汇编语言和机器语言内部的执行 ,所以FORTRAN语言简化了软件的开发,再到后来出现了新语言(如COBOL),然后出现了一种口号“新手就能学,管理层也可以读懂”,所以这个时候,经验主义占了上风。  

  并非所有的人喜欢经验主义,然后理性主义者以约翰.麦卡锡发明了LISP语言,认为万事万物都应该是合理的。。。。

(3)巨大的组件

  在20世纪的前10年中,大部分软件系统都可以用脏乱差来形容。没有哪个软件的设计配的上优雅这个词。这个主要是因为开发的时候,大家的目标就是用尽可能的低成本来实现某个功能,就会引入一些可以复用的半成品框架组件来完成,而不在乎这些组件的重量程度已经远远超过了自己的需求部分。

(4)漂亮、真理和优雅

  现在这样的重量级开发形式,最后会把一个程序变成垃圾堆,那怎么有一种开发方式可以优雅的表达出来? 如此丑陋的应用怎么能力保证其正确性?其实答案是肯定的,我们只需要仔细看一下我们大部分人现在所担心的事情。

  对于真理和美这个起源于古希腊。对于古希腊哲学家,最有价值的科学知识其定义是非常简明的,其意义清楚明白,绝不含糊,于是几何学就成为了科学中最具有价值的。

  古希腊人的几何学世界和现实世界的一个重要区别在于重要性。

  几何学的物体会因为其复杂性而产生改变。

  在文艺复现时期,古希腊风格一级古希腊人对于比例,美丽及和谐的思想,在艺术和科学方面可谓是无所不在。

  。。。。

  计算机科学和软件工程其实也不例外,他们也需要美丽和合理,然而,应该牢记一点,软件的首要目标是在发布给客户以后能够可靠的运行,已提供客户需要的功能,在软件正式发布前的冲刺阶段,所有的软件工程师根本不会想到美丽这个词,而是把精力集中在修复bug上,严格地说是绕开严重bug上,然后按计划发布软件产品,事实上,这个时候,简单和优雅根本不是工作的目标,虽然大家都会感觉应该做这件事,但已无暇顾及,现在我们既然认识到这一点,就可以记起无绪开发这样的大旗,作为从今往后开发方法论。

 (5)更好的无绪

  所谓的无绪,其实就在试实现一个系统的时候,或者开发一个软件,只需要了解怎么实现就ok,不管是当中用了多少组件和插件,只要能满足需求,开发出貌似没有bug的结果也是ok的。

  “无绪”这个词并不是一个贬义词,我们只是用它来区分两种层次的理解水平,有一种理解可以称为“浅层理解”,同理还有一种“深层理解” 浅层理解,它是指对一种事物的了解程度只限于掌握使用方法结果,也就是知其然而不知其所以然,还有一种就是,知其然而知其所以然;

  其实在软件开发当中,我们大部分情况下,我们只要做到“浅层理解”,就可以了,“针对性无绪”,这个词用在这里只是提醒大家,有些内容需要深入了解,而有些内容则无需如此,所以针对性无绪是个彻底褒义的词。

第二章  设计API的动力之源

   设计API并不容易,而且代价也不菲,开发一款不需要API的软件,比开发API相比而言容易很多。

(1)分布式开发

  API可以被看作是对功能和组件内部实现的一个抽象,通常来说,外部人员并不需要了解内部实现情况,只需要通过定义好的API操作说明即可调用操作。

  分布式开发自有其特殊之处,最大的不同就是,整个程序的源代码已经不再完全处于开发人员的控制下,而是散布于世界各地。与完全基于内容代码仓库中的源代码来构建一个程序相比,这种构建软件的方式是明显不同的。

  在这种开发模式下,产品的开发进度是无法全面掌握的,软件的源代码和开发人员散步在全世界,而且开发人员各自有自己的安排,所以项目经理无法全面掌握,作为一个项目经理,超过50人的团队,其实他自己也很清楚,无法全面的掌握每个人的开发情况。

  事实上,要设计良好的API,开源并不是唯一的驱动力。

(2)模块化应用程序

  模块化应用程序是有分布式团队开发出来的独立组件组成的。

  在设计API时,第一课也是非常重要的,就是给自己API的组件起一个不错的名字,尽量做到用户闻其名知其意,比如netbeans平台上的一个支持项目结果组件,org.netbeans.api.projects,这个名字堪称完美。

  在知道如何为自己所开发的组件命名以后,还要看一下每一个组件运行的环境,没有一个程序是在真空中运行的,他需要从其他周围的环境中获取相应的服务,所以每一个组件还需要考虑运行的环境。

(3)交流互通才是一切

  应用程序应该基于无绪原则来开发,尽量让最终负责集成的相关人员不需要深入了解系统页可以把集成工作做好,所以,我们理想的应用程序应该基于模块化架构来开发,可以由散步在世界各地的开发人员分别编写相应的模块,他们可以按照自己的日程来安排,已到达最终目标,但是这种做法却存在一个重要的问题,那就是模块间的关联关系。

  大多数模块并不能孤立存在,他们要依赖于其他模块提供的环境,只是少数模块才可能完成不依赖其他模块而独立对外提供功能,实际上,大部分模块化的组件都需要其他组件为其提供服务,这就意味着这些模块的开发人员需要去发现和了解如何使用外部模块提供的API,而这样的模块也是有其他开发人员编写的,所以还会出现一个隐藏的问题,即该模块又依赖于另外的模块。

  API不是给计算机用的,而是人,是我们这些开发人员,API往往包含了大量的内容,还有相应的说明文档,其实对于要运行程序的计算机来说根本不需要这些东西。

  API最重要的作用就是分解(separation),要做到分解,就需要为设计和维护API制定相应的规则,如果不能分解,那么整个产品就是紧耦合的。一旦这个程序开发完了,也就是不需要对外提供一个API了,但在现实生活当中,往往以模块化的方式进行的,所以我们开发完的模块,会被其他模块所运用,这样就需要一个稳定的契约,来保证这种沟通的有效性。

(4)经验主义编码方式

  经验主义编程方式也就是先用一个API做实验,如果不成功,就再试下一个,经验第一,然后才是深入了解,不过有时候,并不需要深入了解。

  API应该是自描述, 即用户无需任何文档就可以正确使用该API,这样的API能够引导用户利用其提供的各种功能和元素来轻松的完成任务,用在编写代码的时候,就很容易找到解决方案。 

(5)开发第一个版本通常比较容易

  事实上,第一个版本几乎从来没有完美的,但不能说这是因为第一次设计,所以设计上做的不好,这不应该成为一个借口,对于第一版本来说,设计起来总是比较容易的,而在后续版本中加以改进则比较困难,所以在第一个版本中,要尽量将其设计做到完美,同时不要忘记,今天看来完美的设计,明天可能就变得非常差,改变迟早都会出现。

  如果在设计API时就考虑到未来改进,而预先留出相应的空间,那么即使出现升级的情况,也不会给其他用户添加额外的工作。

  因为开发人员不喜欢那些不必要的工作,特别是因为库的开发商考虑不全面,最终使得库无法兼容,进而为开发人员带来一些不必要的工作,所以在开发可供他人使用的组件时,必须要考虑在组件改进的时候还能保持兼容。

第三章  评价API好坏的标准

  为什么开发一个API?

  其根本原因在于:我们希望能够将大块的构建模块“无绪”地集合成应用程序,这些构建模块,包括共享类库,框架,预先定义好的应用程序结构,以及这些内容的组合,我们相信如果每一个程序员都可以很好地完成自己所负责的模块,也就是说他们设计的API完全正确,那么程序的集成工具就会变得非常简单,集成人员不需要花费时间进行调试,阅读源代码,打补丁,更不用去考虑他人到底是如何来设计和开发一个模块的,一句话,我们完全可以在“无绪”的状态下来完成集成工作。

(1)方法和字段签名

(2)文件及其内容

  开发中还有一项内容是经常被人们所忽略的,但他确实非常重要的,那就是应用程序执行时要读写的文件以及这些文件的格式;

  先来简单说下一个例子,现在有一个telnet程序,分析一下他是怎么与一个支持kerboros验证系统进行交互的,这两个组件是不同的人员开发的,因为编写加密代码和处理套接字链接属于两个不同技术领域的范畴,但只要一个文件API就可以将两个组件相互协作,所以文件,和字符串是所有语言和系统支持的解析方式。

  telnet协议是TCP/IP协议组中的一员,是因特网远程登录服务的标准协议和主要方式,他为用户提供了本地计算机上完成远程主机工作的能力

  Kerberos协议主要用于计算机网络的身份鉴别(Authentication),其特定是登录kerberos服务之后就可以获取到票据(ticket),只需要登录验证一次,即SSO,单点登录原理,由于每个客户和服务之间建立了共享秘钥,使得该协议具有相当的安全性。

(3)环境变量和命令行选项

(4)文本信息也是API

  Unix有一个很大的特点,就是它使用了“管道”技术。很多程序不仅仅可以通过环境变量和命令行参数进行控制,还可以向一个程序输入一段文本作为参数并取得一个返回值。在任何Unix系统中,可以用管道技术吧很多程序串在一起变成一个程序。这种功能非常强大,而且易于理解,对于很多unix系统用户来说,这已经是unix系统上常用的一个功能了。同样,如果一个程序有输入值和输出值,那它就是一个API。

(5)协议

  协议是针对文本内容的API,它们用来定义网络传输中的信息格式,所以非常重要。所以在软件升级过程中,版本1,版本2先做一个握手交互,是非常重要的。

(6)行为

(7)国际化支持和信息国际化

(8)API的广泛定义

  关于“API定义”这个问题,现在看起来可能已经很清楚了,所谓的API远远不止于是类、方法、函数和签名这些东西,为了有利于在“无绪”的状态下吧一个大的系统以组件集成的方式拼装出来,从这个角度来看,APi的定义就非常广泛,从简单的文本信息到哪些复杂的或者难以控制的组件行为,都可以算出是API。

(9)如何检查API的质量

  《1》可理解性

  《2》一致性

    一致性包括两方面:a、API多个版本间保持一致,b、对外提供功能一致,比如注册工厂都应该用registerFactory,不能有的写registerFactory,有些写addFactory。

  《3》可见性

  《4》简单的任务应该有简单的方案

    一般API常用的方案:则是将一个API分解成两个或者多个组成部分,一部分针对调用功能的开发人员,一部分则放在独立的包中,供开发人员进行扩展,还有一种就是扩展系统功能的开发人员;

    API:例如:javax.naming,javax.naming.event,javax.naming.spi

  《5》保护投资

    对于API设计者来说,首要的任务就是要保护其用户投入的资源。

第四章  不断变化的目标

  开发一个软件,包括API的开发,第一个版本一般都不是完美的,在后续迭代中才会进行改进和升级,判断一个API是否优秀,并不是简单的根据第一个版本进行评估的。而是要看多年之后,这个API是否存在,是否提供完美的API,是否保持可用性。

(1)第一个版本远非完美

  第一个版本一般都比较简单,而在后续的开发和改进中才会发现很多问题,所以在第一个版本的开发中一定要三思,想好可扩展性,一般第一个版本发布之后,用户投入使用的时候,改动成本就会比较大,一般改动计划会通过两种方式,第一种就是放弃老的版本,重新开发一个新的接口,这样老用户使用老接口,新用户使用新接口,但是一般情况下对于新接口的新特性,老用户是无法兼容使用的,所以这种方式一般都是不好的,还有一种方式就是强化现有API,保持向后兼容,这样所有的客户端都不会有所改变。

(2)向后兼容

  对于每个API的设计者来说,API都渴望能做到“向后兼容”,因为不管是老用户还是现有的用户,都比较相信可兼容的API,但向后兼容性有多个层次的意义,而且不同层次的向后兼容,也意味着不同的重要性和复杂性。

  《1》源代码兼容

  比如Java 1.3可以编译通过的源码,1.4编译不通过,这就是源码上的不兼容,例如:

  

public class WrappingIOException extends IOException {

    private IOException cause;

    public WrappingIOException(IOException cause) {
        this.cause = cause;
    }

    /**
     * Getter method for property <tt>cause</tt>.
     * 
     * @return property value of cause
     */
    public IOException getCause() {
        return cause;
    }
}

以上代码在jdk1.3可以编译通过,但是在jdk1.4就会编译报错,因为编译器觉得以上代码试图去覆盖一个jdk1.4版中引入的方法,jdk1.4语言规则禁止覆盖拥有相同方法和参数但不同返回值类型的方法,否则就会编译失败,但是在jdk1.5以上就可以编译成功了,因为jdk1.5做了改进,就是可以复写相同的方法。

  《2》二进制兼容

  如果一个基于老版本类库开发的程序,在不需要重新编译的前提下,可以和新版本类库进行正常连接并执行,那么这种情况可以称作二进制兼容。如果要达到这种相互调用的灵活性,就需要开发人员去了解一些源代码编译生成的二进制格式,对于java程序员来说,去了解编译后的class文件和类的加载,下面介绍一下源代码兼容和二进制兼容之间的主要区别。

  Class文件和源代码的区别:第一个不同之处在于源代码中的*式导入是不存在的,因为再Class文件中,所有名称必须是全名,同样,一个字段或者方法的名称所包含的不仅仅是源代码中的名称,而是使用了真是的类名和方法名,对于一个方法,就表示所有的参数和返回值都使用全名,因此,在一个类文件中,其实可以有两个同名的方法,即使他们的参数名称完全相同,只要返回值不同,那么就是合法的,虽然Java源代码中不允许这么写。

  在标准的Class文件中,为了节省空间,所有类的全名都有一个缩写的编码。

  private和static的方法默认为final,不能覆盖。

  如果没有覆盖父类的任何非final方法,那么表就是空的。

  比如说有一个A类声明了public static final String a='A',然后B类使用了这个A类中的a字段,如public static final String b = A.a,如果对A中的a进行修改,使之为public static final String a='AA',如果不重新编译B,此时B中的b,其值仍然为‘A’,而不是‘AA’,这就是第一次编译B的时候,已经将确认好的A.a的值直接写入到B.b中了,这种不是引用,而是直接将值复制过去。

    二进制字节码的格式和java源代码的格式非常相似,这有好的方面,也有不好的一面,说好是因为他的格式很容易理解,说它不好,是因为它会引发误解,但大家应该记住,在编写API的时候,只要通过二进制格式才能最终判断不同版本的API是否兼容,也就是说代码执行时的兼容性才是最根本的,所以一定要了解Java源代码编译成何种样子的字节码,如果有疑问的话最好反编译下class文件,检查一下到底是不是自己期望的样子,也许会大吃一惊的。

  《3》功能兼容---阿米巴变形虫效应

  如果所编写的代码在版本1和版本2中可以编译通过,那就是源代码兼容成功,如果编译后的代码执行,可以在新版本和老版本上连接成功,就说明二进制兼容,如果说新版本与老版本运行的结果相同,就说明是功能兼容。

  阿米巴—一种变形虫属或相关属的原生动物,存在于淡水与土壤中,或为其他动物的寄生虫,变形虫没有固定形体,身体主要由原生质组成,包括由一个柔软、有韧性的外膜包围的一个或一个以上的细胞核。

  所以需要API设计者的目标就是减小阿米巴效应,要让API的功能行为尽可能的与规范保持一致,这事绝对不简单,需要开发人员对API要完成的功能有清楚的认识,同样还要有良好的技术水平,才能将自己的意图贯彻到代码上,此外还要评估一下API的用户会如何来使用。

(3)面向用例的重要性

  设计API有两种方式:一种是找一些用户,对其进行研究API的受众范围,第二种就是基于用例,基于用例也是站在用户的角度,然后对部分用例进行针对性的处理,从用户处取得反馈信息,并对用例进行研究,这是一种很好的工作方式,可以通过反馈来检验设计的用例是否正确,在做设计之前,必须进行分析,明确为什么要写这样的API,API应该长什么样,以及如何完成目标。

(4)API设计评审

  对于API的设计,以前一般是一个架构师在设计,而发现最后的瓶颈就是架构师本身,进度会比较慢,所以最后只能由一个架构师带领一帮技术比较好的人,进行API设计开发,人员多了就会导致API的风格不一致情况,因为每个成员都有自己独特的风格习惯,所以最后每个成员设计的API都会提交评审,由大家评审通过,设计API大家评审的也是有一定的规则:(1)用例驱动的API设计。(2)简单明了,(3)少即是多,(4)API设计的一致性,(5)支持改进。

(5)一个API的生命周期

  API开发的过程就是沟通交流的过程,沟通的双方就是API用户和设计者。

  对于自发的API,往往会变成前面说的那种private和friend类型的API,一旦有人发现这种API,并且感觉比较有用,才会慢慢的演化成稳定的API,而另一种从宏观设计开始的API,则在一开始就是出于开发阶段,经过一段时间的开发以后,再正式第一次发布,成为稳定的API,其中含有所有的承诺和保证。

(6)逐步改善

 总而言之,还是要准备增量改进的方式,人们需要软件加以改进,但改进引入的伤害也应该是最小的,特别是要重新编写的那种大变化,如果因为API设计上的问题,使得无法增量改进,那么也许会有充足的时间进行重新编写,但这种大的变化应该限于开发方式上的一些基础性变化,如果发生很大的变化,我们会同时强调要为一个API提供多个大版本类库,只有这种方式才能保证API能力变得更好,而且是API的用户的痛苦最小化。  

 

原文地址:https://www.cnblogs.com/bqcoder/p/6093684.html