对于软件开发,有两种思维模式:
- 命令式
- 说明式
命令式的思维模式是这样:按照每个步骤描述要做的工作和任务,先做什么,后做什么,顺序是不能颠倒的;
说明式的思维模式是,描述一个物体或者概念,它有什么特性,和其他物体有什么关系。
对于编程语言,实际上是命令式与说明式思维混合在一起的。
比如,我们熟悉的C语言,其中,说明式的语句包括:
- 函数定义与声明
- 变量(全局、局部、静态)的定义与说明
- struct定义
- enum定义
- typedef定义
- 分支语句(if-else, switch),循环语句,都属于说明性的部分
- 等
而表达式及其组成的语句,则是命令式的,它们描述了一个任务按照什么样的顺序逐步执行;
C++和java语言中class的定义,则是非常强大的说明性语句。
但是,这些语言中说明的成分还是太少太弱了,而且有一个非常显著的特点:你不能扩展自己的说明性语法。
为什么我这么看中说明性语法呢?
我们考虑一个应用程序,比如一个计算器(带有GUI界面那种),这个程序中大部分内容是可以说明的:
- GUI界面描述
- 事件处理方式
只有加减乘除计算部分,需要命令式描述方法,计算出用户希望的结果。然后将数据送到窗口的控件上进行显示。
显然,窗口及控件的创建、将按钮的click事件关联到一个回调上这些功能,如果用xml一类的描述性语言编写,更加简单和清晰。
一个软件中,总是存在相当部分的对象和数据,是基本不变化的,而另外一些数据,则是要求被处理的。 比如,一个字处理软件,它的GUI,菜单,事件等,在软件启动后,基本不会发生变化;而用户键入的文字,则是经常性的变化。
也就是说,软件由两部分组成,一部分是程序的框架,是不变化或者仅有少数变化的;另外一部分则是程序的处理数据,有程序的输入、输出和中间数据组成。
打个比方来说哦,程序的框架就是生产线,被处理的数据就是生产线上的产品。
不过,在我们开发软件时,这两者经常被混合讨论的,这使得软件开发的难度高而效率低。
如果程序的框架,能够被简单的描述出来,而不需要开发者花费大量的力气去定义和创建,那么,程序的开发难度将大幅降低,而效率的大幅提高。
这是我这么看中说明性语言的原因。
关注软件运行时状态
软件在运行时,有一系列各种各样的对象组成,对象直接相互引用和关联。当软件处于空闲状态时,这些对象什么也不做;当软件在处理数据时,数据在不同的对象之间被分割、转换、传递、复制、组合,最终形成需要的数据,并以某种方式输出出来。
为了构建这样一个运行时系统,软件开发者(对于OO开发者)需要在合适的时间,创建合适的对象,并和其他对象关联起来。 这里面,“
合适”与“
其他对象” 是两个概念,是非常模糊,有时甚至是不可到达的。
正是由于创建合适的对象如此的困难,以至于设计模式中专门列出一个“创建模式”,来描述创建合适对象要使用的各种方法。合适的时间通常和效率、性能相关,解决起来并不是很复杂。
"其他对象“这个范围是非常难以界定的,对于不同的要求,一个对象需要知道不同种类的对象。为此,我们发明接口技术。 接口技术对这个问题很重要,但是,接口有时候也是相互依赖的,而且,接口的修改,对于它的实现类和使用它的类来说,无疑是灾难。
而对于被处理的数据,情况则更加复杂。被处理的数据,可能存在一个复杂的逻辑关系,需要开发者为其单独建模,这样,被处理数据的模型有可能比被处理数据更加复杂;另外,被处理数据,实际上涉及与磁盘、网络等介质交换,这就涉及数据的解析与保存。
因此,从这个角度来说,如果我们能够描述出软件运行时的数据构成,然后让编译器自动构建出这种运行时内存的话,开发的效率和稳定性将极大的提高。
数据网络
通用的数据结构,主要由线性表、树、图以及字典表(struct和class可以看作经过优化的字典,他们都是通过名称访问的)。对象间的相互引用关系,建立了一个及其复杂的网络。 这个复杂网络中的数据,它们的值是相互映射,并保持同步的。
除了一些非常简单的应用外,大多数有实际价值的应用软件,都会建立这样一个数据网络。 对于一些较为复杂的应用软件,被处理的数据会形成临时性的数据网络。
无论是那种网络,由于引入了OO编程思想,开发者极力对外隐藏不必要的数据,因此,以OO编程思想指导的软件,其数据网络由若干小网络连接而成,每个小网络可以看作一个小层次。
GUI的数据网络
很多GUI都可以通过xml语言来描述。从这方面来说,GUI界面是非常典型的树结构。
但是GUI应用程序却远没有这么简单。
单个GUI窗口的数据网络
单个GUI窗口是一个标准的树结构。但是,GUI窗口需要和用户交互,因此,它有很多隐藏的网络。
以窗口/控件ID或者名称为key的字典网络
GUI系统为了让其他模块能够访问窗口上的控件,会提供ID或者name。这些 ID或者name就会形成一个字典数据结构,提供外部访问使用。例如,在HTML的DOM模型中,可以通过getElementById,取得标签对象。
输入和输出数据网络
一个Login窗口,必然要提供username和password两个数据到外部,让其他模块处理。它有可能需要其他模块提供已经保存过的username和password数据来进行预填。
对于窗口系统中的绝大多数对话框来说,输入和/或者输出数据是不可避免的,当然也有例外,如About对话框,完全不需要额外的输入和输出。
有的时候,数据可能来自多个方面。
如一个文件选择对话框,它要求输入的数据包括:标题、文件扩展名过滤表等;而输出则包括选择的文件名。除此之外,它从系统的磁盘上读取目录和文件信息,显示给用户。
输入和输出网络和GUI的树型网络有密切关系:
输入的数据,将映射到GUI树中,某个控件的属性;而输出的网络,则是从GUI树中采集到对应的数据。
这涉及到数据网络直接的相互映射问题。
GUI树的节点之间还存在额外的网络链接:
例如,一个播放器控制面板中,就存在密切的联系:
- 开始按钮按下后,停止按钮变为可用,反之亦然;
- 在播放进度上选择不同进度,播放的实际就会变化;
- 等等。
在通常的编程中,我们通常会通过一些变量,如播放状态变量、当前进度变量等,保存现有状态,然后在按钮或者是进度条的事件中,直接改变相关这些变量的值的同时,改变相关控件的属性。
如果仔细观察,在这些公认的设计思路中,存在一个隐含的数据网络,它包含状态变量、当前进度变量,而且,播放、停止按钮的enable属性,是和状态变量对应的;播放进度条的位置属性和显示播放时间的标签的文本属性,是和当前进度变量对应的。
从上面的总结来看,无论代码多么复杂,它总是由若干存在映射关系的对象网络组成的。这些网络,主体上都是树形网络(或者可以看作xml的DOM结构)。
xml的DOM结构是混合树与字典(节点的属性表)的结构。
因此,我们将窗口的逻辑关系,抽象为若干DOM树的映射关系。
DOM树之间的映射研究
对于xml,可以通过xpath进行节点的选择。 我们可以利用xpath来对DOM树进行节点间的映射。
一对一的映射关系
DOM树A的节点和DOM树B的节点进行一对一的映射。
例如,播放器控制面板的例子。
控制器面板UI的xml示意(简化版本)
<window>
<button name="play"/>
<button name="stop"/>
<progress name="progress"/>
<label name="current_time"/>
</window>
对应的状态控制的xml表述是
<controller>
<variable name="play_state" type="enum">
<option value="playing"/>
<option value="stoped"/>
</variable>
<variable name="curtime" type="time"/>
<variable name="progress" type="percent"/>
</controller>
play_state将绑定到play/stop button上;curtime将绑定到current_time label上,progress将绑定到progress控件上。
描述语法,可以
binding player_controller(window : DOM, ctl: DOM) { window://button[@name='play']/@enable = expr(ctl://variable[@name='play_state'] != playing); window://button[@name='stop']/@enable = expr(ctl://variable[@name='play_state'] != stoped); window://progress/@postion == ctl://progress; window://current_time/@text = ctl://curtime; }
这是利用类似C++代码表示的伪码。其中 "="表示将右边的值绑定到左边; "=="表示左右两边互相绑定。与C++语言中的赋值和比较是不一样的含义。
这是一种外部映射机制。它的特点是,不需要接触两个DOM的内部构成,就可以改变其内容。还有一种较为直观的方法,即利用xlst,将controller的内容嵌入到window的DOM中"{" "}" 分别表示的是绑定机制<window > <import-dom name="ctl"/> <button name="play" enable="{expr(ctl://variable[@name='play_state'] != playing}"/> <button name="stop" enable="{expr(ctl://variable[@name='play_state'] != stoped)}"/> <progress name="progress" postion="{ctl://progress}"/> <label name="current_time" text="{ctl://curtime}"/> </window>
列表数据的映射
通常会有很多列表数据,需要填充。比如,下面是一个书店描述的xml
<bookstore>
<book category="COOKING">
<title lang="en">Everyday Italian</title>
<author>Giada De Laurentiis</author>
<year>2005</year>
<price>30.00</price>
</book>
<book category="CHILDREN">
<title lang="en">Harry Potter</title>
<author>J K. Rowling</author>
<year>2005</year>
<price>29.99</price>
</book>
<book category="WEB">
<title lang="en">XQuery Kick Start</title>
<author>James McGovern</author>
<author>Per Bothner</author>
<author>Kurt Cagle</author>
<author>James Linn</author>
<author>Vaidyanathan Nagarajan</author>
<year>2003</year>
<price>49.99</price>
</book>
<book category="WEB">
<title lang="en">Learning XML</title>
<author>Erik T. Ray</author>
<year>2003</year>
<price>39.95</price>
</book>
</bookstore>
将该数据映射到一个listview控件中。listview由4列组成,首先,我们使用xslt的映射方式
<listview>
<import-dom name="book"/>
<foreach select="book://book"/>
<row>
<column>
<image src="{getimage('book:@category}"/>
</column>
<column>
<label text="{'book:title'}"/>
</column>
<column>
<label text="{connect(book:author)"}/>
</column>
<column>
<label text="{book:year}"/>
</column>
<column>
<label text="{book:price}"/>
</column>
</row>
</foreach>
</listview>
还有另外一种方式,再不需要改变listview的定义内容的情况下,通过外部改变其结构
bind listview(row_templ : DOMTemplate, book:DOM)
{
foreach(book://book, {
DOM row = form_template(row_templ);
row:colum[0]/image/@src = getimage(book:@category);
row:column[1]/label/@text = book:title;
row:column[2]/label/@text = connect(book:author);
row:column[3]/label/@text = book:year;
row:column[4]/label/@text = book:price;
listview.append(row);
})
}
推荐使用这种不直接生成的方法,它的耦合性更小。
映射时的数据转换
在映射时,可以进行数据转换。如上例中,book的category可以映射为对应的图片文件。author部分,可以将多个节点的值链接起来,组成一个数值。
对于映射而来的数据,是无法进行双向转换的。除非定义双向映射函数。
C++对象的DOM化
上面的一切基础,都是将数据想象为一个DOM为基础的。但是,C++对象和DOM之间的区别还是非常大的。
为了解决这个问题,我们创建一个C++类的meta对象,这个meta对象,将数据描述为一个DOM结构。
比如,对于一个Display类,进行描述
class Display
{
public:
int width;
int height;
int depth;
};
描述它的DOM化结构
Node display_node(
"display" //node name,
attribute("width", OFFSET(Display, width), INT, 800,range(1,2000)),
attribute("height", OFFSET(Display,height), INT, 600,range(1,2000)),
attribute("depth", OFFSET(Display,depth), INT, 16, emu(8,16,24,32)))
);
Node类描述一个Node节点,Node节点包括属性和可能存在的子节点。
- "display" 是节点的名称
- attribute会创建属性:
- "width"是属性名称
- OFFSET(Display,width) 是一个宏,它创建一个属性访问的对象,在这里,我们通过width成员相对与Display的偏移来访问
- INT 表示属性的基本类型位integer
- 800 表示默认值
- range(1,2000) 是对width值的一个约束,也就是说,该属性取值必须在1到2000之间
这个DOM的结构,可以通过xpath来访问。
DOM化的结构有很多有用的功能,如,完成代码的序列化。我们可以轻易的将Display对象序列化为xml对象。