论坛首页 Java企业应用论坛

扩展接口的思考

浏览 8738 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2007-06-06  
在设计MeteorTL时,
模板指令,表达式操作符的可扩展性,一直没有找到好的方案。
由于扩展是由第三方实现的,所以一般采用SPI(Service Provide Interface)接口的方式。
在留出SPI接口时,引擎需要传递编译期数据及运行时数据给扩展方,
运行时数据肯定是用callback函数的参数列表传递,
但编译期数据是其应持有的状态,所以必需在初始化时给予。
以指令为例:
public interface Directive extends Serializable {

	public void interpret(TemplateContext templateContext) throws DirectiveException;

	public String getName();

	public Expression getExpression();

	public NestDirective getParentDirective();
}

其中,
templateContext 为运行时数据,在callback函数中传过去。
name, expression, parentDirective 为编译期数据,
是在引擎将模板解析成树时,应该注入的。

这里的问题主要在于编译期数据怎么传给扩展者。
下面是我暂时想到的方案。

方案一:
约定构造函数,如:
public class OutDirective implements Directive {

	private String name;

	private Expression expression;

	private NestDirective parentDirective;
	
	public OutDirective(String name, Expression expression, NestDirective parentDirective) {
		this.name = name;
		this.expression = expression;
		this.parentDirective = parentDirective;
	}

	public void interpret(TemplateContext templateContext) throws DirectiveException {
		......
	}

	public String getName() {
		return name;
	}

	public Expression getExpression() {
		return expression;
	}

	public NestDirective getParentDirective() {
		return parentDirective;
	}
}

这种方法契约性太差,实现者可能要在看了文档后才会知道构造函数的约定。

方案二:
指定初始化函数,如:
public interface Directive extends Serializable {

	public void init(String name, Expression expression, NestDirective parentDirective);

	public void interpret(TemplateContext templateContext) throws DirectiveException;

	public String getName();

	public Expression getExpression();

	public NestDirective getParentDirective();
}


public class OutDirective implements Directive {

	private String name;

	private Expression expression;

	private NestDirective parentDirective;
	
	public void init(String name, Expression expression, NestDirective parentDirective) {
		this.name = name;
		this.expression = expression;
		this.parentDirective = parentDirective;
	}

	public void interpret(TemplateContext templateContext) throws DirectiveException {
		......
	}

	public String getName() {
		return name;
	}

	public Expression getExpression() {
		return expression;
	}

	public NestDirective getParentDirective() {
		return parentDirective;
	}
}

这个方案在方案一的基础上改的,使用init函数初始化,当然还必需有无参构造函数。
init函数在接口中定义,是为了保证明确的契约,
但也因为如此,接口暴露了init函数,init可能被非法调用,
因此引擎在初始化指令时,就需要包一层代理:
public class ProxyDirective implements Directive {

	private Directive directive;

	public ProxyDirective(Directive directive) {
		this.directive = directive;
	}
	
	public void init(String name, Expression expression, NestDirective parentDirective) {
		throw new UnsupportedOperationException("此方法为初始化setter,只允许引擎调用!");
	}

	public void interpret(TemplateContext templateContext) throws DirectiveException {
		directive.interpret(templateContext);
	}

	public String getName() {
		return directive.getName();
	}

	public Expression getExpression() {
		return directive.getExpression();
	}

	public NestDirective getParentDirective() {
		return directive.getParentDirective();
	}
}

上面两个方案都有个缺陷是,初始化工作都由引擎完成。
一般会将OutDirective.class的Class类元传给引擎,
当引擎解析到out指令时,就会用Class.newInstance()创建一个实例并初始化它作为指令树的一个节点。

这样第三方在实现OutDirective就会受到诸多限制。
最大的坏处,就是对IoC的破坏,
假如,第三方在实现OutDirective时,需要一些配置,如:默认格式化串等
由于实现者没有指令对象的生命周期管理权,根本没法注入依赖(不管是构造子注入还是setter注入等)。
这就会迫使实现者直接取配置或其它辅助类。
如:
public void interpret(TemplateContext templateContext) throws DirectiveException {
	MyConfig myConfig = MyConfig.getConfig("config.xml");
	ToStringHandler handler = new NotNullToStringHandler();
	......
}


方案三:
使用Handler回调方式
鉴于上面的缺陷,可以将Directive实现为通用类,
通过回调一个Handler接口,
将所有编译期数据和运行时数据都通过callback函数的参数列表传递。
如:
public interface DirectiveHandler {

	public void doInterpret(TemplateContext templateContext, 
		String name, Expression expression, NestDirective parentDirective) 
		throws DirectiveException;

}

public final class Directive implements Serializable {

	private String name;

	private Expression expression;

	private NestDirective parentDirective;

	private DirectiveHandler directiveHandler;
	
	public Directive(String name, Expression expression, NestDirective parentDirective, DirectiveHandler directiveHandler) {
		this.name = name;
		this.expression = expression;
		this.parentDirective = parentDirective;
	}

	public void interpret(TemplateContext templateContext) throws DirectiveException {
		directiveHandler.doInterpret(templateContext, name, expression, parentDirective);
	}

	public String getName() {
		return name;
	}

	public Expression getExpression() {
		return expression;
	}

	public NestDirective getParentDirective() {
		return parentDirective;
	}
}

这样,因为DirectiveHandler不持有状态,
可以只向引擎供应一个实例,而不是类元,对其构造函数也不再有要求。
如:
DirectiveHandler outDirectiveHandler = new OutDirectiveHandler("xxx.xml"); // 构造子注入
outDirectiveHandler.setConfig("xxx.xml"); // setter注入
....
将IoC组装完成后的实例注册到引擎:
Map handlers = new HashMap();
handlers.put("out", outDirectiveHandler);
引擎在解析到out指令时,就会new Directive(name, expression, parentDirective, handlers.get(name));
这样,Directive就变成一个final的内部class,不再是SPI接口。
而换成DirectiveHandler作为SPI接口,其实例的生命周期可自行管理。

暂时只想到这里,
如果有更好的方案请指教,谢谢。
   发表时间:2007-06-27  
对于指令的包装,我个人觉得,可以不采用接口,把Directive变成抽象类,所有指令都继承它,ini方法变成私有,这个方法,只由引擎通过反射去调用它,这样就可以保证指令初始的封装性了。
0 请登录后投票
   发表时间:2007-06-27  
而且,这样做,有个好处,可以把对于指令的一些想约束的逻辑封装,只暴露那些可以给第三方扩展的方法,由这些方法行为定义新的指令,保证引擎的健壮和安全。
0 请登录后投票
   发表时间:2007-06-29  
谢谢你的提议。
init是可以通过关闭类的访问级隐藏(如PrivateAccesser工具类的做法),但Directive做为抽象类,还有一点就是会限制他的子类必需是无参构造函数的。因为指令树的每一个节点都需要一个新的实例。引擎只能通过Class.newInstance()方式创建这些实例。如果用Handler就可以避免。
0 请登录后投票
   发表时间:2007-06-29  
>引擎只能通过Class.newInstance()方式创建这些实例

可能我没表达清楚我的意思,我的意思是引擎保存指令的实例,实例怎么构造很随便,由实现者构思,那么在指令被执行前,由引擎调用init方法,引擎怎么调用私有方法,很简单,如下面例子:


Class ll = Class.forName("*.*.Directive");  
Method init= ll.getDeclaredMethod("init",参数Class);

//设置安全访问权限级别,允许访问私有方法
init.setAccessible(true);
		
t = init.invoke(指令实例,方法参数实例);
0 请登录后投票
   发表时间:2007-06-29  
就是说,引擎能够提供什么数据给指令都是由引擎调用指令的私有init方法传递给指令的实现类,实现类自己有什么其他属性需要构造由它自己来定,不是很明白为什么要newInstance来实例一个指令?
0 请登录后投票
   发表时间:2007-06-29  
指令也应该是一个抽象类,指令肯定会涉及到一些内在行为需要约束,而不是由扩展者随意实现,有了访问私有方法的办法,应该可以解决约束性和封装性,就像你第二个方案提出的矛盾是可以很好解决的,看看jdk的很多模型的实现基本都采用这种方式。
0 请登录后投票
   发表时间:2007-06-30  
纠正一个笔误,应该init方法要改成protected,否则估计子类通过反射会找不到这个方法,呵呵
0 请登录后投票
   发表时间:2007-07-02  
leadyu 写道
不是很明白为什么要newInstance来实例一个指令?


问题就是你下面一句:

leadyu 写道
t = init.invoke(指令实例,方法参数实例);


这里“指令实例”从哪里来?

而且这个指令实例是每个节点需要新的实例,
如:
@if{xxx}
...@if{yyy}
......
...@end
@end
这里的if指令就有两个实例。

所以指令实例的创建就由引擎负责,
最简单的办法就是Class.newInstance();

用Handler,所有指令可以共享同一Handler(其无状态性,保证单一实例可以在多线程下共享),
所以Handler可以由用户自己创建并传给引擎。
0 请登录后投票
   发表时间:2007-07-02  
呵呵,我只是针对你第二个回复中提出的契约性的矛盾提出一种新的思路而已。我理解你说的问题,一个是指令的逻辑,一个是runtime数据。引擎保存的是Handle实例,handle就代表逻辑,无状态,template代表runtime数据,所以你引擎保存的不是指令的实例。

另外,再请教javatar兄一个问题,你引擎在执行一个指令时,比如out,是一次性把整个template的所有out指令都执行完,还是按照template编写的指令顺序一个个执行?如果是后者,好像从接口没看出来,指令怎么识别自己的template的位置?
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics