参考资料:
《设计模式——可复用面向对象软件的基础》
《Object Design: Roles, Responsibilities, and Collaborations》
http://en.wikipedia.org/wiki/Extensibility
从我理解,在软件范畴上,是软件系统本身的属性,或者进一步说是设计的属性,代码的属性。因为我们经常说设计的可扩展性,代码的可扩展性。那与之相对应的是什么呢?是变化,软件环境的变化(可能是业务环境,运行环境)导致软件要进行改动才能满足人们对它的要求,这种系统本身适应变化的能力就是可扩展性。
首先让我们来看看目前系统被扩展的几种形式:
1)直接修改代码。
相信大家都有过这样的经历,当需求改变或者增加新需求的时候。我们可能会修改多个类文件,可能还涉及配置文件,前台页面文件。这种改动,肯定要引起重新编译,打包和部署,肯定是需要停机的。这种改动涉及的面积广,需要预先经过很细致的分析,改完之后需要面积很大的回归测试以保证修改不会引入新的问题。
2)直接修改代码,但只局限在一个类或方法中。
这种虽然也是直接修改代码,但改动的范围受到了限制,对系统其它部分的影响也不是那么大了,分析起来相对容易。需要进行回归测试的范围也比上面的做法小。依然需要重新编译,打包和部署。
3)利用继承,编写子类继承以后的类,在子类里添加新的业务逻辑。
这种做法,没有修改以前的任何业务逻辑代码,而是增加了新的类来容纳新的业务逻辑。但是,在将新的类链接到代码中,同样需要改动部分代码。比如对新类的实例化过程是需要静态编译链接的。
ClassA{
void doSomething();
}
......
ClassA a = new ClassA();
......
ClassB extends ClassA{
void doSomething();
void addedMethod();
}
......
ClassA a = new ClassB();
......
有人说用工厂方法,简单工厂不行,FactoryMethod也不行,都达不到消除实例化新类的代码。
4)运行时对象装配。 接下来,大家可能想到了,那就是将类的实例化逻辑移到运行时,通过反射,来进行装配,这确实解决了第3)种问题。Spring目前做的主要就是这些事情。到这个时候才真正满足开闭原则。 Spring给我们提供了一个非常灵活扩展的基础技术架构。关于这个,可以看看Martin Fowler关于依赖注入 的文章。
5)基于模块的运行时动态扩展。 到现在为止,我们讨论的都是比较细粒度的在技术上的扩展。想象一下,如果增加一些功能,就去继承各种各样的类,然后经过复杂的装配过程,才能完成一个功能的添加和改进。即使你新增加了类,改动了Spring的配置文件,实现新的装配,但仍然有一点,你需要编译整个项目。也就是说,前四种都没有达到模块级别的扩展。
OSGi给出了解决方案,可以实现模块级别的动态扩展,而且是运行时的。所谓运行时模块的动态扩展,比如说你需要增加一些新的功能,你可以将新开发的类和文件按照Bundle进行组织,然后直接扔到运行时环境下,这些功能就可以用了。
Eclipse的插件体系结构就是以equinox(一个OSGi规范的实现)为核心构建的。它里面提到了扩展/扩展点的概念,这是一个远比前面四种解决方案更灵活的解决方案,而且与4)相比,实现了关注点的分离。举个例子:
比如说我开发了一个上传文件的功能,上传文件类型和上传的地址可以动态进行扩展。
Spring处理方式: 在bean配置里灵活配置上传类型和上传地址,每增加一个地址或文件类型,都要对该配置文件进行修改。更可怕的是,张三开发了这个文件上传模块,开发别的模块的李四如果要用这个功能,需要和张三沟通,要么自己去改配置文件,要么要请求张三添加自己的文件上传路径。当系统规模增大,改动次数增加,这种复杂性几乎是不能管理的,需要非常大的成本。
Eclipse的实现方式:张三开发了一个文件上传插件,里面暴露出两个扩展点,一个是文件类型,一个是文件上传地址。
李四甚至不用和张三打招呼,自行开发两个扩展,并注册到扩展点上。张三在运行时收集扩展,进行处理。实现了功能使用和功能定义的关注点分离。大家只要遵守扩展的契约就行了。
6)基于中间语言。 框架通常定义了一些Hotspot(热点),在这些点上,可以进行扩展。平台,可扩展性是最强的,比如window 平台,你可以开发应用程序运行在上面。windows提供了几千个API,你可以使用他们来开发应用程序。我们在看看Firefox,他可以从主题,插件,扩展来扩充。我们常接触的扩展,也叫Extension可以通过XUL,XBL,Javascript,CSS来进行开发。从思路上看,他和windows的做法是一样的,都是通过中间的某种特殊的语言来进行扩展,只不过Windows支持的语言更多一点罢了。由此推断,从技术上来说,要获得最大的可扩展性,就要通过一种或多种中间语言来进行扩展。比如现在流行的OpenSocial API和Facebook都是这个思路。这样说来,DSL的出现就是顺理成章的了,它是为了满足在某个业务领域的扩展而设计的。思路还是和前面说的一样。 说白了,就是顺序,选择和循环可以表达所有逻辑,这是证明过的,这是语言能够带来灵活性的本质。
到目前为止,从技术层面讲,这应该是最灵活的方式了。但是,模块如何划分呢?大还是小?之间的依赖如何进行控制?为什么要定义这个扩展点?
这就引出另外一个问题,“我们为什么而扩展?”
扩展是要有目的性的,我们不能盲目为了扩展而扩展,把系统搞得极其复杂,来应对几乎不可能发生的变化。说道这里,又谈到设计。传统软件开发过程讲究Big Front Design,就是大量的前期设计。XP的一个实践是简单设计,我比较推崇简单设计,就是刚刚好的设计。不多也不少,就像五花肉,肥而不腻。如何做到这 些,只能依赖于设计者本身的经验了。
回答这个问题,重要的是我们看的多远,能够预测到多少可能的变化。这些都是基础。还有一个就是你对上下文的了解程度,这个需要知识的补充。上下文知识越丰 富,对其中的联系就越清楚,那也就越能识别出系统可能的变化。而我们设计系统时,就可以为这些变化,准备一些Slot,设计一些扩展点。可扩展性是设计出来的。
如何把握这个分寸,确切的说,我也不知道,Context-special。
总结一下:
1)为了控制复杂性,系统必须在逻辑上进行划分,各个逻辑块之间应该是松散耦合,需要对领域的理解和仔细分析。
2)可扩展性不是一蹴而就的,是需要随着你对业务领域理解的深入而不断重构获得。一般三次可以达到比较理想的程度。
3)在系统演化过程中,时刻准备着,保持对复杂性的关注。确保这些复杂性得到消化。
4)要想获得可扩展性,你需要看得远一点,对上下文做充分的了解。
人无远虑,必有近忧。
系统也一样。