系统的扩展性

stars
本文谈下我个人对“系统的扩展性”的看法。首先需要声明,这里的扩展性不是指伸缩性(scalability),而是指灵活性(flexibility)。一个设计良好的系统,就像星空一样宽广

名词理解

这里有2个关键词,一个是系统,一个是扩展性

那么要说明这个主题,就先要解释一下这2个关键词。按照我的习惯,还是从最小的东西开始举例子

“系统”,我认为就是三要素,输入,逻辑,输出。按照这个定义,最小的系统就是一个方法

1
2
3
public String sayHi(String name){
return "hi "+ name;
}

我认为这个方法就是一个系统,输入是name,逻辑是加上一个”hi “,输出也是一个字符串

这三要素可以为空,比如有无参数的方法,无返回值的方法,或者没有业务逻辑的方法。。在极端情况下,甚至可以三要素都为空,不过就不具现实意义了

1
2
public void doNothing(){
}

这个方法没有意义,不过不可否认它也是符合语法的,也有三要素,只不过都为空而已,所以我认为这个也可以算一个系统

因此,从方法往上延伸,类和模块也是系统;淘宝商城,去掉展现层,也是系统;常用的spring,一般认为是个框架,不过其实它也是系统;tomcat是一个servlet容器,但是它也是系统

下面解释我理解的扩展性。这个词我给不出精确的定义,所以只能用大白话加例子来说明。我认为扩展性就是“可变化”,或者说“没写死”

比如说这个方法,我认为没有扩展性

1
2
3
public void sayHi(){
System.out.println("hi kyfxbl");
}

不管怎么调用,它也只能在屏幕上打出hi kyfxbl这句话,所以是“不可变”的,“写死了”,没有扩展性。加一个参数,就有扩展性了

1
2
3
public void sayHi(String name){
System.out.println("hi " + name);
}

这个方法根据被调用时接受参数的不同,会呈现出不同的结果。结合第一点说的,方法也是系统,那么可以认为,这个方法,是一个最小的“有扩展性的系统”

系统即容器

上面为了解释清楚“系统”和“扩展性”的意思,选了很极端的例子,没有什么实际意义。现在就回到常见的场景中

还是以tomcat来举例子,tomcat作为一个servlet容器,也是一个运行的系统,而且是一个扩展性非常强的系统

当tomcat启动并加载一个web app的时候,它并不知道这个web app有哪些servlet规范规定的组件。有多少个Servlet,有没有Listener,都不清楚。但是当它启动web app初始化流程的时候,它就找到ContextListener的组件,然后实例化,再调用生命周期方法。所以如果我们的web app应用没有ContextListener,那么就不会执行;如果有一个,就执行一个;如果有两个,就执行两个

这种感觉就像是,我们在给tomcat写插件一样,或者说我们在根据servlet规范的要求,写一些组件,然后放到servlet容器里运行起来

再举一个spring的例子,spring是常见框架,并且也是一个扩展性很强的系统。大部分情况下,我们只是使用spring提供的功能,比较少会去扩展它。但是实际上是可以扩展的。比如在ApplicationContext初始化的过程中,它会去调用PostBeanDefinitionProcess,PostBeanDefinitionProcess是一个接口,我们完全可以自己增加一个自定义的PostBeanDefinitionProcess实现类,放到配置文件里,那么也就会被spring给执行了

这种情况下,spring的各个组件实际上都是在spring这个容器中运行的。spring容器本身运行的流程,是spring设计者规定的(相当于servlet的规范)。同时,设计者在流程中预先就保留了一些扩展点,等着后来的人(spring的用户)去自行扩展。因此,spring能不能扩展,能在哪些环节被扩展,是在设计的时候就决定的。试想如果rod johnson在一开始,就没有设计查找并执行PostBeanDefinitionProcess这个环节,那我们就不能在这个环节上对spring系统进行扩展了

日常我们写的系统,也是这样。以后能不能扩展,在哪里扩展,怎么扩展,都是在设计的时候就决定的。如果系统中,某个环节调用某个组件,都被固定下来,是“写死的”,那么这个环节就不具扩展性了。当然这个时候,系统依然是容器,只是容器中的组件是不可变的

接口即设计

因此,为什么说接口就是设计呢,我认为就在这里。以前另外一篇博客,也谈到过接口的作用。当时我说的是,接口可以使两个模块之间的依赖减小。比如模块A依赖模块B,但是A只依赖B中的一个接口,那么不管模块B的内部实现怎么变,只要这个接口是稳定的,对A就没有影响

这里要说到接口的第二个作用,就是它关系到系统的扩展性。拿一段代码举例子:

1
2
3
4
public void process(){
ComponentA a = new ComponentA();
a.doProcess();
}

这个方法就没有扩展性了,因为不管在任何情况下,调用process()方法,执行序列都是一样的。要想改变它,除非拿到源码,把ComponentA的实现改掉。当然在实际中,这是不可能的。那么如果是这样写

1
2
3
4
public void process(){
Component a = searchForComponent();
a.doProcess();
}

searchForComponent()方法的逻辑,是从配置文件里找到一个Component接口的实现类,实例化并返回,那么这个方法就很有扩展性了

随时都可以自行实现一个实现Component接口的实现类,比如MySpecialComponent,放到配置文件里。那么这个方法运行时的逻辑,就完全是灵活的了

接口Component在这里,就是充当了一个占位符的作用,但同时又把整体的流程确定了下来,先找到实例,然后调用实例的方法。我们可以随意扩展Component,但是这个整体的流程却是不会改变的

联系上面的内容,这个process()方法(系统),也是一个容器。Component就是它的组件,是不确定的

想想前面举的tomcat和spring的例子,它们之所以有扩展性,就是因为容器本身的代码,用的是接口(ContextListener和PostBeanDefinitionProcess)来占位,而没有用实际的类把逻辑“写死”。但是整体的流程,是固定下来的。这里就体现了接口,对于设计的意义

可扩展性的要素

总结上面的例子,一个系统是否具有扩展性,是在设计之初就固定下来的。

那么系统要有扩展性,就至少需要3个要素:

1、在规定了流程的前提下,允许某些环节扩展

2、将允许扩展的部分,以API的方式对外部提供

3、对外部提供规则

第1条前面已经说了很多了,第2条和第3条也很简单。servlet和spring是可以扩展的,但是如果没有拿到ContextListener和PostBeanDefinitionProcess这2个接口,又怎么能写出实现类呢。那也就只能看着系统运行默认组件了

但是提供API,不需要把整个系统的接口都暴露出去,提供必需的子集即可。比如只需要有servlet-api.jar,就可以开发servlet应用了,并不需要拿到catalina.jar。tomcat的源码里,大部分都是容器自身的实现,允许扩展的部分,仅仅通过servlet-api.jar来提供就足够了

对外部提供规则,就是告诉外部要怎么扩展。容器是别人设计的,用户怎么知道要怎么扩展呢?当然就需要容器的设计者来提供信息。告诉用户,你可以实现ContextListener接口,然后在web.xml里配置一下

插件设计

写本文的原因,其实是最近在分析一个系统。这两天在研究它的插件体系,想到这么多就写下来

搞清楚上面的内容,插件也就不复杂了。从用户的角度来看,要写一个插件,就是拿到API,然后按照规则写扩展组件。从系统(容器)设计者的角度看,我的系统要支持插件扩展,就是:

1、规定流程,设计扩展点(包括加载机制)

2、把扩展点打包成API,作为二次开发的SDK提供给用户

3、告诉用户,应该怎么使用这个API