有时候,程式设计者的工作不是设计应用系统,而是设计、开发API,供其他的程式设计
者用以开发系统。而API本身,也和一般的应用系统一样,会基於需求的变化,而有版本
的演化。
在前一回中,我们提到了API的版本变化,可能发生了影响层面较小的实作内容改变,也
可能发生影响层面较大的介面变动。
当新版本的API仅仅只是发生实作内容的改变,而在介面上没有变动时,便可以维持向下
相容的特性,也就是说,原先使用旧版API的客户端程式码,在升级到新版API之後,并不
需要做任何的更动,就可以继续正常的运作。
但是,倘若API介面有了变化,这将会使得原先相依於该介面的客户端程式码,必须进行
相对应的修正,才能够维持运作。此即发生了不能向下相容的情况。
发生不能向下相容的情况,对客户端程式设计者来说,是一件相当麻烦的事情。因为在客
户端程式码中,倚赖API所改变的介面处可能散布在各处,必须一一找出来,并且进行相
对应的修改。
而这修改程度,影响小的可能只是更改名称,只需要更换程式码中所使用的名称即可。但
大也可能关系到整个程式设计的模型或架构都完全改变,客户端程式码必须做大幅度的调
整才能因应。不论如何,影响到客户端的程式码,都会造成程度不等的冲击。
所以,除非可以带来明显又够多的好处,否则API的设计通常会尽量维持向下相容性。
那麽,在设计上要如何保持API介面的向下相容性呢?在前一回中,我曾提到,介面的长
相可以说是「语法(syntax)」,而其行为可以说是「语义(semantics)」。无论是语
法或是语义的变动,都可以算是API介面的变动。
当我们必须引入新的语法或是新的语义时,都会造成API介面的改变。改变是势在必行,
但是改变不必然破坏向下相容性,只要这个改变不会影响到使用API旧语法,或旧语义的
现有程式码即可。要怎样才能做到这件事呢?一个最简单的方式,就是利用「扩展」语法
或语义的方式。
举例来说,就像微软的Windows作业系统API时常使用的手法,某个原有的API函式名称可
能名叫LegacyAPI,但是,随着时空环境的演变,设计者发现LegacyAPI的功能不足以满足
新的需求了,但是,已经有众多既存的应用程式,都在程式码中呼叫了LegacyAPI,一旦
直接修改LegacyAPI的行为(语义),或是传入的引数列表及回传型别(语法),就会影
响到这些既存的程式码。
而他们常用的解决之道,就是透过增加名为LegacyAPIEx的API(其中 Ex 指的是Extend,
即扩展之意),来提供新增的API语法或语义,如此一来,就不会影响到旧有的既存程式
码。
而要使用新语法或语义的新客户端程式码, 则直接使用Ex 的API版本。
这样子来解决问题,当然是比较简单的手法,但是当需要持续,也就是不只一次扩展API
的语法或语义时,就必须不断增加新的API函式,於是接下来会有 Ex2 、Ex3。
一旦增加的次数多了,就会失去API设计该有的简洁特性,同时也容易造成客户端程式设
计者的混淆,因为很难从名称上,去区分名称类似的各种不同扩展版本间的差异。
这种情况,对於使用支援重载(overloading)特性语言的API来说,问题就会少了许多。
像C++或Java之类的物件导向语言,都支援函式的重载,这使得以C++或Java写成的API,
在扩展函式的语义或语法时,可以使用同样的名称,同时搭配不同的引数列表,藉以提供
不同的语法及语义。这麽一来,虽然会有多个同名的函式,但是起码它们的名称相同,在
简洁特性上不致於大打折扣。
扩展API函式时,使函式因而变得更通用
很多时候,我们在「扩展」同名的API函式时,其实,往往是试着让这个函式更通用化(
generalized)。也就是说,因为我们察觉到原先的API过於局限,只能满足特定的需求,
而更广泛的需求浮现了,所以我们需要一个更通用的版本──可以涵盖原有需求,但又提
供更多的可能性。
换言之,在实作时,尽管仍然维持旧有的API,但是因为已经引入了更通用的API,所以改
版之後,旧有API大多都没有真正的实作,而是直接运用新版、更通用的API来达成旧有
API的作用。
有些时候,扩展的动作不只发生在函式名称这种规模的层次之上,而是发生在类别层次上
。同样的,扩展是基於理解了更通用的需求,想提供更通用的功能,只是变化的范围,不
仅影响到个别的函式,而是影响到特定的类别。举例来说,在JDK 1.1之前就有个
java.uti.Vector的类别,它是直接继承自java.lang.Object,并且提供我们所熟知的「
Vector」的作用。
但是,到了JDK 1.2时,API的设计者认为整个Collection的API需要做大规模的重新设计
,因而在继承体系上做了大幅的调整。在JDK 1.2之後,java.util.Vector成了继承
java.util.AbstractList的类别,并且透过AbstractList得到了许多通用的功能。即使
Vector也因为这样的重新设计,经由继承得到了一些新的函式,但是,旧有的函式也都获
得了保留,例如在JDK 1.0时代就存在的addElement()函式,即使在JDK 1.2之後,透过
AbstractList得到了add()函式,但addElement()依旧存在。
事实上,「叫用通用版本」仍然是扩展之後最常看到的手法,所以介面上维持
addElement()继续存在,但它不过也只是叫用扩展後的add()罢了。
扩展可以发生在函式层级、发生在类别层级,当然也可以发生在一整个类别族系。基本上
其精神就是维持旧有介面存在、行为不变,使得旧版本的语法及语义,得以继续使用下去
。但是,因为扩展後得到的是更通用的语法及语义,所以原有介面的实作,都可改用新版
实作,而不需要有所重复。
透过扩展,一方面提供更通用的功能,一方面也能维持向下的相容性,但是,毕竟还是留
下了额外的负担,也就是旧有的介面必须存在。以上述所提到的Vector为例,同一个类别
就必须同时拥有addElement(),以及add()这两个作用其实一模一样的函式。这说明了设
计API时在介面的设计上需要更多的思量,实作的内容易於演化,但是介面一旦制定,要
再加以更动就是大事了。
这同时也告诉我们,即使当前的需求十分局限,并不需要太通用的实作,但是,我们宁可
在设计时提供更通用的介面,来包装特定的实作,日後再来逐步扩充实作,也不要在一开
始就设计出受限的介面,接着才在日後逐步的加以扩展。
一般来说,现在大多不鼓励设计者「过度工程化(over-engineering)」,希望设计者尽
量只先满足当前的需求,不要对未来的需求有过度的想像,在设计当下便加以考虑,而在
日後才逐步的演化。
但是对於API设计来说,由於更动介面所造成的影响甚大,要保有向下相容性,势必也需
要付出一定的代价,所以,在设计API时,就必须在过度工程化的可能性,以及保有适度
的扩充性之间,取得一个好的平衡性了。
--
plurk
http://www.plurk.com/dasea2017
face book
0911457604,0934169099
google+
https://plus.google.com/u/0/
--
※ 发信站: 批踢踢实业坊(ptt.cc)
◆ From: 61.58.22.74
※ 编辑: dasea2008 (210.66.169.48 台湾), 04/26/2021 05:29:31
※ 编辑: dasea2008 (101.0.228.86 台湾), 04/11/2022 23:33:44