|
浏览器前端编程的面貌自2005年以来已经发生了深刻的变化,这并不简单的意味着出现了大量功能丰富的基础库,使得我们可以更加方便的编写业务代码,更重要的是我们看待前端技术的观念发生了重大转变,明确意识到了如何以前端特有的方式释放程序员的生产力。本文将结合jQuery源码的实现原理,对javascript中涌现出的编程范式和常用技巧作一简单介绍。 1. AJAX: 状态驻留,异步更新 首先来看一点历史。 A. 1995年Netscape公司的Brendan Eich开发了javacript语言,这是一种动态(dynamic)、弱类型(weakly typed)、基于原型(prototype-based)的脚本语言。 B. 1999年微软IE5发布,其中包含了XMLHTTP ActiveX控件。 C. 2001年微软IE6发布,部分支持DOM level 1和CSS 2标准。 D. 2002年Douglas Crockford发明JSON格式。 至此,可以说Web2.0所依赖的技术元素已经基本成形,但是并没有立刻在整个业界产生重大的影响。尽管一些“页面异步局部刷新”的技巧在程序员中间秘密的流传,甚至催生了bindows这样庞大臃肿的类库,但总的来说,前端被看作是贫瘠而又肮脏的沼泽地,只有后台技术才是王道。到底还缺少些什么呢? 当我们站在今天的角度去回顾2005年之前的js代码,包括那些当时的牛人所写的代码,可以明显的感受到它们在程序控制力上的孱弱。并不是说2005年之前的js技术本身存在问题,只是它们在概念层面上是一盘散沙,缺乏统一的观念,或者说缺少自己独特的风格, 自己的灵魂。当时大多数的人,大多数的技术都试图在模拟传统的面向对象语言,利用传统的面向对象技术,去实现传统的GUI模型的仿制品。 2005年是变革的一年,也是创造概念的一年。伴随着Google一系列让人耳目一新的交互式应用的发布,Jesse James Garrett的一篇文章《Ajax: A New Approach to Web Applications》被广为传播。Ajax这一前端特有的概念迅速将众多分散的实践统一在同一口号之下,引发了Web编程范式的转换。所谓名不正则言不顺,这下无名群众可找到组织了。在未有Ajax之前,人们早已认识到了B/S架构的本质特征在于浏览器和服务器的状态空间是分离的,但是一般的解决方案都是隐藏这一区分,将前台状态同步到后台,由后台统一进行逻辑处理,例如ASP.NET。因为缺乏成熟的设计模式支持前台状态驻留,在换页的时候,已经装载的js对象将被迫被丢弃,这样谁还能指望它去完成什么复杂的工作吗? Ajax明确提出界面是局部刷新的,前台驻留了状态,这就促成了一种需要:需要js对象在前台存在更长的时间。这也就意味着需要将这些对象和功能有效的管理起来,意味着更复杂的代码组织技术,意味着对模块化,对公共代码基的渴求。 jQuery现有的代码中真正与Ajax相关(使用XMLHTTP控件异步访问后台返回数据)的部分其实很少,但是如果没有Ajax, jQuery作为公共代码基也就缺乏存在的理由。
2. 模块化:管理名字空间 当大量的代码产生出来以后,我们所需要的最基础的概念就是模块化,也就是对工作进行分解和复用。工作得以分解的关键在于各人独立工作的成果可以集成在一起。这意味着各个模块必须基于一致的底层概念,可以实现交互,也就是说应该基于一套公共代码基,屏蔽底层浏览器的不一致性,并实现统一的抽象层,例如统一的事件管理机制等。比统一代码基更重要的是,各个模块之间必须没有名字冲突。否则,即使两个模块之间没有任何交互,也无法共同工作。 jQuery目前鼓吹的主要卖点之一就是对名字空间的良好控制。这甚至比提供更多更完善的功能点都重要的多。良好的模块化允许我们复用任何来源的代码,所有人的工作得以积累叠加。而功能实现仅仅是一时的工作量的问题。jQuery使用module pattern的一个变种来减少对全局名字空间的影响,仅仅在window对象上增加了一个jQuery对象(也就是$函数)。 所谓的module pattern代码如下,它的关键是利用匿名函数限制临时变量的作用域。 var feature =(function() {
// 私有变量和函数 var privateThing = 'secret', publicThing = 'not secret',
changePrivateThing = function() { privateThing = 'super secret'; },
sayPrivateThing = function() { console.log(privateThing); changePrivateThing(); };
// 返回对外公开的API return { publicThing : publicThing, sayPrivateThing : sayPrivateThing } })(); js本身缺乏包结构,不过经过多年的尝试之后业内已经逐渐统一了对包加载的认识,形成了RequireJs库这样得到一定共识的解决方案。jQuery可以与RequireJS库良好的集成在一起, 实现更完善的模块依赖管理。http://requirejs.org/docs/jquery.html require(["jquery", "jquery.my"], function() { //当jquery.js和jquery.my.js都成功装载之后执行 $(function(){ $('#my').myFunc(); }); }); 通过以下函数调用来定义模块my/shirt, 它依赖于my/cart和my/inventory模块, require.def("my/shirt", ["my/cart", "my/inventory"], function(cart, inventory) { // 这里使用module pattern来返回my/shirt模块对外暴露的API return { color: "blue", size: "large" addToCart: function() { // decrement是my/inventory对外暴露的API inventory.decrement(this); cart.add(this); } } } );
3. 神奇的$:对象提升 当你第一眼看到$函数的时候,你想到了什么?传统的编程理论总是告诉我们函数命名应该准确,应该清晰无误的表达作者的意图,甚至声称长名字要优于短名字,因为减少了出现歧义的可能性。但是,$是什么?乱码?它所传递的信息实在是太隐晦,太暧昧了。$是由prototype.js库发明的,它真的是一个神奇的函数,因为它可以将一个原始的DOM节点提升(enhance)为一个具有复杂行为的对象。在prototype.js最初的实现中,$函数的定义为 var $ = function (id) { return "string" == typeof id ? document.getElementById(id) : id; }; 这基本对应于如下公式 e = $(id) 这绝不仅仅是提供了一个聪明的函数名称缩写,更重要的是在概念层面上建立了文本id与DOM element之间的一一对应。在未有$之前,id与对应的element之间的距离十分遥远,一般要将element缓存到变量中,例如 var ea = docuement.getElementById('a'); var eb = docuement.getElementById('b'); ea.style.... 但是使用$之后,却随处可见如下的写法 $('header_'+id).style... $('body_'+id).... id与element之间的距离似乎被消除了,可以非常紧密的交织在一起。 prototype.js后来扩展了$的含义, function $() { var elements = new Array(); for (var i = 0; i < arguments.length; i++) { var element = arguments[i]; if (typeof element == 'string') element = document.getElementById(element); if (arguments.length == 1) return element; elements.push(element); } return elements; } 这对应于公式 [e,e] = $(id,id) 很遗憾,这一步prototype.js走偏了,这一做法很少有实用的价值。 真正将$发扬光大的是jQuery, 它的$对应于公式 [o] = $(selector) 这里有三个增强 A. selector不再是单一的节点定位符,而是复杂的集合选择符 B. 返回的元素不是原始的DOM节点,而是经过jQuery进一步增强的具有丰富行为的对象,可以启动复杂的函数调用链。 C. $返回的包装对象被造型为数组形式,将集合操作自然的整合到调用链中。
当然,以上仅仅是对神奇的$的一个过分简化的描述,它的实际功能要复杂得多. 特别是有一个非常常用的直接构造功能. $("<table><tbody><tr><td>...</td></tr></tbody></table>").... jQuery将根据传入的html文本直接构造出一系列的DOM节点,并将其包装为jQuery对象. 这在某种程度上可以看作是对selector的扩展: html内容描述本身就是一种唯一指定. $(function{})这一功能就实在是让人有些无语了, 它表示当document.ready的时候调用此回调函数。真的,$是一个神奇的函数, 有任何问题,请$一下。 总结起来, $是从普通的DOM和文本描述世界到具有丰富对象行为的jQuery世界的跃迁通道。跨过了这道门,就来到了理想国。 4. 无定形的参数:专注表达而不是约束 弱类型语言既然头上顶着个"弱"字, 总难免让人有些先天不足的感觉. 在程序中缺乏类型约束, 是否真的是一种重大的缺憾? 在传统的强类型语言中, 函数参数的类型,个数等都是由编译器负责检查的约束条件, 但这些约束仍然是远远不够的. 一般应用程序中为了加强约束, 总会增加大量防御性代码, 例如在C++中我们常用ASSERT, 而在java中也经常需要判断参数值的范围 if (index < 0 || index >= size) throw new IndexOutOfBoundsException( "Index: "+index+", Size: "+size); 很显然, 这些代码将导致程序中存在大量无功能的执行路径, 即我们做了大量判断, 代码执行到某个点, 系统抛出异常, 大喊此路不通. 如果我们换一个思路, 既然已经做了某种判断,能否利用这些判断的结果来做些什么呢? javascript是一种弱类型的语言,它是无法自动约束参数类型的, 那如果顺势而行,进一步弱化参数的形态, 将"弱"推进到一种极致, 在弱无可弱的时候, weak会不会成为标志性的特点? 看一下jQuery中的事件绑定函数bind, A. 一次绑定一个事件 $("#my").bind("mouseover", function(){}); B. 一次绑定多个事件 $("#my").bind("mouseover mouseout",function(){}) C. 换一个形式, 同样绑定多个事件 $("#my").bind({mouseover:function(){}, mouseout:function(){}); D. 想给事件监听器传点参数 $('#my').bind('click', {foo: "xxxx"}, function(event) { event.data.foo..}) E. 想给事件监听器分个组 $("#my").bind("click.myGroup″, function(){}); F. 这个函数为什么还没有疯掉??? 就算是类型不确定, 在固定位置上的参数的意义总要是确定的吧? 退一万步来说, 就算是参数位置不重要了,函数本身的意义应该是确定的吧? 但这是什么? 取值 value = o.val(), 设置值 o.val(3) 一个函数怎么可以这样过分, 怎么能根据传入参数的类型和个数不同而行为不同呢? 看不顺眼是不是? 可这就是俺们的价值观. 既然不能防止, 那就故意允许. 虽然形式多变, 却无一句废话. 缺少约束, 不妨碍表达(我不是出来吓人的). 5. 链式操作: 线性化的逐步细化 jQuery早期最主要的卖点就是所谓的链式操作(chain). $('#content') // 找到content元素 .find('h3') // 选择所有后代h3节点 .eq(2) // 过滤集合, 保留第三个元素 .html('改变第三个h3的文本') .end() // 返回上一级的h3集合 .eq(0) .html('改变第一个h3的文本');
在一般的命令式语言中, 我们总需要在重重嵌套循环中过滤数据, 实际操作数据的代码与定位数据的代码纠缠在一起. 而jQuery采用先构造集合然后再应用函数于集合的方式实现两种逻辑的解耦, 实现嵌套结构的线性化. 实际上, 我们并不需要借助过程化的思想就可以很直观的理解一个集合, 例如 $('div.my input:checked')可以看作是一种直接的描述,而不是对过程行为的跟踪. 循环意味着我们的思维处于一种反复回绕的状态, 而线性化之后则沿着一个方向直线前进, 极大减轻了思维负担, 提高了代码的可组合性. 为了减少调用链的中断, jQuery发明了一个绝妙的主意: jQuery包装对象本身类似数组(集合). 集合可以映射到新的集合, 集合可以限制到自己的子集合,调用的发起者是集合,返回结果也是集合,集合可以发生结构上的某种变化但它还是集合, 集合是某种概念上的不动点,这是从函数式语言中吸取的设计思想。集合操作是太常见的操作, 在java中我们很容易发现大量所谓的封装函数其实就是在封装一些集合遍历操作, 而在jQuery中集合操作因为太直白而不需要封装. 链式调用意味着我们始终拥有一个“当前”对象,所有的操作都是针对这一当前对象进行。这对应于如下公式 x += dx 调用链的每一步都是对当前对象的增量描述,是针对最终目标的逐步细化过程。Witrix平台中对这一思想也有着广泛的应用。特别是为了实现平台机制与业务代码的融合,平台会提供对象(容器)的缺省内容,而业务代码可以在此基础上进行逐步细化的修正,包括取消缺省的设置等。 话说回来, 虽然表面上jQuery的链式调用很简单, 内部实现的时候却必须自己多写一层循环, 因为编译器并不知道"自动应用于集合中每个元素"这回事. $.fn['someFunc'] = function(){ return this.each(function(){ jQuery.someFunc(this,...); } } 6. data: 统一数据管理 作为一个js库,它必须解决的一个大问题就是js对象与DOM节点之间的状态关联与协同管理问题。有些js库选择以js对象为主,在js对象的成员变量中保存DOM节点指针,访问时总是以js对象为入口点,通过js函数间接操作DOM对象。在这种封装下,DOM节点其实只是作为界面展现的一种底层“汇编”而已。jQuery的选择与Witrix平台类似,都是以HTML自身结构为基础,通过js增强(enhance)DOM节点的功能,将它提升为一个具有复杂行为的扩展对象。这里的思想是非侵入式设计(non-intrusive)和优雅退化机制(graceful degradation)。语义结构在基础的HTML层面是完整的,js的作用是增强了交互行为,控制了展现形式。 如果每次我们都通过$('#my')的方式来访问相应的包装对象,那么一些需要长期保持的状态变量保存在什么地方呢?jQuery提供了一个统一的全局数据管理机制。 获取数据 $('#my').data('myAttr') 设置数据 $('#my').data('myAttr',3); 这一机制自然融合了对HTML5的data属性的处理 <input id="my" data-my-attr="4" ... /> 通过 $('#my').data('myAttr')将可以读取到HTML中设置的数据。
第一次访问data时,jQuery将为DOM节点分配一个唯一的uuid, 然后设置在DOM节点的一个特定的expando属性上, jQuery保证这个uuid在本页面中不重复。 elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; 以上代码可以同时处理DOM节点和纯js对象的情况。如果是js对象,则data直接放置在js对象自身中,而如果是DOM节点,则通过cache统一管理。 因为所有的数据都是通过data机制统一管理的,特别是包括所有事件监听函数(data.events),因此jQuery可以安全的实现资源管理。在clone节点的时候,可以自动clone其相关的事件监听函数。而当DOM节点的内容被替换或者DOM节点被销毁的时候,jQuery也可以自动解除事件监听函数, 并安全的释放相关的js数据。
7. event:统一事件模型 "事件沿着对象树传播"这一图景是面向对象界面编程模型的精髓所在。对象的复合构成对界面结构的一个稳定的描述,事件不断在对象树的某个节点发生,并通过冒泡机制向上传播。对象树很自然的成为一个控制结构,我们可以在父节点上监听所有子节点上的事件,而不用明确与每一个子节点建立关联。 jQuery除了为不同浏览器的事件模型建立了统一抽象之外,主要做了如下增强: A. 增加了自定制事件(custom)机制. 事件的传播机制与事件内容本身原则上是无关的, 因此自定制事件完全可以和浏览器内置事件通过同一条处理路径, 采用同样的监听方式. 使用自定制事件可以增强代码的内聚性, 减少代码耦合. 例如如果没有自定制事件, 关联代码往往需要直接操作相关的对象 $('.switch, .clapper').click(function() { var $light = $(this).parent().find('.lightbulb'); if ($light.hasClass('on')) { $light.removeClass('on').addClass('off'); } else { $light.removeClass('off').addClass('on'); } }); 而如果使用自定制事件,则表达的语义更加内敛明确, $('.switch, .clapper').click(function() { $(this).parent().find('.lightbulb').trigger('changeState'); }); B. 增加了对动态创建节点的事件监听. bind函数只能将监听函数注册到已经存在的DOM节点上. 例如 $('li.trigger').bind('click',function(){}} 如果调用bind之后,新建了另一个li节点,则该节点的click事件不会被监听. jQuery的delegate机制可以将监听函数注册到父节点上, 子节点上触发的事件会根据selector被自动派发到相应的handlerFn上. 这样一来现在注册就可以监听未来创建的节点. $('#myList').delegate('li.trigger', 'click', handlerFn); 最近jQuery1.7中统一了bind, live和delegate机制, 天下一统, 只有on/off. $('li.trigger’).on('click', handlerFn); // 相当于bind $('#myList’).on('click', 'li.trigger', handlerFn); // 相当于delegate 8. 动画队列:全局时钟协调 抛开jQuery的实现不谈, 先考虑一下如果我们要实现界面上的动画效果, 到底需要做些什么? 比如我们希望将一个div的宽度在1秒钟之内从100px增加到200px. 很容易想见, 在一段时间内我们需要不时的去调整一下div的宽度, [同时]我们还需要执行其他代码. 与一般的函数调用不同的是, 发出动画指令之后, 我们不能期待立刻得到想要的结果, 而且我们不能原地等待结果的到来. 动画的复杂性就在于:一次性表达之后要在一段时间内执行,而且有多条逻辑上的执行路径要同时展开, 如何协调? 伟大的艾萨克.牛顿爵士在《自然哲学的数学原理》中写道:"绝对的、真正的和数学的时间自身在流逝着". 所有的事件可以在时间轴上对齐, 这就是它们内在的协调性. 因此为了从步骤A1执行到A5, 同时将步骤B1执行到B5, 我们只需要在t1时刻执行[A1, B1], 在t2时刻执行[A2,B2], 依此类推. t1 | t2 | t3 | t4 | t5 ... A1 | A2 | A3 | A4 | A5 ... B1 | B2 | B3 | B4 | B5 ... 具体的一种实现形式可以是 A. 对每个动画, 将其分装为一个Animation对象, 内部分成多个步骤. animation = new Animation(div,"width",100,200,1000, 负责步骤切分的插值函数,动画执行完毕时的回调函数); B. 在全局管理器中注册动画对象 timerFuncs.add(animation); C. 在全局时钟的每一个触发时刻, 将每个注册的执行序列推进一步, 如果已经结束, 则从全局管理器中删除. for each animation in timerFuncs if(!animation.doOneStep()) timerFuncs.remove(animation)
解决了原理问题,再来看看表达问题, 怎样设计接口函数才能够以最紧凑形式表达我们的意图? 我们经常需要面临的实际问题: A. 有多个元素要执行类似的动画 B. 每个元素有多个属性要同时变化 C. 执行完一个动画之后开始另一个动画 jQuery对这些问题的解答可以说是榨尽了js语法表达力的最后一点剩余价值. $('input') .animate({left:'+=200px',top:'300'},2000) .animate({left:'-=200px',top:20},1000) .queue(function(){ // 这里dequeue将首先执行队列中的后一个函数,因此alert("y") $(this).dequeue(); alert('x'); }) .queue(function(){ alert("y"); // 如果不主动dequeue, 队列执行就中断了,不会自动继续下去. $(this).dequeue(); });
A. 利用jQuery内置的selector机制自然表达对一个集合的处理. B. 使用Map表达多个属性变化 C. 利用微格式表达领域特定的差量概念. '+=200px'表示在现有值的基础上增加200px D. 利用函数调用的顺序自动定义animation执行的顺序: 在后面追加到执行队列中的动画自然要等前面的动画完全执行完毕之后再启动. jQuery动画队列的实现细节大概如下所示, A. animate函数实际是调用queue(function(){执行结束时需要调用dequeue,否则不会驱动下一个方法}) queue函数执行时, 如果是fx队列, 并且当前没有正在运行动画(如果连续调用两次animate,第二次的执行函数将在队列中等待),则会自动触发dequeue操作, 驱动队列运行. 如果是fx队列, dequeue的时候会自动在队列顶端加入"inprogress"字符串,表示将要执行的是动画. B. 针对每一个属性,创建一个jQuery.fx对象。然后调用fx.custom函数(相当于start)来启动动画。 C. custom函数中将fx.step函数注册到全局的timerFuncs中,然后试图启动一个全局的timer. timerId = setInterval( fx.tick, fx.interval ); D. 静态的tick函数中将依次调用各个fx的step函数。step函数中通过easing计算属性的当前值,然后调用fx的update来更新属性。 E. fx的step函数中判断如果所有属性变化都已完成,则调用dequeue来驱动下一个方法。
很有意思的是, jQuery的实现代码中明显有很多是接力触发代码: 如果需要执行下一个动画就取出执行, 如果需要启动timer就启动timer等. 这是因为js程序是单线程的,真正的执行路径只有一条,为了保证执行线索不中断, 函数们不得不互相帮助一下. 可以想见, 如果程序内部具有多个执行引擎, 甚至无限多的执行引擎, 那么程序的面貌就会发生本质性的改变. 而在这种情形下, 递归相对于循环而言会成为更自然的描述. 9. promise模式:因果关系的识别 现实中,总有那么多时间线在独立的演化着, 人与物在时空中交错,却没有发生因果. 软件中, 函数们在源代码中排着队, 难免会产生一些疑问, 凭什么排在前面的要先执行? 难道没有它就没有我? 让全宇宙喊着1,2,3齐步前进, 从上帝的角度看,大概是管理难度过大了, 于是便有了相对论. 如果相互之间没有交换信息, 没有产生相互依赖, 那么在某个坐标系中顺序发生的事件, 在另外一个坐标系中看来, 就可能是颠倒顺序的. 程序员依葫芦画瓢, 便发明了promise模式. promise与future模式基本上是一回事,我们先来看一下java中熟悉的future模式. futureResult = doSomething(); ... realResult = futureResult.get(); 发出函数调用仅仅意味着一件事情发生过, 并不必然意味着调用者需要了解事情最终的结果. 函数立刻返回的只是一个将在未来兑现的承诺(Future类型), 实际上也就是某种句柄. 句柄被传来传去, 中间转手的代码对实际结果是什么,是否已经返回漠不关心. 直到一段代码需要依赖调用返回的结果, 因此它打开future, 查看了一下. 如果实际结果已经返回, 则future.get()立刻返回实际结果, 否则将会阻塞当前的执行路径, 直到结果返回为止. 此后再调用future.get()总是立刻返回, 因为因果关系已经被建立, [结果返回]这一事件必然在此之前发生, 不会再发生变化. future模式一般是外部对象主动查看future的返回值, 而promise模式则是由外部对象在promise上注册回调函数. function getData(){ return $.get('/foo/').done(function(){ console.log('Fires after the AJAX request succeeds'); }).fail(function(){ console.log('Fires after the AJAX request fails'); }); }
function showDiv(){ var dfd = $.Deferred(); $('#foo').fadeIn( 1000, dfd.resolve ); return dfd.promise(); }
$.when( getData(), showDiv() ) .then(function( ajaxResult, ignoreResultFromShowDiv ){ console.log('Fires after BOTH showDiv() AND the AJAX request succeed!'); // 'ajaxResult' is the server’s response }); jQuery引入Deferred结构, 根据promise模式对ajax, queue, document.ready等进行了重构, 统一了异步执行机制. then(onDone, onFail)将向promise中追加回调函数, 如果调用成功完成(resolve), 则回调函数onDone将被执行, 而如果调用失败(reject), 则onFail将被执行. when可以等待在多个promise对象上. promise巧妙的地方是异步执行已经开始之后甚至已经结束之后,仍然可以注册回调函数 someObj.done(callback).sendRequest() vs. someObj.sendRequest().done(callback) callback函数在发出异步调用之前注册或者在发出异步调用之后注册是完全等价的, 这揭示出程序表达永远不是完全精确的, 总存在着内在的变化维度. 如果能有效利用这一内在的可变性, 则可以极大提升并发程序的性能. promise模式的具体实现很简单. jQuery._Deferred定义了一个函数队列,它的作用有以下几点: A. 保存回调函数。 B. 在resolve或者reject的时刻把保存着的函数全部执行掉。 C. 已经执行之后, 再增加的函数会被立刻执行。 一些专门面向分布式计算或者并行计算的语言会在语言级别内置promise模式, 比如E语言. def carPromise := carMaker <- produce("Mercedes"); def temperaturePromise := carPromise <- getEngineTemperature() ... when (temperaturePromise) -> done(temperature) { println(`The temperature of the car engine is: $temperature`) } catch e { println(`Could not get engine temperature, error: $e`) } 在E语言中, <-是eventually运算符, 表示最终会执行, 但不一定是现在. 而普通的car.moveTo(2,3)表示立刻执行得到结果. 编译器负责识别所有的promise依赖, 并自动实现调度. 10. extend: 继承不是必须的 js是基于原型的语言, 并没有内置的继承机制, 这一直让很多深受传统面向对象教育的同学们耿耿于怀. 但继承一定是必须的吗? 它到底能够给我们带来什么? 最纯朴的回答是: 代码重用. 那么, 我们首先来分析一下继承作为代码重用手段的潜力. 曾经有个概念叫做"多重继承", 它是继承概念的超级赛亚人版, 很遗憾后来被诊断为存在着先天缺陷, 以致于出现了一种对于继承概念的解读: 继承就是"is a"关系, 一个派生对象"is a"很多基类, 必然会出现精神分裂, 所以多重继承是不好的. class A{ public: void f(){ f in A } } class B{ public: void f(){ f in B } } class D: public A, B{} 如果D类从A,B两个基类继承, 而A和B类中都实现了同一个函数f, 那么D类中的f到底是A中的f还是B中的f, 抑或是A中的f+B中的f呢? 这一困境的出现实际上源于D的基类A和B是并列关系, 它们满足交换律和结合律, 毕竟,在概念层面上我们可能难以认可两个任意概念之间会出现从属关系. 但如果我们放松一些概念层面的要求, 更多的从操作层面考虑一下代码重用问题, 可以简单的认为B在A的基础上进行操作, 那么就可以得到一个线性化的结果. 也就是说, 放弃A和B之间的交换律只保留结合律, extends A, B 与 extends B,A 会是两个不同的结果, 不再存在诠释上的二义性. scala语言中的所谓trait(特性)机制实际上采用的就是这一策略. 面向对象技术发明很久之后, 出现了所谓的面向方面编程(AOP), 它与OOP不同, 是代码结构空间中的定位与修改技术. AOP的眼中只有类与方法, 不知道什么叫做意义. AOP也提供了一种类似多重继承的代码重用手段, 那就是mixin. 对象被看作是可以被打开,然后任意修改的Map, 一组成员变量与方法就被直接注射到对象体内, 直接改变了它的行为. prototype.js库引入了extend函数, Object.extend = function(destination, source) { for (var property in source) { destination[property] = source[property]; } return destination; } 就是Map之间的一个覆盖运算, 但很管用, 在jQuery库中也得到了延用. 这个操作类似于mixin, 在jQuery中是代码重用的主要技术手段---没有继承也没什么大不了的.
11. 名称映射: 一切都是数据 代码好不好, 循环判断必须少. 循环和判断语句是程序的基本组成部分, 但是优良的代码库中却往往找不到它们的踪影, 因为这些语句的交织会模糊系统的逻辑主线, 使我们的思想迷失在疲于奔命的代码追踪中. jQuery本身通过each, extend等函数已经极大减少了对循环语句的需求, 对于判断语句, 则主要是通过映射表来处理. 例如, jQuery的val()函数需要针对不同标签进行不同的处理, 因此定义一个以tagName为key的函数映射表 valHooks: { option: {get:function(){}}} 这样在程序中就不需要到处写 if(elm.tagName == 'OPTION'){ return ...; }else if(elm.tagName == 'TEXTAREA'){ return ...; } 可以统一处理 (valHooks[elm.tagName.toLowerCase()] || defaultHandler).get(elm); 映射表将函数作为普通数据来管理, 在动态语言中有着广泛的应用. 特别是, 对象本身就是函数和变量的容器, 可以被看作是映射表. jQuery中大量使用的一个技巧就是利用名称映射来动态生成代码, 形成一种类似模板的机制. 例如为了实现myWidth和myHeight两个非常类似的函数, 我们不需要 jQuery.fn.myWidth = function(){ return parseInt(this.style.width,10) + 10; } jQuery.fn.myHeight = function(){ return parseInt(this.style.height,10) + 10; } 而可以选择动态生成 jQuery.each(['Width','Height'],function(name){ jQuery.fn['my'+name] = function(){ return parseInt(this.style[name.toLowerCase()],10) + 10; } }); 12. 插件机制:其实我很简单 jQuery所谓的插件其实就是$.fn上增加的函数, 那这个fn是什么东西? (function(window,undefined){ // 内部又有一个包装 var jQuery = (function() { var jQuery = function( selector, context ) { return new jQuery.fn.init( selector, context, rootjQuery ); } .... // fn实际就是prototype的简写 jQuery.fn = jQuery.prototype = { constructor: jQuery, init: function( selector, context, rootjQuery ) {... } } // 调用jQuery()就是相当于new init(), 而init的prototype就是jQuery的prototype jQuery.fn.init.prototype = jQuery.fn; // 这里返回的jQuery对象只具备最基本的功能, 下面就是一系列的extend return jQuery; })(); ... // 将jQuery暴露为全局对象 window.jQuery = window.$ = jQuery; })(window); 显然, $.fn其实就是jQuery.prototype的简写. 无状态的插件仅仅就是一个函数, 非常简单. // 定义插件 (function($){ $.fn.hoverClass = function(c) { return this.hover( function() { $(this).toggleClass(c); } ); }; })(jQuery); // 使用插件 $('li').hoverClass('hover'); 对于比较复杂的插件开发, jQuery UI提供了一个widget工厂机制, $.widget("ui.dialog", { options: { autoOpen: true,... }, _create: function(){ ... }, _init: function() { if ( this.options.autoOpen ) { this.open(); } }, _setOption: function(key, value){ ... } destroy: function(){ ... } });
调用 $('#dlg').dialog(options)时, 实际执行的代码基本如下所示: this.each(function() { var instance = $.data( this, "dialog" ); if ( instance ) { instance.option( options || {} )._init(); } else { $.data( this, "dialog", new $.ui.dialog( options, this ) ); } } 可以看出, 第一次调用$('#dlg').dialog()函数时会创建窗口对象实例,并保存在data中, 此时会调用_create()和_init()函数, 而如果不是第一次调用, 则是在已经存在的对象实例上调用_init()方法. 多次调用$('#dlg').dialog()并不会创建多个实例.
13. browser sniffer vs. feature detection 浏览器嗅探(browser sniffer)曾经是很流行的技术, 比如早期的jQuery中 jQuery.browser = { version:(userAgent.match(/.+(?:rv|it|ra|ie)[/: ]([d.]+)/) || [0,'0'])[1], safari:/webkit/.test(userAgent), opera:/opera/.test(userAgent), msie:/msie/.test(userAgent) && !/opera/.test(userAgent), mozilla:/mozilla/.test(userAgent) && !/(compatible|webkit)/.test(userAgent) }; 在具体代码中可以针对不同的浏览器作出不同的处理 if($.browser.msie) { // do something } else if($.browser.opera) { // ... }
但是随着浏览器市场的竞争升级, 竞争对手之间的互相模仿和伪装导致userAgent一片混乱, 加上Chrome的诞生, Safari的崛起, IE也开始加速向标准靠拢, sniffer已经起不到积极的作用. 特性检测(feature detection)作为更细粒度, 更具体的检测手段, 逐渐成为处理浏览器兼容性的主流方式. jQuery.support = { // IE strips leading whitespace when .innerHTML is used leadingWhitespace: ( div.firstChild.nodeType === 3 ), ... } 只基于实际看见的,而不是曾经知道的, 这样更容易做到兼容未来.
14. Prototype vs. jQuery prototype.js是一个立意高远的库, 它的目标是提供一种新的使用体验,参照Ruby从语言级别对javascript进行改造,并最终真的极大改变了js的面貌。$, extends, each, bind...这些耳熟能详的概念都是prototype.js引入到js领域的. 它肆无忌惮的在window全局名字空间中增加各种概念, 大有谁先占坑谁有理, 舍我其谁的气势. 而jQuery则扣扣索索, 抱着比较实用化的理念, 目标仅仅是write less, do more而已. 不过等待激进的理想主义者的命运往往都是壮志未酬身先死. 当prototype.js标志性的bind函数等被吸收到ECMAScript标准中时, 便注定了它的没落. 到处修改原生对象的prototype, 这是prototype.js的独门秘技, 也是它的死穴. 特别是当它试图模仿jQuery, 通过Element.extend(element)返回增强对象的时候, 算是彻底被jQuery给带到沟里去了. prototype.js与jQuery不同, 它总是直接修改原生对象的prototype, 而浏览器却是充满bug, 谎言, 历史包袱并夹杂着商业阴谋的领域, 在原生对象层面解决问题注定是一场悲剧. 性能问题, 名字冲突, 兼容性问题等等都是一个帮助库的能力所无法解决的. Prototype.js的2.0版本据说要做大的变革, 不知是要与历史决裂, 放弃兼容性, 还是继续挣扎, 在夹缝中求生.
1. C语言抽象出了软件所在的领域(domain): 由变量v1,v2,...和函数f1,f2,...组成的空间
2. 面向对象(OOP)指出,在这一领域上可以建立分组(group)结构:一组相关的变量和函数构成一个集合,我们称之为对象(Object)。同时在分组结构上可以定义一个运算(推理)关系: D > B, 派生类D从基类B继承(inheritance),相应的派生对象符合基类对象所满足的所有约束。推理是有价值的,因为根据 D > B, B > A 可以自动推导出 D > A,所有针对A的断言在理论上对D都成立(这也就是我们常说的“派生对象 is a 基类对象”)。编译器也能有点智能了。 一个有趣的地方是,D > B意味着在D和B之间存在着某种差异,但是我们却无法把它显式的表达出来!也就是说在代码层面上我们无法明确表达 D - B是什么。为了把更多的信息不断的导入到原有系统中,面向对象内置提供的方法是建立不断扩展的类型树,类型树每增长一层,就可以多容纳一些新的信息。这是一种金字塔式的结构,只不过是一种倒立的金字塔,最终基点会被不断增长的结构压力所压垮。
3. 组件技术(Component)本质上是在提倡面向接口(interface),然后通过接口之间的组合(Composition)而不是对象之间的继承(inheritance)来构造系统。基于组合的观念相当于是定义了运算关系:D = B + C。终于,我们勉强可以在概念层面上做加法了。 组件允许我们随意的组合,按照由简单到复杂的方向构造系统,但是组件构成的成品之间仍然无法自由的建立关系。这意味着组件组装得到的成品只是某种孤立的,偶然的产物。 F = A + B + C ? G = A + D + C。
4. 在数学上,配备了加法运算的集合构成半群,如果要成为群(Group),则必须定义相应的逆运算:减法。 群结构使得大粒度的结构变换成为可能。 F = A + B + C = A + D - D + B + C = (A + D + C) - D + B = G - D + B 在不破坏原有代码的情况下,对原有系统功能进行增删,这就是面向切面(AOP)技术的全部价值。
业务架构平台的设计与实现要比普通业务系统困难很多。一个核心难点在于如何建立普遍有效的应用程序模型,如何控制各种偶然性的业务需求对系统整体架构的冲击。大多数现有的业务架构平台都是提供了一个庞大的万能性产品,它预料到了所有可能在业务系统开发中出现的可能性,并提供了相应的处理手段。业务系统开发人员的能力被限定在业务架构平台所允许的范围之内。如果业务架构平台的复杂度为A+,则我们最多只能用它来开发复杂度为A的业务系统。一个典型的特征就是使用业务架构平台的功能配置非常简单,但是要开发相应的功能特性则非常困难,而且必须采用与业务系统开发完全不同的技术手段和开发方式。 采用业务架构平台来开发业务系统,即使看似开发工作量小,最终产生的各类配置代码量也可能会大大超过普通手工编程产生的代码量,这意味着平台封装了业务内在的复杂性,还是意味着平台引入了不必要的复杂性?很多业务架构平台的卖点都是零代码的应用开发,低水平的开发人员也可以主导的开发,但是为什么高水平的程序员不能借助于这些开发平台极大的提高生产率? 一般的业务架构平台无法回答以下问题: 1) 业务系统可以通过使用设计工具来重用业务架构平台已经实现的功能,但是业务系统内部大量相似的模型配置如何才能够被重用? 2) 特定的业务领域中存在着大量特殊的业务规则,例如“审批串行进行,每一步都允许回退到上一步,而且允许选择跳转到任意后一步”。这些规则如何才能够被引入设计工具,简化配置过程? 3) 已经开发好的业务系统作为产品来销售的时候,如何应对具体客户的定制化?如果按照客户要求修改配置,则以后业务系统自身是否还能够实现版本升级? Witrix平台提供的基本开发模型为 App = Biz aop-extends Generator<DSL> 在这一图景下,我们就可以回答以上三个问题: 1) 业务模型通过领域特定语言(DSL)来表达,因此可以使用语言中通用的继承或者组件抽象机制来实现模型重用。 2) 推理机对于所有推理规则一视同仁,特殊的业务规则与通用的业务规则一样都可以参与推理过程,并且一般情况下特殊的业务规则更能够大幅简化系统实现结构。 3) 相对于原始模型的修改被独立出来,然后应用面向切面(AOP)技术将这些特定代码织入到原始模型中。原始模型与差异修改相互分离,因此原始模型可以随时升级。
Witrix平台所强调的不是强大的功能,而是一切表象之后的数学规律。Witrix平台通过少数基本原理的反复应用来构造软件系统,它本身就是采用平台技术构造的产物。我们用复杂度为A的工具制造复杂度为A+的产品,然后进一步以这个复杂度为A+的产品为工具来构造复杂度为A++的产品。
一种技术思想如果确实能够简化编程,有效降低系统构造的复杂性,那么它必然具有某种内在的数学解释。反之,无论一种技术机制显得如何华丽高深,如果它没有 清晰的数学图象,那么就很难证明自身存在的价值。对于模型驱动架构(MDA),我长期以来一直都持有一种批判态度。(Physical Model Driven http://canonical.javaeye.com/blog/29412 )。原因就在于“由工具自动实现从平台无关模型(PIM)向平台相关模型(PSM)的转换”这一图景似乎只是想把系统从实现的泥沼中拯救出来,遮蔽特定语 言,特定平台中的偶然的限制条件,并没有触及到系统复杂性这一核心问题。而所谓的可视化建模充其量不过是说明人类超强的视觉模式识别能力使得我们可以迅速 识别系统全景图中隐含的整体结构,更快的实现对系统结构的理解,并没有证明系统复杂性有任何本质性的降低。不过如果我们换一个视角, 不把模型局限为某种可视化的结构图,而将它定义为某种高度浓缩的领域描述, 则模型驱动基本上等价于根据领域描述自动推导得到最终的应用程序。沿着这一思路,Witrix平台中的很多设计实际上可以被解释为模型定义,模型推导以及 模型嵌入等方面的探索。这些具体技术的背后需要的是比一般MDA思想更加精致的设计原理作为支撑。我们可以进行如下抽象分析。(Witrix架构分析 http://canonical.javaeye.com/blog/126467 ) 1. 问题复杂?线性切分是削减问题规模(从而降低问题复杂性)的通用手段,例如模块(Module)。(软件中的分析学 http://canonical.javaeye.com/blog/33885 )
App = M1 + M2 + M3 +
2. 分块过多?同态映射是系统约化的一般化策略,例如多态(polymorphism)。(同构与同态:认识同一性 http://canonical.javaeye.com/admin/blogs/340704 )
(abc,abb,ade,) -> [a], (bbb, bcd,bab,) -> [b]
3. 递归使用以上两种方法,将分分合合的游戏进行到底,推向极致。 4. 以少控多的终极形态?如果存在,则构成输入与输出之间的非线性变换(输入中局部微小变化将导致输出中大范围明显的变化)。
App = F(M)
5. 变换函数F可以被诠释为解释器(Interpreter)或者翻译机,例如工作流引擎将工作流描述信息翻译为一步步的具体操作,工作流描述可以看作是由底 层引擎支撑的,在更高的抽象层面上运行的领域模型。但是在这种观点下,变换函数F似乎是针对某种特定模型构造的,引擎内部信息传导的途径是确定的,关注的 重点始终在模型上,那么解释器自身应该如何被构造出来呢?
App ~ M
6. 另外一种更加开放的观点是将变换函数F看作是生成器(Generator)或者推理机。F将根据输入的信息,结合其他知识,推理生成一系列新的命题和断 言。模型的力量源于推导。变换函数F本身在系统构造过程中处于核心地位,M仅仅是触发其推理过程的信息源而已。F将榨干M的最后一点剩余价值,所有根据M 能够确定的事实将被自动实现,而大量单靠M自身的信息无法判定的命题也可以结合F内在的知识作出判断。生成器自身的构造过程非常简单--只要不断向推理系 统中增加新的推理规则即可。语言内置的模板机制(template)及元编程技术(meta programming),或者跨越语言边界的代码生成工具都可以看作是生成器的具体实例。(关于代码生成和DSL http://canonical.javaeye.com/blog/275015 )
App = G<M>
7. 生成器G之所以可以被独立实现,是因为我们可以实现相对知识与绝对知识的分离, 这也正是面向对象技术的本质所在。(面向对象之形式系统 http://canonical.javaeye.com/blog/37064 )
G<M> ==> G = {m => m.x(a,b,c);m.y(); }
8. 现实世界的不完美,就在于现实决不按照我们为其指定的理想路线前进。具体场景中总是存在着大量我们无法预知的“噪声”,它们使得任何在“过去”确立的方程 都无法在“未来”保持持久的平衡。传统模型驱动架构的困境就在于此。我们可以选择将模型M和生成器G不断复杂化,容纳越来越多的偶然性,直至失去对模型整 体结构的控制力。另外一种选择是模型在不断膨胀,不断提高覆盖能力的过程中,不断的空洞化,产生大量可插入(plugin)的接入点,最终丧失模型的推理 能力,退化成为一种编码规范。Witrix平台中采用的是第三种选择:模型嵌入--模型中的多余信息被不断清洗掉,模型通过精炼化来突出自身存在的合理 性,成为更广泛的运行环境中的支撑骨架。(结构的自足性 http://canonical.javaeye.com/blog/482620 )
App != G0<M0> , App != G0<M1>, App = G1<M1>
9. 现在的问题是:如何基于一个已经被完美解决的重大问题,来更有效率的搞定不断出现但又不是重复出现的小问题。现在我们所需要的不是沿着某个维度进行均匀的 切分,而必须是某种有效的降维手段。如果我们可以定义一种投影算子P, 将待解决的问题投射到已经被解决的问题域中,则剩下的补集往往可以被简化。(主从分解而不是正交分解 http://canonical.javaeye.com/blog/196826 )
dA = App - P[App] = App - G0<M0>
10. 要实现以上微扰分析策略,前提条件是可以定义逆元,并且需要定义一种精细的粘结操作,可以将分散的扰动量极为精确的应用到基础系统的各处。Witrix平台的具体实现类似于某种AOP(面向切面编程)技术。(逆元:不存在的真实存在 http://canonical.javaeye.com/blog/325051 )
App = A + D + B = (A + B + C) - C + D = App0 + (-C + D) = G0<M0> + dA
11. 模型驱动并不意味着一个应用只能由唯一的一个模型来驱动,但是如果引入多个不同形式的模型,则必须为如下推理提供具体的技术路径: A. 将多个模型变换到共同的描述域 B. 实现多个模型的加和 C. 处理模型之间的冲突并填补模型之间的空白 在Witrix平台中模型嵌入/组合主要依赖于文本化及编译期运行等机制。(文本化 http://canonical.javaeye.com/blog/309395 )
App = Ga<Ma> + Gb<Mb> + dA
12. 系统的开发时刻t0和实施时刻t1一般是明确分离的,因此如果我们要建立一个包含开发与实施时刻信息的模型,则这一模型必须是含时的,多阶段的。关于时 间,我们所知道的最重要的事实之一是“未来是不可预知的”。在t0时刻建立的模型如果要涵盖t1时刻的所有变化,则必须做出大量猜测,而且t1时刻距离 t0时刻越远,猜测的量越大,“猜测有效”这一集合的测度越小,直至为0。延迟选择是实现含时系统控制的不二法门。 在Witrix平台中,所有功能特性的实现都包含某种元数据描述或者定制模板,因此结合配置机制以及动态编译技术既可实现多阶段模型。例如对于一个在未来 才能确定的常量数组,我们可以定义一个Excel文件来允许实施人员录入具体的值,然后通过动态编译技术在编译期解析Excel文件,并完成一系列数值映 射运算,最终将其转化为编译期存在的一个常量。这一过程不会引入任何额外的运行成本,也不要求任何特定的缓存机制,最终的运行结构与在未来当所有信息都在 位之后再手写代码没有任何区别。(D语言与tpl之编译期动作 http://canonical.javaeye.com/blog/57244 )
App(t1) = G(t0,t1)<M(t0,t1)> + dA(t0,t1)
13. 级列理论提供了一个演化框架,它指出孤立的模型必须被放在模型级列中被定义,被解释。(关于级列设计理论 http://canonical.javaeye.com/blog/33824 )
M[n] = G<M[n-1]> + dMn
14. 推理的链条会因为任何局部反例的出现而中断。在任意时空点上,我们能够断言的事实有哪些?信息越少,我们能够确定的事实越少,能够做出的推论也就越少。现 在流行的很多设计实质上是在破坏系统的对称性,破坏系统大范围的结构。比如明明ORM容器已经实现所有数据对象的统一管理,非要将其拆分为每个业务表一个 的DAO接口。很多对灵活性的追求完全没有搞清楚信息是对不确定性的消除,而不确定性的减少意味着限制的增加,约束的增加。(From Local To Global http://canonical.javaeye.com/blog/42874 ) 组件/构件技术的宣言是生产即组装,但是组装有成本,有后遗症(例如需要额外的胶水或者螺钉)。软件的本质并不是物质,而是信息,而信息的本质是抽象的规 律。在抽象世界中最有效的生产方式是抽象的运算,运算即生产。组件式开发意味着服从现有规律,熟练应用,而原理性生产则意味着不断创造新的规律。功能模 块越多,维护的成本越高,是负担,而推理机制越多,生产的成本越低,是财富。只有恢复软件的抽象性,明确把握软件构造过程内在的数学原理,才能真正释放软 件研发的生产力。(从编写代码到制造代码 http://canonical.javaeye.com/blog/333167 )
注解1:很多设计原则其实是在强调软件由人构造由人理解,软件开发本质上是人类工程学,需要关注人类的理解力与协作能力。例如共同的建模语言减少交互成本,基于模型减少设计与实现的分离,易读的代码比高性能的代码更重要,做一件事只有唯一的一种方式等。 注解2:生成系统的演绎远比我们想象的要深刻与复杂得多。例如生命的成长可以被看作是在外界反馈下不断调整的生成过程。 注解3:领域描述是更紧致但却未必是更本质的表达。人类的DNA如果表达为ATGC序列完全可以拷贝到U盘中带走,只要对DNA做少量增删,就可以实现老 鼠到人类的变换(人类和老鼠都有大约30000条基因,其中约有80%的基因是“完全一样的”,大约共享有99%的类似基因),但是很难认为人类所有智慧 的本质都体现在DNA中,DNA看起来更像是某种序列化保存形式而已。 注解4:模型转换这一提法似乎是在强调模型之间的同构对应,转换似乎总是可以双向进行的,仅仅是实现难度限制了反向转换而已。但是大量有用的模型变换却是单向的,变换过程要求不断补充新的信息。 注解5:模型驱动在很多人看来就是数据库模型或者对象模型驱动系统界面运行,但实际上模型可以意指任意抽象知识。虽然在目前业内广泛流行的对象范式下,所 有知识都可以表达为对象之间的关联,但是对象关系(名词之间的关联关系)对运算结构的揭示是远远不够充分的。很多时候所谓的领域模型仅仅是表明概念之间具 有相关性,但是如果不补充一大段文字说明,我们对于系统如何运作仍然一知半解。数学分析其实是将领域内在的意义抽空,仅余下通用的形式化符号。
html主要通过内置的<script>,<link>, <img>等标签引入外部的资源文件,一般的Web框架并没有对这些资源文件进行抽象,因此在实现组件封装时存在一些难以克服的困难。例如一个使用传统JSP Tag机制实现的Web组件中可能用到js1.js, js2.js和css1.css等文件,当在界面上存在多个同样的组件的时候,可能会生成多个重复的<script>和<link>标签调用,这将对页面性能造成严重的负面影响。资源管理应该是一个Web框架的内置组成部分之一。在Witrix平台中,我们主要借助于tpl模板引擎来输出html文本, 因此可以通过自定义标签机制重新实现资源相关的html标签, 由此来提供如下增强处理功能:
1. 识别contextPath tpl模板中的所有资源相关标签都会自动拼接Web应用的contextPath, 例如当contextPath=myApp时 <script src="/a.js"></script> 将最终输出 <script src="/myApp/a.js" ...>
2. 识别重复装载 <script src="a.js" tpl:once="true"></script> tpl:once属性将保证在页面中script标签实际只会出现一次.
3. 识别组件内相对路径 开发Web组件时,我们希望所有资源文件都应该相对组件目录进行定位,但是直接输出的<script>等标签都是相对于最终的调用链接进行相对路径定位的. 例如在page1.jsp中调用了组件A, 在组件A的实现中, 输出了<script src="my_control.js"></script> 我们的意图一般是相对于组件A的实现文件进行定位, 而不是相对于page1.jsp进行定位. tpl模板引擎的相对路径解析规则为永远相对于当前文件进行定位. 例如 <c:include src="sub.tpl" /> 在sub.tpl中的所有相对路径都相对于sub.tpl文件进行定位.
4. 编译期文件有效性检查 在编译期, tpl引擎会检查所有引入的资源文件的有效性. 如果发现资源文件丢失, 将直接抛出异常. 这样就不用等到上线后才发现文件命名已修改等问题.
5. 缓存控制 浏览器缺省会缓存css, js等文件, 因此系统上线后如果修改资源文件可能会造成与客户端缓存不一致的情况. 一个简单的处理方式是每次生成资源链接的时候都拼接文件的修改日期或者版本号, 这样既可利用客户端缓存, 又可以保证总是使用最新版本. 例如 <script src="a.js"></script>将会输出 <script src="/myApp/myModule/a.js?344566" ...>
6. 字符集选择 为了简化国际化处理, 一般提倡的最佳实践方式是坚持使用UTF-8编码. 但是很多情况下可能使用系统内置的GBK编码会更加方便一些, 另外集成一些既有代码时也存在着不同字符集的问题. 在Witrix平台中, 所有输出的资源标签都会标明对应的字符集, 如果没有明确设置就取系统参数中的缺省字符集. 例如 <script src="a.js"></script> 将会输出 <script ... charset="GBK"></script>
7. 缺省theme支持 为了支持多种页面风格, 往往不是简单的替换css文件即可实现的, 它可能意味着整个组件的实现代码的更换. Witrix平台中通过一系列缺省判断来简化这一过程. 例如如下代码表明如果设置了ui_theme系统参数, 并且对应的特殊实现存在, 则使用特殊实现, 否则系统缺省实现. <c:include src="${cp:ui_theme()}/ctl_my_ctl.tpl" > <c:include src="default/ctl_my_ctl.tpl" /> </c:include>
AOP(Aspect Oriented Programming)早已不是什么新鲜的概念,但有趣的是,除了事务(transaction), 日志(Log)等寥寥几个样板应用之外,我们似乎找不到它的用武之地。 http://canonical.javaeye.com/blog/34941 很多人的疑惑是我直接改代码就行了,干吗要用AOP呢?AOP的定义和实现那么复杂,能够提供什么特异的价值呢? Witrix平台依赖于AOP概念来完成领域模型抽象与模型变换,但是在具体的实现方式上,却与常见的AOP软件包有着很大差异。 http://canonical.javaeye.com/blog/542622 AOP的具体技术内容包括定位和组装两个部分。简化切点定位方式和重新规划组装空间,是Witrix中有效使用AOP技术的前提。 在Witrix平台中,对于AOP技术的一种具体应用是支持产品的二次开发。在产品的实施过程中,经常需要根据客户的特定需求,修改某些函数的实现。我们 可以选择在主版本代码中不断追加相互纠缠的if-else语句,试图去包容所有已知和未知的应用场景。我们也可以选择主版本代码和定制代码独立开发的方 式,主版本代码实现逻辑框架,定制代码通过AOP机制与主版本代码融合,根据具体场景要求对主版本功能进行修正。AOP的这种应用与所谓的横切概念是有所 区别的。典型的,一个横切的切点会涉及到很多类的很多方法,而函数定制则往往要求准确定位到某个业务对象的某个特定的业务方法上。传统AOP技术的切点定 义方式并不适合这种精确的单点定位。在Witrix平台中,我们通过直接的名称映射来定义切点。例如,修正spring中注册的MyObject对象的 myFunc方法,可以在app.aop.xml文件中增加如下标签
<myObject.myFunc> 在原函数执行之前执行 <aop:Proceed/> <!-- 执行原函数内容 --> 在原函数执行之后执行</myObject.myFunc>
[spring对象名.方法名]这种映射方法比基于正则字符串匹配的方式要简单明确的多。spring容器本身已经实现了对象的全局管理功能,spring对象名称必然是唯一的,公开发布的,相互之间不冲突的,没有必要再通过匹配运算重新发现出它的唯一性。 对于一些确实存在的横切需求,我们可以通过Annotation机制来实现切点坐标标定,将复杂的切点匹配问题重新划归为[对象名.方法名]。
@AopClass({"myObject","otherObject"}) class SomeClass{ @AopMethod({"myFunc","otherFunc"}) void someFunc(){} }
针对以上对象,在app.aop.xml文件中可以定义
<I-myObject.I-myFunc> .</I-myObject.I-myFunc>
结构的稳定性,直观的理解起来,就是结构在存在外部扰动的情况下长时间保持某种形式不变性的能力。稳定意味着小的扰动造成的后果也是“小”的。在数学中,Taylor级数为我们描绘了变化传播的基本图景。
F(x0 + dx) = F(x0) + F'(x0)*dx + 0.5*F''(x0)*dx^2 +
扰动dx可能在系统F中引发非常复杂的作用过程,在系统各处产生一个个局部变化结果。表面上看起来,似乎这些变化结果存在着无穷多种可能的分组方式,例如 (F'(x0)-2)*dx + 2*dx^2, 但是基于微分分析,我们却很容易了解到Taylor级数的每一级都对应着独立的物理解释,它们构成自然的分组标准。某一量级下的所有变化汇总归并到一起,并对应一个明确的整体描述。在抽象的数理空间中,我们具有一种无所不达的变化搜集能力。变化项可以从基础结构中分离出来,经过汇总后可以对其进行独立的研究。变化本身并不会直接导致基础结构的崩溃。 在软件建模领域,模型的稳定性面临的却是另一番场景。一个软件模型一旦被实现之后,种种局部需求变更就都会形成对原有基础结构的冲击。一些局部的需求变化可能造成大片原有实现失效,我们将被迫为类似的需求重新编写类似的代码。此时,软件开发并不像是一种纯粹的信息创造,而是宛若某种物质产品的生产(参见从编写代码到制造代码 http://canonical.javaeye.com/blog/333167 )。显然,我们需要一种能力,将局部变化从基础结构中剥离出来,经过汇总归并之后再进行综合分析和处理。这正是AOP(Aspect Oriented Programming)技术的价值所在。
M1 = (G0+dG0)<M0+dM0> ==> M1 = G0<M0> + dM
AOP本质上是软件结构空间的自由修正机制。只有结合AOP技术之后,软件模型才能够重新恢复抽象的本质,在时间之河中逃离随机变化的侵蚀,保持实现层面的稳定性。在这一背景下,建模的目的将不是为了能够跟踪最终需求的变动,而是要在某个独立的层面上能够自圆其说,能够具有某种独立存在的完满性,成为思维上可以把握的某个稳定的基点。模型的真实性将因为自身结构的完备性而得到证明,与外部世界的契合程度不再是价值判断的唯一标准。 http://canonical.javaeye.com/blog/482620
说到软件建模,一个常见的论调是模型应该符合实际需求,反映问题的本质。但是何谓本质,却是没有先验定义的。在成功的建立一个模型之前,无论在内涵上还是在外延上我们都很难说清楚一个问题的本质是什么。如果将模型看作是对领域结构的一种显式描述和表达,我们可以首先考察一下一个“合适”的结构应该具备哪些特征。 按照结构主义哲学的观点,结构具有三个要素:整体性,具有转换规律或法则(转换性),自身调整性(自律性)。整体性意味着结构不能被简单的切分,其构成要素通过内在的关系运算实现大范围的关联与转换,整体之所以成为整体正是以转换/运算的第一性为保证的。这种转换可以是共时的(同时存在的各元素),也可以是历时的(历史的转换构造过程),这意味着结构总要求一个内在的构造过程,在独立于外部环境的情况下结构具有某种自给自足的特性,不依赖于外部条件即可独立的存在并保持内在的活动。自律性意味着结构内在的转换总是维持着某种封闭性和守恒性,确保新的成分在无限地构成而结构边界却保持稳定。注意到这里对结构的评判并不是来自外在规范和约束,而是基于结构内在的规律性,所强调的不是结构对外部条件的适应性,而是自身概念体系的完备性。实际上,一个无法直接对应于当前实际环境的结构仍然可能具有重要的价值,并在解决问题的过程中扮演不可或缺的角色。在合理性这个视角下,我们所关注的不仅仅是当前的现实世界,而是所有可能的世界。一个“合理”的结构的价值必能在它所适应的世界中凸现出来。 在信息系统中,我们可能经常会问这个模型是否是对业务的准确描述,是否可以适应需求的变更,是否允许未来的各种扩展等等。但是如果换一个思维方向,我们会发现这些问题都是针对最终确立的模型而发问的,而在模型构建的过程中,那些可被利用的已存在的或者可以存在的模型又是哪些呢。每一个信息模型都对应着某种自动推理机,可以接收信息并做一定的推导综合工作。一个可行的问题是,如何才能更有效的利用已有的信息进行推导,如何消除冗余并减少各种转换成本。我们经常可以观察到,某一信息组织方式更充分的发掘了信息之间的内在关联(一个表象是它对信息的使用不是简单的局域化的,而是在多处呈现为互相纠缠的方式,难以被分解),这种内在关联足够丰富,以至于我们不依赖于外部因素就可以独立的理解。这种纠缠在一起的信息块自然会成为我们建模的对象。 如果模型的“覆盖能力”不再是我们关注的重点,那么建模的思维图式将会发生如下的转化
最终的模型可以由一系列微模型交织构成。模型的递进构造过程并不同于组件(Component)的实物组装接口,也不是CAD图纸堆叠式的架构概念所能容纳的。在Witrix平台中,模型分解和构造表达为如下形式 http://canonical.javaeye.com/blog/333167 Biz[n] = Biz[n+1] aop-extends CodeGenerator<DSLx, DSLy>。 在软件发展的早期,所有的程序都是特殊构造的,其必然的假设是【在此情况下】,重用不在考虑范围之内,开发可以说是一个盲目试错的过程。随着我们逐步积累了一些经验,开始自觉的应用理论分析手段,【在所有情况下】都成立的一些普适的原理被揭示出来,它们成为我们在广阔的未知世界中跋涉时的向导。当我们的足迹渐渐接近领域的边界,对领域的全貌有一个总体的认知之后,一种对自身成熟性的自信很自然的将我们导向更加领域特定的分析。很多时候,我们会发现一个特化假设可以大大提高信息的利用率,推导出众多未被显式设定的知识。我们需要那些【在某些情况下】有效的规则来构成一个完备的模型库。这就如同有大量备选的数学定理,面对不同的物理现象,我们会从一系列的数学工具中选择一个进行使用一样。
软件开发技术的技术本质在于对代码结构的有效控制. 我们需要能够有效的分解/重组代码片断, 凸显设计意图. 面向对象是目前最常见的代码组织技术. 典型的, 它可以处理如下模式 A1 --> B2, A2 --> B2, A3 --> B3 ... 我们观察到A1和A2之间, B2和B2之间具有某种概念关联性, 同时存在某种抽象结构 [A] --> [B]. 对于这种情况, 我们可以定义对象 [A], [B], 它们分别是 A1和A2的聚合, B1和B2的聚合等. 举例来说, 对于如下表格描述, <ui:Col>所提供的信息在映射为html实现的时候将在多处被应用. < ui:Table data="${data}"> <ui:Col name="fieldA" label="labelA" width="20" /> <ui:Col name="fieldB" label="labelB" width="10" /> < /ui:Table> 这里<ui:Col>提供的信息对应三个部分的内容: 1. 列标题 2. 列样式(宽度等) 3. 列数据
面向对象的常见做法是抽象出 UiCol对象, 它作为UiTable对象的属性存在, 在生成表头, 表列样式和表格数据内容时将被使用. 但是我们注意到面向对象要求多个方法通过this指针形成状态耦合
,在某种意义上它意味着所有的成员方法在任一时刻都是同时存在着的。它们所代表着的存在的代价必须被接受(存储空间等)。即使并不同时被使用,我们仍然需要同时持有所有成员函数指针及
共享的this指针。实际上, 我们并不一定需要A1和A2同时在场. 在这种情况下, 编译期技术可以提供另一种不同的行为聚合方式.
< table> <thead> <sys:CompileTagBody cp:part="thead" /> </thead> <cols> <sys:CompileTagBody cp:part="cols" /> </cols> <tbody> <sys:CompileTagBody cp:part="tbody" /> </tbody> < /table>
只要<ui:Col>标签的实现中针对编译期的cp:part变量进行分别处理, 即可实现信息的部分析取.
html最早的设计目标只是作为某种多媒体文档展现技术,其设计者显然无法预料到今天Web应用的蓬勃发展,一些设计缺陷也就难以避免。特别是html规范中缺乏对于复杂交互式组件模型的支持,直接导致企业应用的前台开发困难重重。AJAX技术可以看作是对这种困境的一种改良性响应,它试图通过javascript语言在应用层创建并维护一系列复杂的交互机制。很多完善的ajax框架走得相当遥远,最终基本将html作为一种底层“汇编”语言来使用。例如,一个很整齐美观的类Excel表格可能是由一个个div拼接而成,与html原生的table元素已经没有任何关系。 Witrix平台中对于前台html模型也作了一定的增强,但基本的设计思想是尽量利用原生控件,并尽量保持原生控件内在的数据关系,而不是重新构建一个完整的底层支撑环境。采用这种设计的原因大致有如下几点: 1. 前台技术目前竞争非常激烈,我们优先选择的方式是集成第三方组件,尽量保持原生环境有利于降低集成成本。 2. 通过javascript构造的控件可能存在性能瓶颈和其他浏览器内在的限制。例如一般Ajax框架提供的Grid控件都无法支撑大量单元格的显示。 3. Witrix平台的tpl模板技术可以非常方便的生成html文本,并提供强大的控件抽象能力,因此在前台动态创建并组织界面元素在Witrix平台中是一种不经济的做法。 4. Witrix平台提供的分解机制非常细致,存储于不同地方的不同来源的代码会在不同的时刻织入到最终的页面中,基于原生环境有利于降低平台快速演进过程中的设计风险。
Witrix平台中对于html模型的增强主要关注于以最少的代码实现界面控件与业务逻辑的自然结合。基本结构包括: 1. 通过ControlManager对象在前台建立一种container结构,统一管理控件的注册和获取。js.makeControl(elmOrId)返回特殊注册的控件对象或者根据原生html元素生成一个包装对象。 2. 通过js.getWxValue(elm)和js.setWxValue(elm,value)这两个函数统一对控件的值的存取过程。 3. 通过js.regListener(elm,listenerFunc)统一管理控件之间的相关触发,实现控件之间的相互监听。当js.setWxValue(elm,value)被调用时,注册在ControlManager中的listenerFunc将被调用。 4. stdPage.setFieldValue(fieldName,value)和stdPage.getFieldValue(fieldName,value)统一针对业务字段的值的存取过程,这里fieldName对应于实体上的业务字段名。 5. 通过ajax.addForm(frmId)等函数统一前台提交参数的提取过程,通过stdPage.buildAjax()等函数统一后台服务的调用方式。 6. 通过stdPage对象统一封装业务场景中的"常识"。 基于以上一些基础机制,Witrix平台即可提供一些复杂的业务组件封装。例如<input name="productCode" onkeypress="stdPage.keyPressToLoadRefByCode({objectName:'SomeProduct',queryField:'productCode'})" .../>通过简单的调用一个js函数即可实现如下功能: a. 在文本框中输入回车的时候自动提交到后台查找对应产品代码的产品,并更新前台多个相关字段的值 b. 如果没有查找到相应产品,则弹出对话框根据界面上已有的部分字段信息提示客户添加新的产品信息。 c. 如果找到多个对应产品,则弹出列表允许客户选择其一。 d. 具体的处理过程可以通过函数参数进行精细的控制。 在meta文件中,结合上下文环境中的元数据信息,我们在缺省情况下可以直接使用 <ds:LoadRefByCodeInputor objectName="SomeProduct" />标签,不需要任何其他附加参数。
Witrix平台中一般利用原生控件来保存数据值,而不是将数据保存在分离的js对象中。例如对于一个选择控件,经常要求选择得到的是实体的id,而显示在界面上的是某个其他字段的值。Witrix平台中一般的实现结构是 <input type="hidden" name="${fieldName}" value="${entity[dsMeta.idField]}" id="${id}" textId="text_${id}" /> <input type="text" value="${entity[dsMeta.nameField]}" id="text_${id}" /> 通过textId等扩展属性即可明确定义控件多个部分之间的关联关系,同时保证控件的实现完全与html规范相兼容。 Witrix平台中目前使用的"标准化"的扩展属性有textId(对应文本显示控件的id), showName(某些无文字显示的选择控件需要保留显示字段值), op(字段作为查询条件提交时的比较算符),validator(字段值对应的检验函数),setWxValue/getWxValue(重定义控件值的存取行为),serializer(特殊处理前台控件的提交参数)等。扩展属性不仅可以引入说明信息,还可以引入丰富的控件行为。
|
No comments:
Post a Comment