软件中的质量属性(二)

现在我们接着上一次的话题来看看其它的质量属性。
互操作性 (Interoperability
互操作性指的是系统内或者系统之间不同的组件可以有效地进行信息交换,通常是以服务(Service)的形式来进行的。互操作性的关键因素包括通信协议,接口定义,数据格式的定义等等,而标准化是实现互操作性的重要手段。
实现互操作性的主要挑战有以下这些方面:
  • 系统内部或者和已有的旧系统(legacy system)之间的数据定义不一致
  • 系统的边界模糊,模块之间耦合严重,导致数据冗余
  • 缺乏标准,或者各方对标准的实现和认识不一致
我现在所在的商务智能团队的总架构师(Chief Architect)就一直在部门间推动对数据文档统一格式标准的定义和实现。这本身对于我们产品内部的互操作性是非常有必要的,然而BI的团队分布在各个大洲(主要是德国,法国,加拿大,中国和印度),每个部门对各自产品优先级的认识不一致,在加上对旧系统兼容性的要求,这项工作的进展非常非常的缓慢。BI的各个产品仍然很难互操作。
我之前开发过通信网管,当时做的产品是统一网管平台,就是把各个厂商(华为,中兴,朗讯等等)的电信设备统一的管理起来。当时已经有了相当成熟的电信网管理标准(TMF,ISO等标准)和技术标准(Q3,Corba)。然而理解的不同,厂商对标准的实现千奇百怪,所以实际上需要给每一个厂商定制不同的接口适配器。我当时就在负责一些这样的接口开发。
面向服务的架构(SOA)曾经是一个非常热门的词汇,现在似乎不怎么提起了。我司当年曾经大张旗鼓的宣传SOA。其实这样的架构能够解决的一个主要的问题就在能够把企业内部各种已有系统通过暴露标准服务接口的方式有效的协调起来。
为了实现互操作性,我们一般需要
  • 定义标准的数据格式和语义
  • 定义标准的服务接口,并使用基于服务的架构
  • 设计高内聚,低耦合的模块以获得最大的灵活性可重用性
可维护性 (Maintainability
可维护性有两个不同的角度,一个是指从软件用户和运维人员的角度,另一个是从软件开发人员的角度。
从用户和运维人员的角度,软件的可维护性是指软件是不是容易安装,升级,打补丁,有了问题是不是容易修复,能不能很容易的获得支持。
从开发人员的角度,软件的可维护性是指软件的架构是不是清楚简单,代码是不是容易阅读,有了问题是不是容易定位错误的原因,有没有可以提供帮助的文档,等等。
软件系统可维护性的主要问题有:
  • 模块之间紧耦合导致无法或很难对单独的模块进行修改,替换和升级
  • 在高层直接使用底层协议和接口,导致无法替换物理设备实体
  • 没有有效的分层和责任的划分,导致一个肿大的模块以及巨大的代码出现在同一个文件甚至函数中。
  • 没有帮助和设计文档
  • 为了兼容旧系统而不得不同时存在两个以上的复杂的代码栈甚至技术不同的实现
知道了问题,改进的建议是非常明显的,我要讨论的是一个关于代码可读性的有趣话题。
很多人都认为,代码是写给人看的,它碰巧也能被计算机读懂。所以我们应该像写文学作品那样写代码。碰巧我也非常的赞同这样的观点。然而,最近的一篇文章提到另一种观点,代码不是文学作品,我们不是阅读而是评审。这篇文章也许能够帮助你改善代码评审。不管代码是不是文学作品,写出容易阅读的代码对于提高软件的可维护性的好处是不言而喻的。

性能(Performance
性能也许是软件开发中最被重视的质量属性,也是最特殊的一个,从它的英文名字中不以(-ility)为后缀,我们可以看到他的特殊性。我们通常以系统执行某操作所需要的响应时间(latency)或者在某单位时间所能完成的任务的数量(throughput)来定义性能指标。
性能和其它的质量属性的相关性很高,有一些会对性能产生正面影响,有一些则是负面的。
记得我当年参加一个关于C++性能优化的培训,有一道算法,要求大家试着用最快的方式实现。因为C++中的指针操作按数组索引访问要快,于是当时最快的一个解决方案是用了一大推复杂指针访问来实现算法。然而这样的代码很难维护,而且容易出错。所以为提高性能就牺牲了可维护性。
一般而言,计算机提供了许多的资源,包括CPU,内存,硬盘等等,提高性能的核心就是充分利用这些资源。要保证对资源的使用是正确和有效的。通常提高性能的考虑包括:

  • 利用缓存(空间换时间)
  • 设计高效的资源共享,多线程,多进程,锁
  • 异步
  • 减少模块见得信息传递
  • 使接口设计传递最小所需的信息
  • 增加系统的可伸缩性,是系统能够有效的部署在分布式的资源上
另外我们还需要考虑另一个性能,就是程序员实现功能的性能。随着软件的发展,现在的程序员可以更高效的实现功能需求。一方面编程语言和方法在不断进步,另一方面大量的可重复使用的组件,服务,开源的库使得想在实现同样的功能的时间和需要的开发人员的数量比以前极大的缩小了。whatsapp以区区55人的团队开发出价值190亿美元,拥有4.5亿用户的软件产品,这在以前是难以想象的。所以软件架构设计者应该把软件的开发效率看成是更重要的性能指标。

可重用性 (Reusability
老板一般都非常重视可重用性,因为如果把软件代码看成产品,那么如果该产品如果只能用一次就扔掉,那他显然是很不开心的,因为这是一笔很糟糕的投入。在我的软件开发生涯这么多年以来,我体验的软件的可重用性都不是很高,也许是我大多在大型的软件企业服务有关。大企业的特点就是团队非常多,产品非常丰富,老板经常更换。每一个新的老板上台后,面临一大堆功能技术各异的系统都非常的不happy,于是整合在所难免,如何重用已有的系统来实现一个新的,大一统的新产品成了重中之重。然而这并不比找到宇宙中的终极大一统理论更简单。最终的整合结果往往并不能有效的重用已有的系统,当老板因为各种原因离开时,我们会发现,对新的老板来说,整合的任务变得更加艰巨了,因为,又有一个新的系统需要整合了。
提高可重用性的一个最主要的原则就是避免重复,“Don't repeat yourself!”我想大家应该非常熟悉了,这里就不多说了。
在成熟度不高的开发团队中(很不幸,我们大多数人都处在这样的团队中),对代码的重用很难,其实只有人才是最可靠的可重用组件,人离开了,所有的可重用性也就跟着离开了。

伸缩性(Scalability)
伸缩性要求软件系统能够跟着所需处理的工作量相应的伸缩。例如如果计算机是多CPU多核心的,软件是否能够相应的利用到这些计算资源。另一个方面就是软件是不是能够部署到分布式的网络,有效的利用网络中的每一个节点的资源。
有两个方向的伸缩,垂直和水平。
在垂直方向的伸缩(scale up)是指提高单节点的处理能力,比如提高CPU主频和内核数,增大内存,增大磁盘容量等等。SAP的HANA就是一个典型的垂直方向的伸缩。
在水平方向的伸缩(scale out)通常是指通过并发和分布的方式来增加节点以提高处理能力。Hadoop就是一个很好地水平伸缩的例子。
设计高伸缩性的软件时,我们可以考虑
  • 避免有状态的服务或组件
  • 使用配置文件决定组件的的部署和关系
  • 考虑支持数据的分区
  • 避免统一层的责任跨越不同的物理实体
在云时代,软件的伸缩性越发重要。

可测试性(Testability)
可测试性顾名思义就是指软件是否容易测试。
什么样的软件是不可测试的呢?举个例子来说,我们曾经开发过一个数据可视化的组件,就是把数据以图表的形式展现出来。有一种数据可视化的类型使用力导向的算法(force-directed)把数据以网络拓扑图的形式展现出来,该算法使用一些随机的参数来模拟节点的初始位置,并通过迭代计算生成最终layout的结果。也就是说每次的layout结果都是不一样的。测试团队对这样的算法很不满意,他们认为这样的实现是无法测试的,因为当时我们的测试主要以比对图形为基础,也就是生成一个正确的图形为基准,每次测试都会把输出的图形和基准图形进行比较,如果不一致则认为出错或者要修改基准。随机算法虽然从功能上讲并没有错误,但是在这样的测试方法下是无法满足可测试性的要求的。最后,开发团队修改了算法,使得每次的初始位置未固定位置来解决这个问题。
David Catlett提出了一个SOCK模型可以有效地帮助我们了解可测试性的要素
  • Simple
    代码越简单越容易测试。
  • Observable
    软件系统应该是可观测,无法观测也就无法衡量
  • Control
    软件系统应该是可以控制的,尽可能多的把控制权暴露给测试模块。
  • Knowledge
    测试人员或者模块需要更多地理解被测试模块,理解的越多也就越容易测试(白盒测试)
软件质量属性的每一个方面都有很多的内容,我们只能浅尝而止,而且仍然有许多重要的质量属性我们还没有涉及到,有时间的话,我们以后再说。

发帖者Unknown 时间: 23:28 0 评论  

软件中的质量属性(一)

开发高质量的软件是一件极具挑战的工作。其中一个重要的原因就是对于“质量”的定义各不相同,变化莫测。
杰拉尔德温伯格在他的四部曲巨作《质量软件管理》的第一卷第一章中就谈到了什么是质量以及质量的重要性。温伯格在书中讲了一个很有趣的故事。某软件企业每年都会根据所开发软件的质量对开发团队进行奖励,质量好的团队将会获得相应的嘉奖。可是如何评价软件的质量是一个令人头疼问题,于是他们采用了量化指标,根据用户反馈的defect的数量来定。某款软件质量超群,自推出至市场以来,只收到了一个defect。于是开发相应软件的团队获得了年度质量大奖!可是这款软件真的是一款高质量的软件么?看看这个defect的内容吧:“该软件无法安装!”
为了了解软件的质量是否满足要求,我们必须定义软件的质量属性(Quality Attributes)。同时质量属性也是影响软件架构的重要因素。软件架构主要由需求决定,需求有功能性的和非功能性的,其中非功能性的需求主要就是指质量属性,即各种“性”(“-ilities“)。wikipedia上列出了大概80种不同的质量属性,下面我们讨论一下其中最常见的属性。
可用性(Availability
可用性是指系统正常工作的时间所占的比例。可用性会遇到系统错误,恶意攻击,高负载等问题的影响。
当年在一家存储公司开发管理软件,"HA"(High Availability)是一个经常被提及的性能属性。"DU"(Data Unavailable)和"DL"(Data Lost)都是非常严重的可用性问题。
可用性面临的主要问题有:

  • 物理层失效:比如数据库服务器宕机,停电, 网络欠费被中国电信断网
  • 恶意攻击:例如DOS(Deny of Service)攻击
  • 软件的设计问题或BUG:比如错误的资源控制锁导致某个资源长期被占用,各种core dump, out of memery, out of stack。
  • 升级或日常维护
针对这些问题,为了增加可用性,需要考虑
  • 如何设计故障转移(failover),一般可以使用冗余来消除系统中的单点故障。可以是各种冷热备份,分布式系统。
  • 如何设计在线升级。我当年在那家存储公司的一个主要责任就是做在线升级,因为存储设备有两个同时工作的单元构成,所以升级的过程简单说就是先升级第一个单元,然后再升级第二个单元。听上去非常简单,然而实际的升级过程非常复杂,在升级之前,会做非常多的健康检查,比如检查两个单元是不是都在正常工作,有没有坏的磁盘,存储设备的版本是不是满足要求,等等等等。特别是由于存储设备的软硬件型号复杂,还要考虑各种不同的运行环境,各种软件BUG,健康检查非常非常的复杂。当然大多数情况下,用户可以跳过健康检查,然而由此引发的升级失败,后果自负!
  • 如何设计异常处理。异常处理是一个很大的话题,为了支持高可用性,我们应该如何处理异常呢?举个例子,一个网站要处理用户的订单,然而由于数据库服务器的故障,虽然前端的服务一切正常,可是订单无法处理。你会怎么处理这个数据库服务器异常的情况呢?大多数的程序员会在捕获异常的时候写日志,把异常状记录下来,然后在客户端的UI上显示一段谁也看不懂的异常代码。这样的异常处理其实跟没做差不多,甚至更糟。那么把异常代码翻译成用户可以看懂的语言是不是会好一点呢?也许会,如果你告诉用户因为你的数据库故障,请明天再来,可想而知用户会多么失望!好的异常处理是把用户的订单请求记录下来,发给人工处理,或者等待数据库恢复后自动处理,并告诉用户订单已经处理,并有可能迟延,请求得到用户的谅解。
  • 如何应对不稳定的网络连接。
灵活性 (Flexibility
灵活性是指系统是否能够很容易的适应环境和需求的变化。
例如现在需求是返回10以内的所有质数。我们可以使用以下程序:
function prime(){
    var result = [2,3,5,7];
    return result;
}

这段程序非常好,性能也非常高。然而非常的不具备灵活性,通过对需求的分析我们似乎可以大胆的预见10是一个非常有可能会改变的需求,于是提高灵活性的方式就是把10变成可变参数:
function prime(range){
    var result = [];
    var i,k;
    for(i=2; i<=range; i++){
      result.push(i);
    }
    for(i=0; i<result.length; i++){
      for(k=i+1; k<result.length; k++){
        if(result[k]%result[i]==0){
          result.splice(k,1);
        }
      }
    }
    return result;
}
我们看到第二段代码为了增加灵活性,代码变的更复杂,运行时间变长。当然第一段代码中的质数根本就没有经过计算验证,完全是我自己计算出来的,因为10以内的质数这样简单的运算根本不需要计算机。
在软件开发中有哪些问题会引起灵活性下降呢?
  • 由于各种原因而造成的数量惊人的代码
  • 过于复杂的代码
  • 不断重复的代码
糟糕的软件中这三点往往是一起出现的。
软件系统通常可以通过以分层,组件化等方式来提高灵活性。我在实践中的原则是”用简单构造复杂“。软件系统本身是非常复杂的,然而构建软件系统的基本单元却应该是非常简单的。例如计算机的基本组成单元是门电路,每一个门电路都非常简单,然而计算机系统却是如此的复杂和灵活。凯文凯利在他的《失控》一书中,也有同样的观点。
构建灵活软件系统的关键在于找到那个简单单元的边界,每一个单元应该足够的简单,但是不能够过于简单,爱因斯坦说过“Simple,but not Simpler!”
概念一致性 (Conceptual Integrity
我们很少在软件设计的时候谈论概念一致性,也许我们认为概念一致是一个共同的假定,然而实际上,在软件开发的过程中,往往会出现很多概念不一致的情况。
概念一致性的问题主要表现在以下的方面:  
  • 在一个模块的设计中混杂不同的问题域
  • 不同的组织或者团队负责系统中的同一个功能
  • 没有统一的代码规范为了满足后向兼容,系统中存在新旧两套不同的代码栈
我们可以看出,这些问题大多是管理的问题。我个人认为,为了实现概念一致性,在软件的设计过程中应该尽可能少的引入新的概念。软件设计的过程是一个抽象的过程,我们把复杂的软件系统抽象为一个个的层,问题域,过程,模块,服务,接口,这些都是非常必要的。但是这些东西都应该是越少越好,能在一两层解决的问题,绝不要划分成四五层,能提供一个API接口,绝不要给三个。人类能够同时掌握的概念是有限的,大部分人可能也就三四个把,当你设计的系统中有五六个或者七八个陌生的概念需要同时掌握的时候,对你的团队中其它需要使用你的设计的开发人员来说绝对是一个巨大的挑战。大部分人不会费力气去搞懂你设计的高大上的新概念。他们很有可能会设计出一套对他们自己更容易理解的并行的方案来解决同样的问题。
在以后我们将接着讨论软件中其它主要的质量属性。
当我们设计软件的时候,需要定义哪些质量属性是我们希望实现的,切记,质量属性并非越多越好。一般来说找到最重要的三个来构建软件就好了,而且"鱼和熊掌不能得兼",各个质量属性之间有可能是互相矛盾或者互相影响的。分布式数据库中的CAP理论就是一个典型的例子。对于一个分布式的计算系统可不能同时满足一致性(Consistency),可用性(Availability),分区容忍性(Partition Tolerance)。
下面分享一篇我以前做的关于软件质量属性的ppt。

发帖者Unknown 时间: 00:11 0 评论  

程序员量子力学-海森堡式BUG

今天在阅读《The Pragmatic Programmer》的时候发现书中提到了Heisenbug让我想起来了多年以前在开发中碰到的一个海森堡式的BUG。
The Pragmatic Programmer
海森堡是德国著名的物理学家,量子力学的创始人之一,“哥本哈根学派”的代表人物。

如果大家对这段历史或者物理原理不清楚的话,推荐大家阅读《上帝掷骰子吗》,非常好看的一本关于量子力学历史的科普书。
上帝掷骰子吗
海森堡对量子力学的一个重要贡献是提出了著名的“不确定性原理”(又称“海森堡测不准原理”),在一个量子力学系统中,一个运动粒子的位置和它的动量不可被同时确定。这是因为要观察就必须用光击中被观察的粒子,通过反射的光波来确定粒子的位置。然而观测所发出的光必然会影响被观测的粒子。
海森堡式的BUG是该物理原理在软件开发中的一个表现,当然这是一个类比,它和量子力学没有半毛钱的关系。举个例子吧,当程序出现了一个bug,程序猿为了找到bug出现的原因,在出现bug的代码处加入了一条打印语句,想要了解在出现bug的时候本地变量和参数的值以便确定bug出现的原因。然而奇迹出现了,当加入打印语句后,bug消失了。哦,买糕的,这条打印语句不就是射向粒子的那束光么?
下面,我来给大家分享一下我遇到的那个Heisenbug。
当时我们团队做一个商用软件从Windows平台到Linux平台的移植,因为该软件早期是以Mac为平台编写的(那是Mac还没有像现在这样风靡),所以我们的移植平没有遇到太大的困难,主要的工作是包括:

  • 用QT来实现所有的UI组件
  • 支持Linux下的数据库连接
  • 移植C++非标准的使用
  • Unicode的支持
项目的前期非常顺利,然而就是当项目接近尾声的时候,一个可怕的海森堡式bug出现了,产品的release版本会出现一些随机的错误,这些错误都很奇怪,并没有一个统一的表现。而这些错误在debug版本中完全不会出现。为了解决这个问题,在boss的率领下我们开始加班除虫。在当时公司加班还是很少见的,一方面欧洲公司以人为本,并不鼓励加班;另一方面我们团队,大家能力都很强,不需要加班来解决问题。然而这个bug属于必须要解决的问题,加班在所难免。
可是要解决问题这个问题还真是不容易,因为问题不会出现在debug版本中,所以用gdb加断点单步调试的方式根本没有用。所以调试的方式就只剩下了打日志。经过我一天不断的加入日志,运行测试,加入日志,运行测试…… 最终终于找到了问题的原因。一般情况下,找到问题的原因比解决问题要困难的多,当你发现问题的原因后,解决的方法就像秃子头上的虱子。
原来,这个问题是由于我们生成软件版本的方法所造成的。
为了在build的时候自动的生成软件的版本,我们的天才工程师想了一个好主意,那就是把版本信息写入Linux的可执行文件中(大家可以参考ELF的规范)。我们的软件可以从文件中读出这个版本,让后通过API告诉任何组件或者使用者当前的软件版本是什么。不得不承认这是一个非常有创意的好主意,但是这也是引起错误的主要原因。由于疏忽,在写入版本信息的时候,并没有严格按照规范来写,所以其实版本信息超出了本应写入的位置,也就是说写越界了,破坏了程序数据,所以造成了release版本中的错误。可是为什么debug版本没有问题呢?原因大概是debug版本由于加入了大量的代码和调试信息,版本信息很可能只是覆盖了调试信息而没有影响程序的正常运行。
这么多年过去了,很多细节我已经记不清楚啦,然而对于这个海森堡式的bug,现在想想,我们应该可以做的更好:
  • 设计评审
    我们鼓励创新,当引入一个新的解决方案,尤其是别人很少或根本没有使用过得方案时,我们必须非常小心。因为没有人碰到同样的问题。认真的设计评审或许可以有所帮助。
  • 代码审查
    如果当时我们能够认真的进行代码审查,也许我们能发现这个问题。但也许即使进行代码审查,也不一定能发现这个错误。团队中没有几个人能够搞懂ELF的规范
  • 单元测试
    单元测试能帮助发现这个问题么?也许可以,当时我们并没有就这个功能引入任何的单元测试。
最后引用一个我发现的软件开发中的测不准原理:软件的需求和完成时间不可能同时测准,如果你清楚需求是什么,那么你就不知道什么时候能完成;如果你知道什么时候要做完,那你根本就不知道需求是什么。

发帖者Unknown 时间: 18:59 0 评论  

用编程的思路模拟解决脑筋急转弯问题

前些日子看了可汗学院的这个关于诚实人和说谎者的脑筋急转弯问题,我觉得如果能用程序来模拟,那一定很有趣。

这个题目是这样的,有两扇门,一扇通往天堂,一扇通往地狱,你要做出选择打开那扇门。门口各有一个人,他们都知道门后面的情况,其中一个总是说实话,而另一个总是撒谎。你可以问他们问题。要怎样问问题,才能做出正确的选择?
在揭开答案之前,我们先看看我是如何用JavaScript来模拟这个问题的。

function Game() {
  var destinations = ["Heaven", "Hell"];
  var gates = {}, persons = {};

  function Gate() {
    var val = undefined;
    return {
      value: function(_) {
        if (!arguments.length) {
          return val;
        }
        val = _;
      },
      open: function() {
        if (val === destinations[0]) {
          console.log("Happy Ending!");
        } else {
          console.log("You Die!");
        }
      }
    }
  }

  function Behavior(isLier) {
    var tellTruth = function(info) {
      return info;
    };
    var tellLie = function(info) {
      var index = destinations.indexOf(info);
      if (index === 0) {
        return destinations[1];
      } else {
        return destinations[0];
      }
    };
    return isLier ? tellLie : tellTruth;
  }

  function Person(gate, behavior) {
    return {
      ask: function() {
        return behavior.call(this, gate.value());
      }
    }
  }

  function Init() {
    gates.A = Gate();
    gates.B = Gate();

    if (Math.random() > 0.5) {
      gates.A.value(destinations[0]);
      gates.B.value(destinations[1]);
    } else {
      gates.A.value(destinations[1]);
      gates.B.value(destinations[0]);
    }

    if (Math.random() > 0.5) {
      persons.A = Person(gates.A, Behavior(true));
      persons.B = Person(gates.B, Behavior(false));
    } else {
      persons.A = Person(gates.A, Behavior(false));
      persons.B = Person(gates.B, Behavior(true));
    }
  }

  Init();

  return {
    open: function(gateName) {
      gates[gateName].open();
    },
    ask: function(personName) {
      return persons[personName].ask();
    }
  }
}

程序主要有以下方法:
  • Gate
    Gate方法负责创建们,每扇门提供两个方法, value方法负责读写们的内容,open方法检查开门后的结果。如果是天堂,则打印“Happy Ending”, 游戏成功;如果是地狱则打印“You Die”,游戏失败。
  • Behavior
    Behavior是对说谎和说实话这两种行为的抽象。返回两个不同的方法,说谎的行为总是返回和原本信息不一致的信息,而说实话则返回原本的信息。
  • Person
    Person方法负责创建人,并关联人所在的门,指定人的行为。
  • Init
    Init方法随机初始化门和人
  • Game
    Game方法是最外层的闭包,他暴露出游戏者可以调用的两个方法,问问题和开门。因为Person和Gate都在闭包里所以游戏者无法获得任何相关的信息,只能通过ask方法来问问题。
调用以下方法来运行游戏:
var g = Game();
console.log(g.ask("A"));
console.log(g.ask("B"));

因为A和B有一个人说谎,所以条用的结果是他们都说自己后面的门是“天堂”,或者都说是“地狱”。
于是无论打开A还是B,游戏者都只有50%的概率获胜。
好了,现在是揭晓答案的时候了,这道题,游戏者需要这样问,“如果问另外一个人你后面的门后是什么,他会怎么说?”会有两种情况:
  • 问诚实人说谎者怎么回答他后面的门是什么,因为说谎者会说谎话,而诚实的人会原封不动的返回这个回答,所以提问者会得到错误的答案
  • 问说谎者诚实人怎么回答他后面的门是什么,因为说谎者会说谎话,他会把诚实人的正确答案变成谎话回答,所以答案还是错误的。
那么我们要怎样修改我们的程序呢?
其实改动很简单,首先要增强Person方法
function Person(gate, behavior) {
    return {
      ask: function(aGate) {
        if(!aGate) {
          return behavior.call(this, gate.value());
        } else {
          return behavior.call(this, aGate.value());
        }
      },
      askBy: function(accordingTo) {
        return behavior.call(this, accordingTo.ask(gate));
      }
    }
  }

在Person方法中增强ask方法,暴露门的信息,也就是说每个人不但知道自己的门后的东西是什么,也能回答另一扇门后是什么。所以通过参数传递门的引用。这里我们可以看出,在前一版中假定人和门之间的耦合关系是不成立的,其实完全应该可以帮人和门解耦。
另外增加了askBy方法,这个方法会去问另一个人,我给你的门后面的信息,然后用自己的行为返回出去。
除了扩展Person,我们还要扩展Game返回的对象,增加新的提问方式:
  return {
    open: function(gateName) {
      gates[gateName].open();
    },
    ask: function(personName) {
      return persons[personName].ask();
    },
    askBy : function(personName, byName) {
      return persons[personName].askBy(persons[byName]);
    }
  }

这样,新暴露的askBy方法就可以正确的找到错误的门了(听上去很绕口)
增强后的代码如下:
function Game() {
  var destinations = ["Heaven", "Hell"];
  var gates = {}, persons = {};

  function Gate() {
    var val = undefined;
    return {
      value: function(_) {
        if (!arguments.length) {
          return val;
        }
        val = _;
      },
      open: function() {
        if (val === destinations[0]) {
          console.log("Happy Ending!");
        } else {
          console.log("You Die!");
        }
      }
    }
  }

  function Behavior(isLier) {
    var tellTruth = function(info) {
      return info;
    };
    var tellLie = function(info) {
      var index = destinations.indexOf(info);
      if (index === 0) {
        return destinations[1];
      } else {
        return destinations[0];
      }
    };
    return isLier ? tellLie : tellTruth;
  }

  function Person(gate, behavior) {
    return {
      ask: function(aGate) {
        if(!aGate) {
          return behavior.call(this, gate.value());
        } else {
          return behavior.call(this, aGate.value());
        }
      },
      askBy: function(accordingTo) {
        return behavior.call(this, accordingTo.ask(gate));
      }
    }
  }

  function Init() {
    gates.A = Gate();
    gates.B = Gate();

    if (Math.random() > 0.5) {
      gates.A.value(destinations[0]);
      gates.B.value(destinations[1]);
    } else {
      gates.A.value(destinations[1]);
      gates.B.value(destinations[0]);
    }

    if (Math.random() > 0.5) {
      persons.A = Person(gates.A, Behavior(true));
      persons.B = Person(gates.B, Behavior(false));
    } else {
      persons.A = Person(gates.A, Behavior(false));
      persons.B = Person(gates.B, Behavior(true));
    }
  }

  Init();

  return {
    open: function(gateName) {
      gates[gateName].open();
    },
    ask: function(personName) {
      return persons[personName].ask();
    },
    askBy : function(personName, byName) {
      return persons[personName].askBy(persons[byName]);
    }
  }
}

调用以下代码来问新的问题:
var g = Game();
console.log(g.askBy("A","B"));
console.log(g.askBy("B","A"));

游戏者只要打开和回答不一致的门就可以马上升入天堂!
通过这个例子,我们发现对Behavior的抽象对进一步问题的扩展起到了很大的帮助。这也展现了基于Function编程的灵活性,不难想象,如果我们把说谎者和诚实人按照面向对象的方式来构造,而不是把说谎还是说真话作为行为注入到一般的人上,对新的问题的扩展将会比较困难。

发帖者Unknown 时间: 18:50 0 评论  

用动态可视化来讲故事

在科学松鼠会的网站上有一篇漫画,来自imgur.com,讲述了 科学理论是如何建立的
我用到d3.js把这个静态的图改成了动态效果,见这里。 代码可见github

注:这个其实主要就是奥卡姆剃刀原理,“若无必要,勿增实体”。
附原图 :

发帖者Unknown 时间: 02:58 0 评论  

数据可视化中的视觉属性

Stephan Few 是数据可视化领域里面数一数二的专家,他的几本书《Show Me The Numbers》,《Information Dashboard Design》和《Now You See It》都是非常优秀的关于数据可视化的书。
Stephan Few 和 Tableau 的合作非常紧密,在 Tableau 的数据可视化的设计中,处处可见 Stephan Few 的思想的影子。Stephan Few 在他的博客中毫不掩饰的提到 Tableau 是他唯一欣赏和敬重的公司。
以下是我阅读《Now You See It》的读书笔记:

在书中提到了视觉属性在可视化中的作用,视觉属性包含了以下分类:

  • 形式
    • 长,宽, 方向, 大小, 形状, 弯曲度, 围绕, 模糊
  • 颜色
    • 色调, 强度
  • 空间位置
    • 2-D 位置, 空间分组
  • 动画
    • 方向
每一种属性在表达数据时的表现力度是不一样的,大家可以通过这个图(Design By Stephane Few)来体会一下,点这里看动态图(Implemented By JavaScript + D3.js)
把鼠标移动到你关注的视觉属性上看效果。


发帖者Unknown 时间: 02:56 0 评论