叶凡网络:程序的本质复杂性和元语言抽象

2013-11-04 09:18:58 | 新闻来源:叶凡网络 | 点击量:651

即著名的NoSilverBullet认为不存在一种技术能使得软件开发在生产力、可靠性、简洁性方面提高一个数量级。不清楚Brook这一论断详细的背景,但是就个人的开发经验而言,元驱动编程和普通编程方法相比在生产力、可靠性和简洁性方面的确是数量级的提升,人月神话》作者FredBrook曾在80年代论述了对于软件复杂性的看法。看来它就是软件开发的银弹!

组件复用技术的局限性

一直严格遵循DRY原则,常听到有人讲“写代码很讲究。把重复使用的功能都封装成可复用的组件,使得代码简短优雅,同时也易于理解和维护”显然,DRY原则和组件复用技术是最常见的改善代码质量的方法,不 过,看来以这类方法为指导,能帮助我写出“不错的顺序”但还缺乏以帮助我写出简短、优雅、易理解、易维护的好程序”对于熟悉MartinFowler重构》和GoF设计模式》顺序员,经常提出这样一个问题帮助他进一步加深对程序的理解:

 如果目标是代码“简短、优雅、易理解、易维护”组件复用技术是最好的方法吗?这种方法有没有根本性的局限?

 

提升了代码的笼统层次,虽然基于函数、类等形式的组件复用技术从一定水平上消除了冗余。但是这种技术却有着本质的局限性,其根源在于 每种组件形式都代表了特定的笼统维度,组件复用只能在其维度上进行抽象层次的提升比方,可以把常用的HashMap等功能封装为类库,但是不管怎么封装复用类永远是类,封装虽然提升了代码的笼统层次,但是永远不会变成Lambda而实际问题所代表的笼统维度往往与之并不匹配。

组件复用技术所能做到只是把读取字节,以常见的二进制消息的解析为例。检查约束,计算CRC等功能封装成函数,这是远远不够的比方,下面的表格定义了二进制消息X格式:

解析函数大概是这个样子:

bool parse_message_xchar* data, int32 size, MessageX& x { 

  •     char *ptr = data;     if ptr + sizeofint8 <= data + s { 
  •         x.message_type = read_int8ptr;         if 0x01 != x.message_typ return false; 
  •         ptr += sizeofint8;     } else { 
  •         return false;     } 
  •     if ptr + sizeofint16 <= data + s {         x.payload_size = read_int16ptr; 
  •         ptr += sizeofint16;     } else { 
  •         return false;     } 
  •     if ptr + x.payload_size <= data + s {         x.payload = new int8[x.payload_size]; 
  •         readptr, x.payload, x.payload_s;         ptr += x.payload_size; 
  •     } else {         return false; 
  •     }     if ptr + sizeofint32 <= data + s { 
  •         x.crc = read_int32ptr;         ptr += sizeofint32; 
  •     } else {         delete x.payload; 
  •         return false;     } 
  •     if crcdata, sizeofint8 + sizeofint16 + x.payload_s != x.crc {         delete x.payload; 
  •         return false;     } 
  •     return true; } 

    虽然消息X定义非常简单,很明显。但是解析函数却显得很繁琐,需要小心翼翼地处理很多细节。处置其他消息Y时,虽然虽然Y和X很相似,但是却不得不再次在解析过程中处置这些细节,就是组件复用方法的局限性,只能帮我依照函数或者类的语义把功能封装成可复用的组件,但是消息的结构特征既不 函数也不是类,这就是笼统维度的失配。

    顺序的实质复杂性

    现在要进一步思考:上面分析了组件复用技术有着根本性的局限性。

    如果目标还是代码“简短、优雅、易理解、易维护”那么代码优化是否有一个理论极限?这个极限是由什么决定的普通代码比起最优代码多出来的冗余部分”底干了些什么事情?

    综合二位的观点,回答这个问题要从程序的实质说起。Pascal语言之父NiklauWirth70年代提出:Program=DataStructur+Algorithm随后逻辑学家和计算机科学家RKowalski进一步提出:Algorithm=Logic+Control谁更深刻更有启发性?当然是后者!而且我认为数据结构和算法都属于控制战略。加上我自己的理解,顺序的实质 Program=Logic+Control换句话说,顺序包括了逻辑和控制两个维度。

    比方,逻辑就是问题的定义。对于排序问题来讲,逻辑就是什么叫做有序,什么叫大于,什么叫小于,什么叫相等”控制就是如何合理地安排时间和空间 资源去实现逻辑。逻辑是顺序的灵魂,定义了顺序的实质;控制是为逻辑服务的非本质的可以变化的如同排序有几十种不同的方法,时间空间效率各不相 同,可以根据需要采用不同的实现。

     顺序的实质复杂性就是逻辑,顺序的复杂性包括了实质复杂性和非实质复杂性两个方面。套用这里的术语。非实质复杂性就是控制逻辑决定了代码复杂性的下限,也就是说不管怎么做代码优化,Office顺序永远比Notepad顺序复杂,这是因为前者的逻辑就更为复杂。如果要代码简洁优雅,任何语言和技术所能做的只是尽量接近这个实质复杂性,而不可能逾越这个理论下限。

    理解”顺序的实质复杂性是由逻辑决定的从理论上为我指明了代码优化的方向:让逻辑和控制这两个维度坚持正交关系。来看JavaCollections.sort方法的例子:

    interface Comparator<T> { 
  •     int comparT o1, T o2; } 
  • public static <T> void sortList<T> list, Comparator<? super T> compar 

    即提供一个Compar对象标明序在类型T上的定义;控制的局部完全交给方法实现者,使用者只关心逻辑部份。可以有多种不同的实现,这就是逻辑和控制解耦。同时,也可以断定,这个设计已经达到代码优化的理论极限,不会有本质上比它更简洁的设计(忽略相同语义的语法差别)为什么?因为 逻辑决定了实质复杂度,Compar和Collections.sort定义完全是逻辑的体现,不包括任何非本质的控制局部。

    控制往往直接决定了顺序的性能,另外需要强调的上面讲的控制是非实质复杂性”并不是说控制不重要。当我因为性能等原因必需采用某种控制的时 候,实际上被固化的控制战略也是一种逻辑。比方,当你需求是从进程虚拟地址ptr1拷贝1024个字节到地址ptr2那么它就是问题的定义,就 逻辑,这时,提供进程虚拟地址直接访问语义的底层语言就与之完全匹配,反而是更高层次的语言对这个需求无能为力。

    可能很多朋友已经开始意识到上面二进制文件解析实现的问题在哪里,介绍了逻辑和控制的关系。其实这也是 绝大多数顺序不够简洁优雅的根本原因:逻辑与控制耦合上面那个消息定义表格就是不包含控制的纯逻辑,相信即使不是顺序员也能读懂它而相应的代码把逻辑和控制搅在一起之后就不那么容易读懂了

    不是一回事呢?其实,把这里所说的逻辑和OOP中的接口划等号是似是而非的而GoF设计模式最大的问题就在于有意无意地让人们以为“what就是interface,熟悉OOP和GoF设计模式的朋友可能会把“逻辑与控制解耦”与经常听说的接口和实现解耦”联系在一起。interfac就是what很多朋友一想到要表达what要抽象,马上写个接口出来,这就是潜移默化的惯性思维,自己根本意识不到问题在哪里。其实,接口和前面提到组件复用技术一样,同样受限于特定的笼统维度,不是表达逻辑的通用方法,比方,无法把二进制文件格式特征用接口来表示。

    熟悉的许多GoF模式以“逻辑与控制解耦”观点来看,另外。都不是最优的比方,很多时候Observer模式都是典型的以控制代逻辑,来看一个例子:

    要求其颜色随着状态不同而变化, 对于某网页的超链接。点击之前的颜色是#FF0000点击后颜色变成#00FF00

    基于Observer模式的实现是这样的

    $a.css'color', '#FF0000'; 
  •   $a.clickfunction { 
  •     $thi.css'color', '#00FF00'; }; 

    而基于纯CSS实现是这样的

    a:link {color: #FF0000} 
  • a:visited {color: #00FF00} 

    您看出二者的差异了吗?显然,通过对比。Observer模式包括了非本质的控制,而CSS只包含逻辑。理论上讲,CSS能做的事情,JavaScript都能通过控制做到那么为什么浏览器的设计者要引入CSS呢,这对我有何启发呢?

    元语言抽象

    好的继续思考下面这个问题:

    但接口不是表达逻辑的通用方式, 逻辑决定了顺序的实质复杂性。那么是否存在表达逻辑的通用方式呢?

    通常所说的配置就是元,答案是有!这就是元(Meta包括元语言(MetaLanguag和元数据(MetaData两个方面。元并不神秘。元语言就是配置的语法和语义,元数据就是具体的配置,之间的关系就是C语言和C顺序之间 关系;但是同时元又非常神奇,因为元既是数据也是代码,表达逻辑和语义方面具有无与伦比的灵活性。至此,终于找到让代码变得简洁、优雅、易理 解、易维护的终极方法,这就是 通过元语言笼统让逻辑和控制完全解耦

    对于二进制消息解析,比方。经典的做法是类似GooglProtocolBuffer把消息结构特征笼统进去,定义消息描述元语言,再通过元数据描述消息结构。下面是ProtocolBuffer元数据的例子,这个元数据是纯逻辑的表达,复杂度体现的消息结构的实质复杂度,而如何序列化和解析这些控制相关的局部被 ProtocolBuffer编译器隐藏起来了

    message Person { 
  •   required int32 id = 1;   required string name = 2; 
  •   optional string email = 3; } 

    但是最终要与控制相结合成为具体实现,元语言解决了逻辑表达问题。这就是元语言到目标语言的映射问题。通常有这两种方法:

    将元数据编译为目标顺序代码;1元编程(MetaProgram开发从元语言到目标语言的编译器。

    2元驱动编程(MetaDrivenProgram直接在目标语言中实现元语言的解释器。

    元编程由于有静态编译阶段,这两种方法各有优势。一般发生的目标顺序代码性能更好,但是这种方式混合了两个层次的代码,增加了代码配置管理的难度,一般还需要同时配备Build脚本把整个代码生成自动集成到Build过程中,此外,和IDE集成也是问题;元驱动编程则相反,没有静态编译过程,元语 言代码是动态解析的所以性能上有损失,但是更加灵活,开发和代码配置管理的难度也更小。除非是性能要求非常高的场所,推荐的元驱动编程,因为它更轻 量,更易于与目标语言结合。

    meta_message_x元数据,下面是用元驱动编程解决二进制消息解析问题的例子。parse_messag解释器:

    var meta_message_x = { 
  •     id: 'x',     fields: [ 
  •         { name: 'message_type', type: int8, value: 0x01 },         { name: 'payload_size', type: int16 }, 
  •         { name: 'payload', type: bytes, size: '$payload_size' },         { name: 'crc', type: crc32, source: ['message_type', 'payload_size', 'payload'] } 
  •     ] } 
  •   var message_x = parse_messagemeta_message_x, data, s; 

    因为对于支持Liter类似JSON对象表示的语言中,这段代码我用的JavaScript语法。实现元驱动编程最为简单。如果是Java或C++语言,语法上稍微繁琐一点,不过实质上是一样的或者引入JSON配置文件,然后解析配置,或者定义MessageConfig类,直接把这个类 对象作为配置信息。

    有ProtocolBufferAndroidAIDL等大量的实例,二进制文件解析问题是一个经典问题。所以很多人能想到引入消息定义元语言,但是如果我把问题稍微变换,能想到采用这种方法的人就不多了来看下面这个问题:

    和个性设置等Web表单。出于性能和用户体验的考虑, 某网站有新用户注册、用户信息更新。用户点击提交表单时,会先进行浏览器端的验证,比方:name字段至少3个字符,password字段至少8个字符,并且和repeatpassword要一致,email要符合邮箱格式;通过浏览器端验证以后才通过HTTP请求提交到服务器。

    普通的实现是这个样子的

    function check_form_x { 
  •     var name = $'#name'.val;     if null == name || name.length <= 3 { 
  •         return { status : 1, message: 'Invalid name' };     } 
  •       var password = $'#password'.val; 
  •     if null == password || password.length <= 8 {         return { status : 2, message: 'Invalid password' }; 
  •     }   
  •     var repeat_password = $'#repeat_password'.val;     if repeat_password != password.length { 
  •         return { status : 3, message: 'Password and repeat password mismatch' };     } 
  •       var email = $'#email'.val; 
  •     if check_email_formatemail {         return { status : 4, message: 'Invalid email' }; 
  •     }   
  •     ...   
  •     return { status : 0, message: 'OK' };   
  • 这和刚才的二进制消息解析非常相似,上面的实现就是依照组建复用的思想封装了一下检测email格式之类的通用函数。没法在不同的表单之间进行大规模复用,很多细节都必需被重复编写。下面是用元语言笼统改进后的做法:

    var meta_create_user = { 
  •     form_id : 'create_user',     fields : [ 
  •         { id : 'name', type : 'text', min_length : 3 },         { id : 'password', type : 'password', min_length : 8 }, 
  •         { id : 'repeat-password', type : 'password', min_length : 8 },         { id : 'email', type : 'email' } 
  •     ] }; 
  •   var r = check_formmeta_create_us; 

    整个逻辑顿时清晰了细节的处置只需要在check_form中编写一次,过定义表单属性元语言。完全实现了简短、优雅、易理解、以维护”目 标。其实,不只Web表单验证可以通过元语言描述,整个Web页面从布局到功能全部都可以通过一个元对象描述,完全将逻辑和控制解耦。此外,编写的用于 解析命令行参数的lineparser.j库也是基于元语言的有兴趣的朋友可以参考并对比它和其他命令行解析库的设计差别。

    再来从代码长度的角度来分析一下元驱动编程和普通方法之间的差别。假设一个功能在系统中出现了n次,最后。对于普通方法来讲,由于逻辑和控制的耦合,代码量是n*L+C而元驱动编程只需要实现一次控制,代码长度是C+n*L其中L表示逻辑相关的代码量,C表示控制相关的代码量。通常情况下L局部都是一些配置,不容易引入bug复杂的主要是C局部,普通方法中C被重复 n次,引入bug可能性大大增加,同时修改一个bug也可能要改n个地方。所以,对于重复呈现的功能,元驱动编程大大减少了代码量,减小了引入bug可能,并且提高了可维护性。

    总结

  • 上一篇:叶凡网络:外交专家:安倍想重温“甲午旧梦”是白日梦 下一篇:叶凡网络:消息称遵义落马书记廖少华下属曾行贿刘铁男