千变万化的表达式
上回说到,去和组件的合作者以及组件的使用者讨论,还是从上一篇的最后的Xml开始:
还记得上一回说过,要完成这样一个修改pptx文本的几个要素吗?
- 在哪里修改(where)
- 修改成什么(what)
- 怎么修改(how to)
- 何时修改(when)
而上一次仅仅解答了where、what的问题,当然how依然不用关心,因为这回继续是讲xsd。
思考一下,然后来说说一些可以想到的改进:
- 关于what的问题,因为直接用value中的值,显然并不可行,不可能用为了改一个值就修改一次xml配置文件
- 关于when的问题,对于每一个modification可能有条件限制,在某些条件下才执行某些修改
关于第一个问题,显然,value这个节点的类型不能简单地用string,需要是一个可以代表变量的或者代表参数的类型。
那么就定义一个参数类型,并且修改value的类型:
再看看xml变成什么样子了:
似乎不怎么样啊,没什么大区别。
别急,替换的内容不一定就非要是变量,常量也是有可能的。(例如:同一个模板的同一个地方在不同的配置里面被替换成不同的常量)
那么好吧,现在再加一个常量类型:
问题来了,现在value可以是一个参数(Parameter)也可以是一个常量(Const),那么value的类型应该是什么哪?
还记得第一篇吗?如果出现多个可选择的类型,要是用xs:choice,要么就用继承,这里,似乎还是选择继承更好一些,那么就定义一个比常量和参数更基础的类型:
(是不是想起了编程语言中的表达式?)
然后把参数和常量也修改成从表达式继承,并且把value的类型修改成表达式:
现在再来看看xml:
现在,value就可以在参数和常量中随意选择,并且对将来新添加的其他表达式也可以兼容。
接下来,来看看when的问题。
任何modification都可能有某些条件才能执行的限制,所以,modificaion应该包含一个when节,但是如果考虑更多的情况,例如给某个slide加条件,或者给整个配置加条件,那么when节就可以出现在configuation或slide或modification或者其它将来可能出现的节下面,因此,再次借助抽象类的力量,定义一个Conditionable的抽象类:
写到这里,立即发现when的类型是boolean显然是不正确的,如果是boolean的话,那显然在配置xml写好之后,就立即可以确定这些被when修饰的节点到底需不需要执行了,这显然违反了我们的初衷。
这里,我们需要为这个xml配置做出一个重要的选择:是想把这么xml的类型系统(这里可以把它当成某一门语言看待)做成一个强类型的还是弱类型的。
强类型的好处,显而易见,禁止错误类型的参数继续运算,可以说,之前的xsd就是这种强类型的体现。
那么弱类型哪?弱类型语言可以隐式的转换类型,也就是说,一个字符串可以被当成布尔值,也可以被当成数字,遇到无法转换的情况,在运行时报错。
说了这一段废话,再来看xml,when后边的类型必须是个boolean,但是它又需要在运行时计算,也就是需要上面的Expression类型,但是Expression类型并不保证其结果是boolean。
这里强类型的做法是再定义一个BooleanExpression,但是这样就做得太复杂了,使用者可能无法接受。
如果用弱类型,Expression类型并不保证其结果是boolean,但是因为是弱类型的,所以,无论Expression返回了什么类型,都尝试把它转换成boolean,如果无法转换,就报错。
这样,使用者就不需要关心烦人的类型问题了:
另外,增加了minOccurs=“0”,这个是表达了when节可以不出现的意思,也就所在的节必须执行。
有时候,条件可能比较复杂,不能用简单的一个boolean表示,这是,我们就需要新添加几个boolean运算的表达式,来执行And、Or、Not:
写到这里,是不是突然发现Expression和弱类型结合在一起,真是强大到让人不可思议?当有任何需求时,似乎都可以通过继承Expression的方式来扩展。
(强大的Expression类型就暂时说到这里,后面继续会提到它)
回过头来看Conditionable类型,目前还没有任何类型继承自它,接着,修改一下Modification,Slide已经Configuation类型:
现在,再看一下xml:
例子中的条件就是相当于:(p1||p2)&&(!p3),是不是够复杂了?
现在,思考另一个问题,如果某些应用需要其他表达式做更加复杂的问题,怎么办哪?方案有下面几个:
- 新写一些表达式,继承自Expression类
- 写一个通用的表达式,用于回调.net中符合特定接口
关于第一个,我想已经不用多说了,前面的And,Or和Not已经充分显示了这种做法的强大。但是,这个方法的缺点也显而易见,And不会真正的做And操作,这个需要后面的执行引擎去支持,也就是说,新加一个表达式不但需要修改Xsd,也需要修改执行引擎部分的代码,导致执行引擎的稳定性下降。因此,对于那些不常用的表达式而言,这样做显然是得不偿失的。
第二个方案其实和.net的PInvoke相似,只不过这里是Xml配置PInvoke到.net代码上而已。
这里,给这个特殊的"PInvoke"取名叫CliFunction,至于如何PInvoke到.net的代码上,将在执行引擎部分详细介绍。这里,就仅仅看一下Xsd以及Xml中的形式:
是不是出奇的简单?不过要注意里面的有个属性function的类型——QName,这里有一个很重要的约定:
- QName的Namespace部分代表这个特定的CliFunction所在的Assembly
- QName的Name部分代表特定的CliFunction的方法名
所以,xml的形式如下:
例子中调用了"cli:StringConcat"方法,其中cli被定义成"cli-assembly::PptxExtensions",也就是说,这里让执行引擎去查找一个叫PptxExtensions的assembly,使用cli-assembly的协议(虽然目前只支持.net的assembly,不过为了以后扩展,还是加上协议比较好),并且去查找里面一个叫StringConcat的方法,然后去调用这个方法,把p1,p2,p3作为参数传递进去。
而且,因为每一个param节的类型都是Expression类,所以在一个CliFunction的参数上调用另一个CliFunction也是合法的:
现在是不是感觉这个xml已经无所不能了?但是别忘了一点,调用CliFunction的时候,所有的参数都将会被预先求解出来,也就是说,它的行为和c#中的:
StringConcat(Trim(p1), Trim(p2))
的行为一致,也就是这样的顺序运行:
- 从参数中取出p1
- 执行Trim方法(Trim(p1))
- 从参数中取出p2
- 执行Trim方法(Trim(p2))
- 在把前面两次调用的结果传给StringConcat,执行方法
看起来很正常是吧,但是如果是这样的语句:
if (p1 == null)
return “”;
else
return Trim(p1);
这时候,需要额外定义一个IsNull的表达式:
xml可能就会这样写:
这里调用了一个IIF的CliFunction,来完成一个if分支,但是有没有发现一个问题:
如果p1的值为null,那么IsNull返回true,理论上只需要求解第二个表达式const的"",而不需要求解Trim(p1)。而实际上,在调用IIF时,3个参数都被求解出来了,看起来似乎没什么问题,但是,这一个运行顺序的改变,在某些时候可能是致命的。如果Trim方法在输入值为null时,会抛出异常,那么这里的IIF就会因为异常而被终止执行,这显然是不愿意看到的结果。
所以,为了能够获得一个有延迟求解效果的if语句,必须在xsd中定义一个if表达式:
这当然还是需要执行引擎延迟对待If表达式中的trueValue和falseValue里面的表达式,仅仅当condition为true时,才求解trueValue,反之为false时,才求解falseValue。
写到这里,表达式已经已千变万化的形态展现给我们了,什么是表达式,一块可以有输入,但必须有输出的代码段就是表达式,当然看到现在,似乎还没看到代码段,仅仅看到了单节的代码(不计算嵌套),那么是否可以有多行的表达式哪?
可以,但是这里我更想称它为代码块(Block),但是一旦出现代码块,就必须引入一些新的概念,使其变得比较复杂,将在下一篇中继续。