这里侧重设计模式的角度,具体结合tomcat细节的运行机制,准备在另一篇写。
只有先把这个模式抽出来理解清楚,再看tomcat的实现会更简单。
pipeline模式并不难理解,类似servlet规范中的filter。指的是一个pipeline,需要多个阀门串行处理,流水线作业。
那么很简单,我们可以写出如下的代码:
import java.util.ArrayList;
import java.util.List;
public class Pipeline {
List<Valve> list = new ArrayList<Valve>();
public Pipeline(){
list.add(new Valve1());
list.add(new Valve2());
list.add(new Valve3());
list.add(new Valve4());
}
public void invoke(){
for(Valve valve : list){
valve.invoke();
}
}
public static void main(String args[]){
new Pipeline().invoke();
}
}
public interface Valve {
void invoke();
}
public class Valve1 implements Valve {
public void invoke() {
System.out.println("valve1 invoke");
}
}
其余valve类似。
上面的代码实现很简单,也可以完成pipeline的模型,既然如此,为什么还需要搞一个设计模式出来?
必然是上面的代码存在一定的问题:
没有抽象。分两层,一是对外提供服务,调用pipeline的使用方直接依赖了pipeline的具体实现,我们说软件设计一个重要的原则是“依赖倒置”,如果我们的功能是对外提供的,那么最好是以接口这种稳定的方式提供,这不是仅仅在pipeline模式中适用的,而是所有软件设计都应该重视的。依赖接口的好处是我们可以随意变更接口的实现,而依赖方不需要很大的修改。举个例子,上面的pipeline是基于list实现的,现在我想要添加一个array实现的,而且调用方在某种情况下需要调用array实现的,那么我们必须新建一个ArrayPipeline的类,然后在需要调用array的地方把创建pipeline实例的方式改了,调用方式也有可能改。如果我们使用了接口,那么任何调用方的代码不需要需改,只需要修改接口的赋值语句。如果使用了spring,那么只需要修改一个bean的定义文件即可。二是对内实现上,上面的实现pipeline和valve是未解耦的,问题和对外的一样,需要使用接口解耦,其次pipeline的构建是硬编码的,事实上,我么很有可能需要动态添加valve,因此pipeline需要有addValve之类的接口。
综上,上面的代码最大问题是没有解耦,在之后很难做扩展。因此需要抽象出每一种角色的职责。这其实也是所有设计模式出现的初衷,设计模式很大程度上就是为了抽象,让实现以接口的方式进行。
重新设计:
public interface Pipeline {
void setBasic(Valve valve);
void add(Valve valve);
void remove(Valve valve);
void invoke();
}
import java.util.ArrayList;
import java.util.List;
public class StandardPipeline implements Pipeline {
List<Valve> valves = new ArrayList<Valve>();
Valve basic;
public void setBasic(Valve valve) {
basic = valve;
}
public void add(Valve valve) {
valves.add(valve);
}
public void remove(Valve valve) {
valves.remove(valve);
}
public void invoke() {
for(Valve valve : valves){
valve.invoke();
}
}
}
抽象出了valve和pipeline的职责,就形成了上面的代码。
tomcat的pipeline规定必须有有个basic的valve,该valve是该流水线的最终处理器,完成这条流水线的主要职责,basic前可以没有valve,但是basic是必须存在的。就好比可以没有前面的filter但是必须有最后的serlvet来处理一个http请求。
这里的pipeline的实现是一种for循环方式,如果每一个valve可以决定是否打断当前的执行链,这种for循环就无法处理了。当然,我们可以让每一个valve返回一个boolean值,但是这种方式代价高昂,前提是valve本省没有返回值。如果是类似linux管道的模式,每一个valve要处理上一个valve的返回值,那么这种方式就不可行。而且,从逻辑上讲,是否往下执行,是在一个valve内部需要解决的事情,pipeline不应该干涉。
所以上面for循环的方式假定了每一个valve都必须执行一次,没有打断执行链的能力。
为了更优雅的解决这个问题,tomcat的pipeline模式加入了一个新的角色context。那么最后的设计变成了这样:
public interface PipelineContext {
void invokeNext();
}
public interface Valve {
void invoke(PipelineContext context);
}
public class Valve1 implements Valve {
public void invoke(PipelineContext context) {
System.out.println("valve1 invoke");
context.invokeNext();
}
}
import java.util.ArrayList;
import java.util.List;
public class StandardPipeline implements Pipeline {
List<Valve> valves;
Valve basic;
PipelineContext context;
public StandardPipeline(){
valves = new ArrayList<Valve>();
context = new StandardPipelineContext();
}
public void setBasic(Valve valve) {
basic = valve;
}
public void add(Valve valve) {
valves.add(valve);
}
public void remove(Valve valve) {
valves.remove(valve);
}
public void invoke() {
context.invokeNext();
}
private class StandardPipelineContext implements PipelineContext{
int step = 0;
public void invokeNext() {
if(!valves.isEmpty() && step < valves.size())
valves.get(step++).invoke(context);
else
basic.invoke(context);
}
}
public static void main(String args[]){
Pipeline pipeline = new StandardPipeline();
pipeline.setBasic(new BasicValve());
pipeline.add(new Valve1());
pipeline.add(new Valve2());
pipeline.add(new Valve3());
pipeline.add(new Valve4());
pipeline.invoke();
}
}
取消了pipeline里invoke方法的for循环。添加了一个context的角色。之前说每一个valve要有决定是否打断链子的能力,那它必须获取到一些全局的信息才行,这里的context就是为了让valve具有这种打断能力。context提供了valve执行下一个valve的能力,这样valve在内部就可以通过if来判断是否需要执行下一个valve。
context本身的实现像是一个游标,提供了一个接口来滑动游标。这样,pipeline里invoke方法就像是一个迭代器模式,没有for循环。
输出: