blogjava-凯发k8网页登录http://www.blogjava.net/killme2008/category/19797.html生活、程序、未来zh-cntue, 22 may 2012 20:09:46 gmttue, 22 may 2012 20:09:46 gmt60如何熟悉一个开源项目?http://www.blogjava.net/killme2008/archive/2012/05/22/378885.htmldennisdennistue, 22 may 2012 15:12:00 gmthttp://www.blogjava.net/killme2008/archive/2012/05/22/378885.htmlhttp://www.blogjava.net/killme2008/comments/378885.htmlhttp://www.blogjava.net/killme2008/archive/2012/05/22/378885.html#feedback0http://www.blogjava.net/killme2008/comments/commentrss/378885.htmlhttp://www.blogjava.net/killme2008/services/trackbacks/378885.html    你有个任务,需要用到某个开源项目;或者老大交代你一个事情,让你去了解某个东西。怎么下手呢?如何开始呢?我的习惯是这样:

1.首先,查找和阅读该项目的博客和资料,通过google你能找到某个项目大体介绍的博客,快速阅读一下就能对项目的目的、功能、基本使用有个大概的了解。

2.阅读项目的文档,重点关注类似getting started、example之类的文档,从中学习如何下载、安装、甚至基本使用该项目所需要的知识。

3.如果该项目有提供现成的example工程,首先尝试按照开始文档的介绍运行example,如果运行顺利,那么恭喜你顺利开了个好头;如果遇到问题,首先尝试在项目的faq等文档里查找答案,再次,可以将问题(例如异常信息)当成关键词去搜索,查找相关的解决办法,你遇到了,别人一般也会遇到,热心的朋友会记录下解决的过程;最后,可以将问题提交到项目的邮件列表,请大家帮你看看。在没有成功运行example之前,不要尝试修改example。

4.运行了第一个example之后,尝试根据你的理解和需要修改example,测试高级功能等。

5.在了解基本使用后,需要开始深入的了解该项目。例如项目的配置管理、高级功能以及最佳实践。通常一个运作良好的项目会提供一份从浅到深的用户指南,你并不需要从头到尾阅读这份指南,根据时间和兴趣,特别是你自己任务的需要,重点阅读部分章节并做笔记(推荐evernote)。

6.如果时间允许,尝试从源码构建该项目。通常开源项目都会提供一份构建指南,指导你如何搭建一个用于开发、调试和构建的环境。尝试构建一个版本。

7.如果时间允许并且有兴趣,可以尝试阅读源码:
(1)阅读源码之前,查看该项目是否提供架构和设计文档,阅读这些文档可以了解该项目的大体设计和结构,读源码的时候不会无从下手。
(2)阅读源码之前,一定要能构建并运行该项目,有个直观感受。
(3)阅读源码的第一步是抓主干,尝试理清一次正常运行的代码调用路径,这可以通过debug来观察运行时的变量和行为。修改源码加入日志和打印可以帮助你更好的理解源码。
(4)适当画图来帮助你理解源码,在理清主干后,可以将整个流程画成一张流程图或者标准的uml图,帮助记忆和下一步的阅读。
(5)挑选感兴趣的“枝干”代码来阅读,比如你对网络通讯感兴趣,就阅读网络层的代码,深入到实现细节,如它用了什么库,采用了什么设计模式,为什么这样做等。如果可以,debug细节代码。
(6)阅读源码的时候,重视单元测试,尝试去运行单元测试,基本上一个好的单元测试会将该代码的功能和边界描述清楚。
(7)在熟悉源码后,发现有可以改进的地方,有精力、有意愿可以向该项目的开发者提出改进的意见或者issue,甚至帮他修复和实现,参与该项目的发展。

8.通常在阅读文档和源码之后,你能对该项目有比较深入的了解了,但是该项目所在领域,你可能还想搜索相关的项目和资料,看看有没有其他的更好的项目或者凯发天生赢家一触即发官网的解决方案。在广度和深度之间权衡。

    以上是我个人的一些习惯,我自己也并没有完全按照这个来,但是按照这个顺序,基本上能让你比较高效地学习和使用某个开源项目。

dennis 2012-05-22 23:12 发表评论
]]>
写dsl的步骤http://www.blogjava.net/killme2008/archive/2011/07/25/355010.htmldennisdennismon, 25 jul 2011 11:30:00 gmthttp://www.blogjava.net/killme2008/archive/2011/07/25/355010.htmlhttp://www.blogjava.net/killme2008/comments/355010.htmlhttp://www.blogjava.net/killme2008/archive/2011/07/25/355010.html#feedback1http://www.blogjava.net/killme2008/comments/commentrss/355010.htmlhttp://www.blogjava.net/killme2008/services/trackbacks/355010.html1.选定宿主语言,最好选用元编程能力强悍的语言作为宿主语言。
2.确定dsl的样子,让脑袋空白,不去考虑任何实现问题,纯粹思考你想要实现的dsl是什么样子
3.用你想要的dsl写一个最基本的例子,只包括最基本的功能
4.开始实现dsl,尽快让你的dsl例子以dirty and quick的方式跑起来。
5.写更多dsl的例子,慢慢包括你想要的所有功能,并一一实现,在这个过程中你可能改变dsl的样子,原来模糊的东西渐渐清楚。
6.大功告成,review你的代码并添加自动化测试,将代码中dirty和bad smell的部分一一剔除。
7.让你的dsl接受实际应用的考验吧。


dennis 2011-07-25 19:30
]]>
做基础产品的体会http://www.blogjava.net/killme2008/archive/2011/05/22/350752.htmldennisdennissun, 22 may 2011 02:30:00 gmthttp://www.blogjava.net/killme2008/archive/2011/05/22/350752.htmlhttp://www.blogjava.net/killme2008/comments/350752.htmlhttp://www.blogjava.net/killme2008/archive/2011/05/22/350752.html#feedback6http://www.blogjava.net/killme2008/comments/commentrss/350752.htmlhttp://www.blogjava.net/killme2008/services/trackbacks/350752.html      一个公司大了,总有部分人要去做一些通用的东西给大家用,我这里说的基础产品就是这类通用性质的东西,不一定高科技,但是一定很多人依赖你的东西来完成各种各样的功能。做这样的东西,有些体会可以说下。

    首先,能集中存储的,就不要分布存储,数据集中存储有单点的危险,但是比之分布式存储带来的复杂度不可同日而语。况且集中式的存储也可以利用各种机制做备份,所谓单点风险远没有想象中那么大。

   其次,能利用开源框架的,就不要重复造轮子。程序员都喜欢造轮子,但是造轮子的周期长,并且不一定造的更好。在强调开发效率的互联网时代,如果能直接利用现有框架组装出你想要的东西,迅速占领市场,比你造的高性能、高可用、高科技的轮子更实用。这个跟做新产品开发有点类似,迅速组装,高效开发,然后再想办法改进。

   第三,要文本,不要二进制。协议要文本化,配置要文本化。不要担心性能,在可见的时间里,你基本不会因为文本化的问题遇到性能瓶颈。

   第四,要透明,不要黑盒。基础产品尤其需要对用户透明,你的用户不是小白用户,他们也是程序员,而程序员天生对黑盒性质的东西充满厌恶,他们总想知道你的东西背后在做什么,这对于查找问题分析问题也很重要。怎么做到透明呢?设计,统计,监控,日志等等。

   第五,要拥抱标准,不要另搞一套。已经有了久经考验的http协议,你就不要再搞个sttp,有了amqp协议,你就不要再搞个bmqp。被广泛认可的标准是一些业界的顶尖专家制定出来的,他们早就将你没有考虑到的问题都考虑进去了。你自己搞的那一套,随着时间推移你会发现跟业界标准越来越像,因为面对的问题是一样的。使用标准的额外好处是,你有一大堆可用的代码或者类库可以直接使用,特别是在面对跨语言的时候。

    第六,能share nothing,就不要搞状态复制。无状态的东西是最可爱的,天然的无副作用。水平扩展不要太容易。

    第七,要将你的系统做的越不“重要”越好,如果太多的产品依赖你的系统,那么当你的系统故障的时候,整个应用就完蛋了。我们不要担这个责任,我们要将系统做的越来越“不重要”,别人万一没了你也能重启,也能一定时间内支撑正常的工作。

    第八,要专注眼前,适当关注未来。有远见是好事,但是太多远见就容易好高骛远。为很小可能性设计的东西,没有机会经历实际检验,当故障真的发生的时候,你也不可能完全信赖它。更好的办法是将系统设计得可介入,可在紧急情况下人工去介入处理,可介入是不够的,还要容易介入。

    第九,不要对用户有假设,假设你的用户都是smart programmer,假设你的用户不需要位运算,假设你的用户要同步不要异步。除非你对这个领域非常熟悉并实际使用过类似的东西,否则还是不要假设。

    第十,咳咳,似乎没有第十了,一大早憋了这么篇无头无脑的blog,大伙将就看看。







dennis 2011-05-22 10:30
]]>
push or pull?http://www.blogjava.net/killme2008/archive/2011/04/30/349303.htmldennisdennisfri, 29 apr 2011 17:06:00 gmthttp://www.blogjava.net/killme2008/archive/2011/04/30/349303.htmlhttp://www.blogjava.net/killme2008/comments/349303.htmlhttp://www.blogjava.net/killme2008/archive/2011/04/30/349303.html#feedback1http://www.blogjava.net/killme2008/comments/commentrss/349303.htmlhttp://www.blogjava.net/killme2008/services/trackbacks/349303.html  
push模型 pull模型
描述 服务端主动发送数据给客户端 客户端主动从服务端拉取数据,通常客户端会定时拉取
实时性 较好,收到数据后可立即发送给客户端 一般,取决于pull的间隔时间
服务端状态 需要保存push状态,哪些客户端已经发送成功,哪些发送失败 服务端无状态
 客户端状态  无需额外保存状态 需保存当前拉取的信息的状态,以便在故障或者重启的时候恢复
状态保存 集中式,集中在服务端 分布式,分散在各个客户端
负载均衡 服务端统一处理和控制 客户端之间做分配,需要协调机制,如使用zookeeper
其他

服务端需要做流量控制,无法最大化客户端的处理能力。

其次,在客户端故障情况下,无效的push对服务端有一定负载。

客户端的请求可能很多无效或者没有数据可供传输,浪费带宽和服务器处理能力
缺点方案 服务器端的状态存储是个难点,可以将这些状态转移到db或者key-value存储,来减轻server压力。

针对实时性的问题,可以将push加入进来,push小数据的通知信息,让客户端再来主动pull。

针对无效请求的问题,可以设置逐渐延长间隔时间的策略,以及合理设计协议尽量缩小请求数据包来节省带宽。



在面对大量甚至海量客户端的时候,使用push模型,保存大量的状态信息是个沉重的负担,加上复制n份数据分发的压力,也会使得实时性这唯一的优点也被放小。使用pull模型,通过将客户端状态保存在客户端,大大减轻了服务器端压力,通过客户端自身做流量控制也更容易,更能发挥客户端的处理能力,但是需要面对如何在这些客户端之间做协调的难题。

dennis 2011-04-30 01:06
]]>
seda架构笔记http://www.blogjava.net/killme2008/archive/2010/06/20/324022.htmldennisdennissun, 20 jun 2010 15:53:00 gmthttp://www.blogjava.net/killme2008/archive/2010/06/20/324022.htmlhttp://www.blogjava.net/killme2008/comments/324022.htmlhttp://www.blogjava.net/killme2008/archive/2010/06/20/324022.html#feedback5http://www.blogjava.net/killme2008/comments/commentrss/324022.htmlhttp://www.blogjava.net/killme2008/services/trackbacks/324022.html
一、传统并发模型的缺点


基于线程的并发


特点:
每任务一线程
直线式的编程
使用资源昂高,
context切换代价高,竞争锁昂贵
太多线程可能导致吞吐量下降,响应时间暴涨。

基于事件的并发模型



特点:
单线程处理事件
每个并发流实现为一个有限状态机
应用直接控制并发
负载增加的时候,吞吐量饱和
响应时间线性增长


二、seda架构




特点:
(1)服务通过queue分解成stage:
   每个stage代表fsm的一个状态集合
   queue引入了控制边界
(2)使用线程池驱动stage的运行:
   将事件处理同线程的创建和调度分离
   stage可以顺序或者并行执行
   stage可能在内部阻塞,给阻塞的stage分配较少的线程

1、stage-可靠构建的基础



(1)应用逻辑封装到event handler
   接收到许多事件,处理这些事件,然后派发事件加入其他stage的queue
   对queue和threads没有直接控制
   event queue吸纳过量的负载,有限的线程池维持并发
(2)stage控制器
  负责资源的分配和调度
  控制派发给event handler的事件的数量和顺序
  event handler可能在内部丢弃、过滤、重排序事件。

2、应用=stage网络

   (1)有限队列
        入队可能失败,如果队列拒绝新项的话
        阻塞在满溢的队列上来实现吸纳压力
        通过丢弃事件来降低负载
   (2) 队列将stage的执行分解
        引入了显式的控制边界
        提供了隔离、模块化、独立的负载管理
   (3)方便调试和profile
        事件的投递可显
        时间流可跟踪
        通过监测queue的长度发现系统瓶颈

3、动态资源控制器

(1)、线程池管理器

目标: 决定stage合理的并发程度
操作:
观察queue长度,如果超过阀值就添加线程
移除空闲线程



(2)、批量管理器
目的:低响应时间和高吞吐量的调度
操作:
batching因子:stage一次处理的消息数量
小的batching因子:低响应时间
大的batching因子:高吞吐量

尝试找到具有稳定吞吐量的最小的batching因子
观察stage的事件流出率
当吞吐量高的时候降低batching因子,低的时候增加


三、小结
   seda主要还是为了解决传统并发模型的缺点,通过将服务器的处理划分各个stage,利用queue连接起来形成一个pipeline的处理链,并且在stage中利用控制器进行资源的调控。资源的调度依据运行时的状态监视的数据来进行,从而形成一种反应控制的机制,而stage的划分也简化了编程,并且通过queue和每个stage的线程池来分担高并发请求并保持吞吐量和响应时间的平衡。简单来说,我看中的是服务器模型的清晰划分以及反应控制。



  


dennis 2010-06-20 23:53
]]>
代码自我审查的一些体会http://www.blogjava.net/killme2008/archive/2010/05/18/321219.htmldennisdennismon, 17 may 2010 16:28:00 gmthttp://www.blogjava.net/killme2008/archive/2010/05/18/321219.htmlhttp://www.blogjava.net/killme2008/comments/321219.htmlhttp://www.blogjava.net/killme2008/archive/2010/05/18/321219.html#feedback4http://www.blogjava.net/killme2008/comments/commentrss/321219.htmlhttp://www.blogjava.net/killme2008/services/trackbacks/321219.html 1、首先态度需要端正,做代码的自我审查并不是否定自己,而是给自己将工作做得更好的一次机会。在审查过程中要尽量将自己作为一个旁观者的心态去审查自己的代码,尽管这比较困难。

2、代码审查离不开重构,在审查过程中发现任何坏味道都请使用重构去改善,发现缺乏测试的地方要及时补充测试,不要让bug遗漏。

3、代码的自我审查可能不是越早越好,隔一段时间之后回去看自己写的东西,对一些设计上的选择能有更客观的评价,在审查的过程中可能需要重新去理解代码,在此过程中可以检查自己代码的可读性,并思考如何改善可读性,切记代码首先是给人读的

4、审查过程中需要记录下一些犯下的错误,以及当时为什么会犯下这样的错误,建立自己的bug数据库,并时常review,在以后的工作中避免同样的错误。

5、代码的自我审查应该是一个持续性的过程,而非特定时间的特定行动,时常审查自己的代码,不仅能辨析自己的得失,还能够进一步提高自己在未来工作中的设计能力和预见能力。

6、代码的自我审查跟团队成员之间的相互review并不矛盾,在相互review之前做一个自我审查,有助于提高review的效率,包括可读性的提高和一些一般错误的避免。

7、代码自我审查的一些常见注意点:
(0)自认为绝不会出错,并且从来没有审查过的代码。
(1)注意else语句,if条件下的子语句通常可能是个正常的流程,而else意味着异常的情况或者特殊的场景,你可能特别注意怎么处理正常的情况,却忽略了else子句的实现细节,如该释放的锁没释放,该递减的计数没有递减,该赋予特殊值却没有赋予等等。
(2)注意空的方法,没有方法体的方法,是不需要实现?还是忘了实现?
(3)注意switch语句,有没有忘了break?这种错误通过findbugs之类的静态代码检查工具都能避免。
(4)注意大块的注释,为什么这么多注释?是代码写的很糟糕?还是遗留的注释?遗留的注释会误导人,要及时删除。
(5)注意一些看起来“不合常理”的代码,这样的代码很多都是基于所谓性能考虑而优化过的代码,这样的优化是否还需要?是否能去除这些“奇怪”的代码也能实现正常的需求?
(6)对客户端的使用有假设的代码,假设用户只会这么用,假设用户只会用到返回对象中的某些属性,其他属性一定不会用到?不要对客户代码做假设!这个客户代码包括外部用户和调用这个模块的内部代码。
(7)标注了fixme、todo之类task标签的代码,是否忘了修复bug,实现功能?
(8)任何超过15行以上的方法,这些方法是否能拆分成更细粒度的方法,并保持在同一个抽象层次上?
(9)任何在代码中出现的常量值,是否应该提取出来成为单独的常量,常量的默认值设置是否合理?
(10) 任何持有容器的代码,提供了放入容器的方法,是否提供了从容器中移除对象的方法?确保没有内存泄漏的隐患。
(11)重构中提到的其他坏味道,别放过它们,但是也不要追求完美,oo不是圣杯,如果能简单的实现一个算法,你不要引入3个对象和4个接口。
(12)在review最后能列出一张清单,开列下该项目面临的风险点,并提出解决办法,然后按照这张清单去review关键代码,着重检查异常情况下的处理。风险点的review,我建议可以放在后面,在一般性错误解决之后进行,此时你对代码也将再度变的熟悉。


dennis 2010-05-18 00:28
]]>
yanf4j设计的两张图片http://www.blogjava.net/killme2008/archive/2010/03/26/316637.htmldennisdennisfri, 26 mar 2010 06:19:00 gmthttp://www.blogjava.net/killme2008/archive/2010/03/26/316637.htmlhttp://www.blogjava.net/killme2008/comments/316637.htmlhttp://www.blogjava.net/killme2008/archive/2010/03/26/316637.html#feedback1http://www.blogjava.net/killme2008/comments/commentrss/316637.htmlhttp://www.blogjava.net/killme2008/services/trackbacks/316637.html
基本组件结构图——典型的reactor handler模式



一次io读请求的处理过程:







dennis 2010-03-26 14:19
]]>
《unix编程艺术》重读笔记(三)http://www.blogjava.net/killme2008/archive/2010/02/22/313571.htmldennisdennissun, 21 feb 2010 16:46:00 gmthttp://www.blogjava.net/killme2008/archive/2010/02/22/313571.htmlhttp://www.blogjava.net/killme2008/comments/313571.htmlhttp://www.blogjava.net/killme2008/archive/2010/02/22/313571.html#feedback1http://www.blogjava.net/killme2008/comments/commentrss/313571.htmlhttp://www.blogjava.net/killme2008/services/trackbacks/313571.html

blogjava-凯发k8网页登录

无论在协作进程还是在同一进程的协作子过程层面上,unix设计风格都运用“做单件事并做好的方法“,强调用定义良好的进程间通信或共享文件来连通小型进程,提倡将程序分解为更简单的子进程,并专注考虑这些子进程间的接口,这至少需要通过以下三种方法来实现:

1降低进程生成的开销(思考下process)

2、提供方法(shelloutio重定向、管道、消息传递和socket)简化进程间通信

3、提倡使用能由管道和socket传递的简单、透明的文本数据格式

unix ipc方法的分类:

1、将任务转给专门程序,如system(3)popen调用等,称为shellout

2pipe重定向和过滤器,如bcdc

3包装器,隐藏shell管线的复杂细节。

4安全包装器和bernstein

5/进程

6对等进程间通信:

(1)临时文件

(2)信号

(3)系统守护程序和常规信号

(4)socket

(5)共享内存,mmap

远程过程调用(rpc)的缺憾:

1rpc接口很难做到可显,如果不编写和被监控程序同样复杂的专用工具,也难以监控程序的行为。rpc接口和库一样具有版本不兼容的问题,由于是分布式的,因此更难被追查。

2、类型标记越丰富的接口往往越复杂,因而越脆弱。随着时间的推移,由于在接口之间传递的类型总量逐渐变大,单个类型越来越复杂,这些接口往往产生类型本体蠕变问题。这是因为接口比字符串更容易失配;如果两端程序的本体不能正确匹配,要让它们通信肯定很难,纠错更是难上加难。

3、支持rpc的常见理由是它比文本流方法允许“更丰富”的接口,但是接口的功能之一就是充当阻隔点,防止模块的实现细节彼此泄漏,因此,支持rpc的主要理由同时恰恰证明了rpc增加而不是降低了程序的全局复杂度

unix传统强烈赞成透明、可显的接口,这是unix文化不断坚持文本协议ipc的动力。

esr在这里还谈到xml-rpcsoap等协议,认为是rpcunix对文本流支持的一种融合,遗憾的是soap本身也成为一种重量级、不那么透明的协议了,尽管它也是文本协议。

线程是有害的:

线程是那些进程生成昂贵、ipc功能薄弱的操作系统所特有的概念。

尽管线程通常具有独立的局部变量栈,它们却共享同一个全局内存,在这个共享地址空间管理竞争临界区的任务相当困难,而且成为增加整体复杂度和滋生bug的温床。除了普通的竞争问题之外,还产生了一类新问题:时序依赖

当工具的作用不是控制而是增加复杂度的时候,最好扔掉从零开始。

(注,这里谈的微型语言,就是现在比较热门的词汇dsl

对软件错误模式进行的大量研究得出的一个最一致的结论是,程序员每百行程序出错率和所使用的编程语言在很大程度上是无关的。更高级的语言可以用更少的行数完成更多的任务,也意味着更少的bug

防御设计不良微型语言的唯一方法是知道如何设计一个好的微型语言。

语言分类法:


对微型语言的功能测试:不读手册可以编写吗

现代微型语言,要么就完全通用而不紧凑,要么就非常不通用而紧凑;不通用也不紧凑的语言则完全没有竞争力。

一些引申想法:我认为这个评判标准也可以用在任何编程语言上,以此来判断一些语言,c语言既通用又紧凑,java是通用而不紧凑,python之类的脚本语言也是如此,正则表达式(如果也算语言的话)是不通用而紧凑,也是通用而紧凑,awk却是既不通用也不紧凑,xslt也可以归入不通用不紧凑的行列;javascript是个另类,按理说它也是不通用不紧凑,说它不通用是因为它的主要应用范围还是局限在web开发的ui上,实际上javascript也是门通用语言,但是很少会有人会用javascript去写批处理脚本,javascript显然是不紧凑的,太多的边边角角甚至奇奇怪怪的东西需要你去注意,然而就是这样一门不通用不紧凑的语言现在却非常有前途,只能说时势所然。

设计微型语言:

1、选择正确的复杂度,要的是数据文件格式,还是微型语言?

2扩展嵌入语言

3、编写自定义语法yacclex

4慎用宏,宏的主要问题是滥用带来的奇怪、不透明的代码,以及对错误诊断的扰乱。

5语言还是应用协议





dennis 2010-02-22 00:46
]]>
《unix编程艺术》重读笔记(二)http://www.blogjava.net/killme2008/archive/2010/02/19/313419.htmldennisdennisfri, 19 feb 2010 11:25:00 gmthttp://www.blogjava.net/killme2008/archive/2010/02/19/313419.htmlhttp://www.blogjava.net/killme2008/comments/313419.htmlhttp://www.blogjava.net/killme2008/archive/2010/02/19/313419.html#feedback1http://www.blogjava.net/killme2008/comments/commentrss/313419.htmlhttp://www.blogjava.net/killme2008/services/trackbacks/313419.html

软件设计有两种方式:一种是设计得极为简洁没有看得到的缺陷;另一种是设计得极为复杂有缺陷也看不出来。第一种方式的难度要大得多。

模块化代码的首要特质就是封装api在模块间扮演双重角色,实现层面作为模块之间的滞塞点阻止各自的内部细节被相邻模块知晓;在设计层面,正是api真正定义了整个体系。

养成在编码前为api编写一段非正式书面描述的习惯,是一个非常好的方法。

模块的最佳大小,逻辑行200-400行,物理行在400-800之间

紧凑性就是一个设计能否装入人脑中的特性。测试软件紧凑性的一个简单方法是:一个有经验的用户通常需要用户手册吗?如果不需要,那么这个设计是紧凑的。

理解紧凑性可以从它的“反面”来理解,紧凑性不等于“薄弱”,如果一个设计构建在易于理解且利于组合的抽象概念上,则 这个系统能在具有非常强大、灵活的功能的同时保持紧凑。紧凑也不等同于“容易学习”:对于某些紧凑 设计而言,在掌握其精妙的内在基础概念模型之前,要理解这个设计相当困难;但一旦理解了这个概念模型,整个视角就会改变,紧凑的奥妙也就十分简单了。紧凑也不意味着“小巧”。即使一个设计良好的系统,对有经验的用户来说没什么特异之处、“一眼”就能看懂,但仍然可能包含很多部分。

评测一个api紧凑性的经验法则是:api入口点通常在7左右,或者按《代码大全2》的说法,7 27-2的范围内。

重构技术中的很多坏味道,特别是重复代码,是违反正交性的明显例子,“重构的原则性目标就是提高正交性”。

dry原则,或者称为spot原则(single point of truth)——真理的单点性。重复的不仅仅是代码,还包括数据结构,数据结构模型应该最小化,提倡寻找一种数据结构,使得模型中的状态跟真实世界系统的状态能够一一对应

要提高设计的紧凑性,有一个精妙但强大的方法,就是围绕“解决一个定义明确的问题”的强核心算法组织设计,避免臆断和捏造,将任务的核心形式化,建立明确的模型。


文本流是非常有用的通用格式,无需专门工具就可以很容易地读写和编辑文本流,这些格式是透明的。如果担心性能问题,就在协议层面之上或之下压缩文本协议流,最终产生的设计会比二进制协议更干净,性能可能更好。使用二进制协议的唯一正当理由是:如果要处理大批量的数据,因而确实关注能否在介质上获得最大位密度,或者是非常关心将数据转化为芯片核心结构所必须的时间或指令开销

数据文件元格式:

1dsv风格delimiter-seperated values

使用分隔符来分隔值,例如/etc/passwd

适合场景:数据为列表,名称(首个字段)为关键字,而且记录通常很短(小于80个字符)

2rfc 822格式

互联网电子邮件信息采用的文本格式,使用属性名 冒号 值的形式,记录属性每行存放一个,如http 1.1协议。

适合场景:任何带属性的或者与电子邮件类似的信息,非常适合具有不同字段集合而字段中数据层次又扁平的记录。

3cookie-jar格式。简单使用跟随%%的新行符(或者有时只有一个%)作为记录分隔符,很适用于记录非结构化文本的情况。

适合场景:词以上结构没有自然顺序,而且结构不易区别的文本段,或适用于搜索关键字而不是文本上下文的文本段。

4record-jar格式cookie-jarrfc-822的结合,形如

name:dennis
age:
21
%%
name:catty
age:
22
%%
name:green
age:
10

这样的格式。

适合场景:那些类似dsv文件,但又有可变字段数据而且可能伴随无结构文本的字段属性关系集合。

5xml格式,适合复杂递归和嵌套数据结构的格式,并且经常可以在无需知道数据语义的情况下仅通过语法检查就能发现形式不良损坏或错误生成的数据。缺点在于无法跟传统unix工具协作。

6windows ini格式,形如

[default]
account
=esr

[python]
directory
=/home/ers/cvs/python
developer
=1

[sng]
directory
=/home/esr/www/sng
numeric_id
=1001
developer
=1

[fetchmail]
numeric_id
=4928492

这样的格式

适合场景:数据围绕指定的记录或部分能够自然分成“名称-属性对”两层组织结构。

unix文本文件格式的约定:

1、如果可能,以新行符结束的每一行只存一个记录

2、如果可能,每行不超过80字符

3、使用”“引入注释

4、支持反斜杠约定

5、在每行一条记录的格式中,使用冒号或连续的空白作为字段分隔符。

6、不要过分区分tabwhitespace

7、优先使用十六进制而不是八进制。

8、对于复杂的记录,使用“节(stanza)”格式,要么让记录格式和rfc 822电子邮件头类似,用冒号终止的字段名关键字作为引导字段。

9、在节格式中,支持连续行,多个逻辑行折叠成一个物理行

10、要么包含一个版本号,要么将格式设计成相互独立的的自描述字节块。

11、注意浮点数取整。

12不要仅对文件的一部分进行压缩或者二进制编码。

应用协议元格式

1、经典的互联网应用元协议 rfc 3117《论应用协议的设计》,如smtppop3imap等协议

2、作为通用应用协议的http,在http上构建专用协议,如互联网打印协议(ipp

3beep:块可扩展交换协议,既支持c/s模式,又支持p2p模式

4xmp-rpcsoapjabber,基于xml的协议。

美在计算科学中的地位,要比在其他任何技术中的地位都重要,因为软件是太复杂了。美是抵御复杂的终极武器

如果没有阴暗的角落和隐藏的深度,软件系统就是透明的。透明性是一种被动品质。如果实际上能预测到程序行为的全部或大部分情况,并能建立简单的心理模型,这个程序就是透明的,因为可以看透机器究竟在干什么。

如果软件系统所包含的功能是为了帮助人们对软件建立正确的“做什么、怎样做”的心理模型而设计,这个软件系统就是可显的。

不要让调试工具仅仅成为一种事后追加或者用过就束之高阁的东西。它们是通往代码的窗口:不要只在墙上凿出粗糙的洞,要修整这些洞并装上窗。如果打算让代码一直可被维护,就始终必须让光照进去。例如fetchmail-v选项将处理smtppop的处理过程打印到标准输出,使得fetchmail行为具有可显性。

在“这个设计能行吗?”之后要提出的头几个问题就是“别人能读懂这个设计吗?这个设计优雅吗?”我们希望,此时大家已经很清楚,这些问题不是废话,优雅不是一种奢侈。在人类对软件的反映中,这些品质对于减少软件bug和提高软件长期维护性是最基本的。

要追求代码的透明性,最有效的方法是很简单,就是不要在具体操作的代码上叠放太多的抽象层。

oo语言使得抽象变得容易——也许是太容易了。oo语言鼓励“具有厚重的胶合和复杂层次”的体系。当问题领域真的很复杂,确实需要大量抽象时,这可能是好事,但如果coder到头来用复杂的办法做简单的事情——仅仅是为他们能够这样做,结果便适得其反。

所有的oo语言都显示出某种使程序员陷入过度分层陷阱的倾向。对象框架和对象浏览器并不能代替良好的设计和文档,但却常常被混为一谈。过多的层次破坏了透明性:我们很难看清这些层次,无法在头脑中理清代码到底是怎样运行的。简洁、清晰和透明原则通通被破坏了,结果代码中充满了晦涩的bug,始终存在维护问题。

胶合层中的“智能数据”却经常不代表任何程序处理的自然实体——仅仅只是胶合物而已(典型现象就是抽象类和混入(mixin)的不断扩散)

oo抽象的另一个副作用就是程序往往丧失了优化的机会。

oo在其取得成功的领域(gui、仿真和图形)之所以能成功,主要原因之一可能是因为在这些领域里很难弄错类型的本体问题。例如,在gui和图形系统中,类和可操作的可见对象之间有相当自然的映射关系

unix风格程序设计所面临的主要挑战就是如何将分离法的优点(将问题从原始的场景中简化、归纳)同代码和设计的薄胶合、浅平透层次结构的优点相组合。

太多的oo设计就像是意大利空心粉一样,把“is-a”have a的关系弄得一团糟,或者以厚胶合层为特征,在这个胶合层中,许多对象的存在似乎只不过是为了在陡峭的抽象金字塔上占个位置罢了。这些设计都不透明,它们晦涩难懂并且难以调试。

为透明性和可显性而编码:

1、程序调用层次中(不考虑递归)最大的静态深度是多少?如果大于,就要当心。

2、代码是否具有强大、明显的不变性质(约束)?不变性质帮助人们推演代码和发现有问题的情况。

3、每个api中各个函数调用是否正交?或者是否存在太多的magic flags或者模式位?

4、是否存在一些顺手可用的关键数据结构或全局唯一的记录器,捕获系统的高层次状态?这个状态是否容易被形象化和检验,还是分布在数目众多的各个全局变量或对象中而难以找到?

5、程序的数据结构或分类和它们所代表的外部实体之间,是否存在清晰的一对一映射。

6、是否容易找到给定函数的代码部分?不仅单个函数、模块,还有整个代码,需要花多少精力才能读懂

7、代码增加了特殊情况还是避免了特殊情况?每一个特殊情况可能对任何其他特殊情况产生影响:所有隐含的冲突都是bug滋生的温床。然而更重要的是,特殊情况使得代码更难理解。

8、代码中有多少个magic number?通过审查是否很容易查出代码中的限制(比如关键缓冲区的大小)?

隐藏细节无法访问细节有着重要区别。不要过度保护

无论何时碰到涉及编辑某类复杂二进制对象的设计问题时,unix传统都提倡首先考虑,是否能够编写一个能够在可编辑的文本格式和二进制格式之间来回进行无损转换的工具?这类工具可称为文本化器(textualizer).

宁愿抛弃、重建代码也不愿修补那些蹩脚的代码。

“代码是活代码、睡代码还是死代码?”活代码周围存在一个非常活跃的开发社团。睡代码之所以“睡着”,经常是因为对作者而言,维护代码的痛苦超过了代码本身的效用。死代码则是睡得太久,重新实现一段等价代码更容易



dennis 2010-02-19 19:25
]]>
《unix编程艺术》重读笔记(一)http://www.blogjava.net/killme2008/archive/2010/02/18/313402.htmldennisdennisthu, 18 feb 2010 11:23:00 gmthttp://www.blogjava.net/killme2008/archive/2010/02/18/313402.htmlhttp://www.blogjava.net/killme2008/comments/313402.htmlhttp://www.blogjava.net/killme2008/archive/2010/02/18/313402.html#feedback7http://www.blogjava.net/killme2008/comments/commentrss/313402.htmlhttp://www.blogjava.net/killme2008/services/trackbacks/313402.html

   unix哲学是自下而上,而不是自上而下的,注重实效,立足于丰富的经验,你不会在正规方法学和标准中找到它。

   unix管道的发明人doug mcilroy曾经说过:

1、让每个程序就做好一件事,如果有新任务就重新开始,不要往新程序中加入功能而搞的复杂。

2、假定每个程序的输出都会成为另一个程序的输入,哪怕那个程序是未知的。输出中不要有无关的信息干扰,避免使用严格的分栏格式和二进制格式输入。不要坚持使用交互式输入。

3、尽可能早将设计和编译的软件投入试用,哪怕是操作系统也不例外,理想情况下应该是几星期内,对抽劣的代码别犹豫,扔掉重写

4、优先使用工具,而非拙劣的帮助来减轻编程任务的负担,工欲善其事,必先利其器。


rob pike在《notes on c programming》中提到:

原则1:你无法断行程序会在什么地方耗费运行时间。瓶颈经常出现在想不到的地方,所以别急于胡乱找个地方改代码,除非你已经证实那儿就是瓶颈所在。

原则2:估量。在你没对代码进行估量,特别是没找到最耗时的那部分之前,别去优化速度。

原则3:花哨的算法,在n很小的适合通常很慢,而n通常很小。花哨算法的常数复杂度很大,除非你确定n一直很大,否则不要用花哨算法(即使n很大,也要优先考虑原则2)。

原则4:花哨的算法比简单的算法更容易出bug ,更难实现。尽量使用简单的算法配合简单的数据结构。

原则5数据压倒一切。如果已经选择了正确的数据结构并且把一切组织得井井有条,正确的算法也就不言自明,编程的核心是数据结构,而不是算法。

原则6:没有原则6.


ken thompson对原则4做了强调:

拿不准就穷举。


unix哲学的17条原则:

1、模块原则:简洁的接口拼合简单的部件。

2、清晰原则:清晰胜于机巧。

3、组合原则:设计时考虑拼接组合。

4、分离原则:策略同机制分离,接口同引擎分离。

5、简洁原则:设计要简洁,复杂度能低则低。

6、吝啬原则:除非却无他法,不要编写庞大的程序。

7、透明性原则:设计要可见,以便审查和调试。

8、健壮原则:健壮源于透明与简洁。

9、表示原则:把知识叠入数据,以求逻辑质朴而健壮。

10、通俗原则:接口设计避免标新立异。

11、缄默原则:如果一个程序没什么好说的,就沉默。

12、补救原则:出现异常时,马上退出,并给出足够错误信息。

13、经济原则:宁花机器一分,不花程序员一秒。

14、生成原则:避免手工hack,尽量编写程序去生成程序。

15、优化原则:雕琢前先要有原型,跑之前先学会走。

16、多样原则:绝不相信所谓“不二法门”的断言。

17、扩展原则:设计着眼未来,未来总是比预想来得快。


unix哲学之一言以蔽之:kiss

keep it simple,stupid!

应用unix哲学:

1、只要可行,一切都应该做成与来源和目标无关的过滤器

2、数据流应尽可能的文本化(这样可以用标准工具来查看和过滤)。

3、数据库部署和应用协议应尽可能文本化(让人阅读和编辑)。

4、复杂的前端(用户界面)和后端应该泾渭分明

5、如果可能,用c编写前,先用解释性语言搭建原型

6、当且仅当只用一门编程语言会提高程序复杂度时,混用语言编程才比单一语言编程来得好。

7宽收严发(对接收的东西要包容,对输出的东西要严格)

8、过滤时,不需要丢弃的消息绝不丢。

9小就是美。在确保完成任务的基础上,程序功能尽可能的少。


最后强调的是态度:

要良好地运用unix哲学,你就应该不断地追求卓越,你必须相信,程序设计是一门技艺,值得你付出所有的智慧、创造力和激情。否则,你的视线就不会超越那些简单、老套的设计和实现;你就会在应该思考的时候急急忙忙跑去编程。你就会在该无情删繁就简的时候反而把问题复杂化——然后你还会反过来奇怪你的代码怎么会那么臃肿,那么难以调试。

要良好地运用unix哲学,你应该珍惜你的时间绝不浪费。一旦某人已经解决了某个问题,就直接拿来利用,不要让骄傲或偏见拽住你又去重做一遍。永远不要蛮干;要多用巧劲,省下力气在需要的时候用,好钢用到刀刃上。善用工具,尽可能将一切自动化

软件设计和实现是一门充满快乐的艺术,一种高水平的游戏。如果这种态度对你来说听起来有些荒谬,或者令你隐约感到有些困窘,那么请停下来,想一想,问问自己是不是已经把什么给遗忘了。如果只是为了赚钱或者打发时间,你为什么要搞软件设计,而不是别的什么呢?你肯定曾经也认为软件设计值得你付出激情……

要良好地运用unix哲学,你需要具备(或者找回)这种态度。你需要用心。你需要去游戏。你需要乐于探索。


操作系统的风格元素:

1、什么是操作系统的统一性理念

2、多任务能力

3、协作进程(ipc

4、内部边界

5、文件属性和记录结构

6、二进制文件格式

7、首选用户界面风格

8、目标受众

9、开发的门坎



dennis 2010-02-18 19:23
]]>
设计方案的评判http://www.blogjava.net/killme2008/archive/2009/11/28/304066.htmldennisdennissat, 28 nov 2009 15:38:00 gmthttp://www.blogjava.net/killme2008/archive/2009/11/28/304066.htmlhttp://www.blogjava.net/killme2008/comments/304066.htmlhttp://www.blogjava.net/killme2008/archive/2009/11/28/304066.html#feedback0http://www.blogjava.net/killme2008/comments/commentrss/304066.htmlhttp://www.blogjava.net/killme2008/services/trackbacks/304066.html简单、符合当前和一定预期时间内的需求、可靠、直观(或者说透明)。
   简单,不是简陋,这是废话了。但是要做到简单,却是绝不简单,简单跟第四点直观有直接的关系,简单的设计就是一个直观的设计,可以让你一眼看得清的设计方案,也就是透明。一个最简单的评判方法,你可以讲这个方案讲给一个局外人听,如果能在短时间内让人理解的,那么这个方案八成是靠谱的。记的有本书上讲过类似的方法,你有一个方案,那就拿起电话打给一个无关的人员,如果你能在电话里说清楚,就表示这个方案相当靠谱,如果不能,那么这个方案很可能是过度复杂,过度设计了。
   简单的设计,往往最后得出的结果是一个可靠性非常高的系统。这很容易理解,一个复杂的设计方案,有很多方面会导致最后的实现会更复杂:首先是沟通上的困难,一个复杂的方案是很难在短时间内在团队内部沟通清楚,每个开发人员对这个方案的理解可能有偏差;其次,复杂的方案往往非常考验设计人员和开发人员的经验、能力和细致程度,复杂的方案要考量的方面肯定比简单方案多得多,一个地方没有考虑到或者不全面,结果就是一个充满了隐患的系统,时不时地蹦出一个bug来恶心你,这并非开发人员的能力问题,而是人脑天然的局限性(天才除外,咳咳)。
   第二点,符合当前和一定预期时间内的需求。我们都知道,不变的变化本身,指望一个方案永久解决所有问题是乌托邦的梦想。复杂方案的出炉通常都是因为设计人员过度考量了未来系统的需求和变化,我们的系统以后要达到10倍的吞吐量,我们的系统以后要有几十万的节点等等。当然,首先要肯定的是对未来需求的考量是必需的,一个系统如果实现出来只能应付短时间的需求,那肯定是不能接受的。但是我们要警惕的是过度考量导致的过度复杂的设计方案,这还有可能是设计人员“炫技”的欲望隐藏在里头。这里面有一个权衡的问题,比如这里有两个方案:一个是两三年内绝对实用的方案,简单并且可靠直观,未来的改进余地也不错;另一个方案是可以承载当前的几十倍流量的方案,方案看起来很优雅,很时尚,实现起来也相对复杂。如何选择?如果这个系统是我们当前就要使用的,并且是关键系统,那么我绝对会选择前一个方案,在预期时间内去改进这个方案,因为对于关键系统,简单和可靠是性命攸关的。况且,我坚定地认为一个复杂的设计方案中绝对隐藏着一个简单的设计,这就像一个复杂的数学证明,通常都可以用更直观更简单的方式重新证明(题外话,费尔马大定理的证明是极其复杂的,现在还有很多人坚信有一个直观简单的证明存在,也就是那个费尔马没有写下的证明)。最近我们的一个方案讨论也证明了这一点,一个消息优先级的方案,一开始的设想是相对复杂的,需要在存储结构和调度上动手脚,后来集思广益,最后定下的方案非常类似linux的进程调度策略,通过分级queue和时间片轮询的方式完美解决了优先级的问题。这让我想起了软件开发的“隐喻”的力量,很多东西都是相通相似的。
   上面这些乱弹都是自己在最近碰到的一些讨论和系统故障想起的,想想还是有必要写下来做个记录。

   



dennis 2009-11-28 23:38
]]>
一个优秀的mq产品的特点。http://www.blogjava.net/killme2008/archive/2009/09/18/295517.htmldennisdennisthu, 17 sep 2009 16:09:00 gmthttp://www.blogjava.net/killme2008/archive/2009/09/18/295517.htmlhttp://www.blogjava.net/killme2008/comments/295517.htmlhttp://www.blogjava.net/killme2008/archive/2009/09/18/295517.html#feedback1http://www.blogjava.net/killme2008/comments/commentrss/295517.htmlhttp://www.blogjava.net/killme2008/services/trackbacks/295517.html
    首先显然是高可用性,我们当然希望mq能支撑7x24小时应用,而不是三天两头当机,我们要追求的是99.9%的可靠服务时间。要做到高可用性,显然我们需要做mq的集群,一台当了,不影响整个集群的服务能力,这里涉及到告警、流控、消息的负载均衡、数据库的使用、测试的完备程度等等。

    其次是消息存储的高可靠性。我们要保证100%不丢消息。要做到消息存储的高可靠性,不仅仅是mq的责任,更涉及到硬件、操作系统、语言平台和数据库的一整套方案。许多号称可靠存储的mq产品其实都不可靠,要知道,硬件错误是常态,如果在硬件错误的情况下还能保证消息的可靠存储这才是难题。这里可能需要用到特殊的存储硬件,特殊的数据库,分布式的数据存储,数据库的分库分表和同步等等。你要考虑消息存储在哪里,是文件系统,还是数据库,是本地文件,还是分布式文件,是搞主辅备份呢还是多主机写入等等。
   
    第三是高可扩展性,mq集群能很好地支持水平扩展,这就要求我们的节点之间最好不要有通信和数据同步。

    第四是性能,性能是实现高可用性的前提,很难想象单机性能极差的mq组成的集群能在高负载下幸免于难。性能因素跟采用的平台、语言、操作系统、代码质量、数据库、网络息息相关。mq产品的核心其实是消息的存储,在保证存储安全的前提下如何保证和提高消息入队的效率是性能的关键因素。这里需要开发人员建立起性能观念,不需要你对一行行代码斤斤计较,但是你需要知道这样做会造成什么后果,有没有更好更快的方式,你怎么证明它更好更快。软件实现的性能是一方面,另一方面就是平台相关了,因为mq本质上是io密集型的系统,瓶颈在io,如何优化网络io、文件io这需要专门的知识。性能另一个相关因素是消息的调度上,引入消息顺序和消息优先级,允许消息的延迟发送,都将增大消息发送调度的复杂性,如何保证高负载下的调度也是要特别注意的地方。

  
   第五,高可配置性和监控工具的完整,这是一个mq产品容易忽略的地方。异步通信造成了查找问题的难度,不像同步调用那样有相对明确的时序关系。因此查找异步通信的异常是很困难的,这就需要mq提供方便的debug工具,查找分析日志的工具,查看消息生命周期的工具,查看系统间依赖关系的工具等等。可定制也是mq产品非常重要的一方面,可方便地配置各类参数并在集群中同步,并且可动态调整各类参数,这将大大降低维护难度。

   一些不成熟的想法,瞎侃。


   




dennis 2009-09-18 00:09
]]>
资源获取模式http://www.blogjava.net/killme2008/archive/2008/09/07/227596.htmldennisdennissun, 07 sep 2008 14:10:00 gmthttp://www.blogjava.net/killme2008/archive/2008/09/07/227596.htmlhttp://www.blogjava.net/killme2008/comments/227596.htmlhttp://www.blogjava.net/killme2008/archive/2008/09/07/227596.html#feedback1http://www.blogjava.net/killme2008/comments/commentrss/227596.htmlhttp://www.blogjava.net/killme2008/services/trackbacks/227596.html     资源的超找可以通过lookup模式,所谓lookup模式就是引入一个查找服务作为中介,让资源使用者可以 找到并使用资源提供者提供的资源。这方面一个显然的例子就是jndi,我们通过jndi查找ejb、数据库连接池等,另一个例子就是rmi,客户端通过命名服务查找到远程对象。查找服务提供查询语言,也提供接口让服务注册到它的容器内。
    何时获取资源?根据时机的不同可以分为预先获取模式和延迟获取模式。如果对系统的可用性和运行时性能要求比较高,那么可能你会预先加载或者分配所要用到的资源,通过一个资源提供者的代理对象拦截对资源的请求,返回预先加载或者分配的资源,从而提高系统性能。这方面的例子就是 memcached,memcached启动时预先分配内存来提高性能(相应的带来的是内存的不能有效利用)。线程池、数据库连接池也是预先获取模式的例子,通过预先创建的线程或者数据库连接,来提高系统的整体性能。反之,延迟获取模式就是尽可能到使用资源的最后一刻才去获取资源,一开始返回的只是一个资源的代理对象,真正使用到资源的时候由这个代理对象去加载实际的资源。延迟获取模式的例子也很多,例如 hibernate的延迟加载,延迟加载的对象其实是一个代理类(通过java动态代理或者cglib增强),只有真正用到的时候才去数据库获取实际的数 据。函数式编程中的延时求值也是延迟获取模式的例子,没有求值前只是个promise,求值到的时候才force执行。singleton模式下的单例也常常做延迟初始化,这是延迟获取模式的特化。
    如果资源的大小很大、极大或者是未知尺寸,预先获取会导致系统速度的缓慢甚至耗尽系统资源,延迟获取在获取的时候也可能时间过长难以忍受或者耗尽系统资源,两者都不适合,解决办法就是分步获取,这就是所谓部分获取模式。将资源的获取分为两步或者多步,每一步获取一定数目的资源,每一步获取资源也可以采用预先或者延迟的策略。这样的例子如socket读数据到缓冲区,一次可能只读一部分数据到缓冲区,分步才读完。再比如web页面一棵树的展 现,一开始可能只是显示部分节点,在需要查看一些节点的时候才会通过ajax加载和展开它们的子结点乃至深层子结点。

参考:posa3 第二章


dennis 2008-09-07 22:10
]]>
hdfs用户指南(翻译)http://www.blogjava.net/killme2008/archive/2008/08/14/222097.htmldennisdennisthu, 14 aug 2008 12:24:00 gmthttp://www.blogjava.net/killme2008/archive/2008/08/14/222097.htmlhttp://www.blogjava.net/killme2008/comments/222097.htmlhttp://www.blogjava.net/killme2008/archive/2008/08/14/222097.html#feedback2http://www.blogjava.net/killme2008/comments/commentrss/222097.htmlhttp://www.blogjava.net/killme2008/services/trackbacks/222097.html 原文地址:http://hadoop.apache.org/core/docs/current/hdfs_user_guide.html
译者:dennis zhuang(killme2008@gmail.com),有错误请指正,多谢。

目的

本文档可以作为使用hadoop分布式文件系统用户的起点,无论是将hdfs应用在一个hadoop集群中还是作为一个单独的分布式文件系统使用。hdfs被设计成可以马上在许多环境中工作起来,那么一些hdfs的运行知识肯定能大大地帮助你对一个集群做配置改进和诊断。

概览

hdfs是hadoop应用的主要分布式存储。一个hdfs集群由一个管理文件系统元数据的namenode,和存储实际 数据的一些datanode组成。hdfs的架构在这里有详细描述。这个用户指南主要提供给需要跟hdfs集群打交道的用户或者管理员。hdfs架构文章 中的图描绘了namenode、datanode和客户端们之间的基本交互。本质上,客户端与namenode通讯获取或者修改文件的元数据,与 datanode进行实际的io操作。

下面的列表应该是大多数用户关心的hdfs突出特点。斜体字的术语将在后面详细描述。

1)hadoop,包括hdfs,非常适合廉价机器上的分布式存储和分布式处理。它是容错的、可伸缩的,并且非常易于扩展。并且,以简单性和适用性著称的map-reduce是hadoop不可或缺的组成部分。

2)hdfs的默认配置适合于大多数安装的应用。通常情况下,只有在一个非常大规模的集群上才需要修改默认配置。

3)hdfs是用java编写的,支持大多数平台。

4)支持shell命令行风格的hdfs目录交互。

5)namenode和datanode都内建了web服务器,可以方便地查看集群的状态

6)hdfs经常性地实现新的特性和改进,下面是hdfs中的一些有用特性的子集:

   文件许可和授权

   rack awareness:当调度任务和分配存储的时候将节点的物理位置考虑进去。

   safemode(安全模式):用于维护的一个管理状态

   fsck: 诊断文件系统的一个工具,用来查找丢失的文件或者block

   rebalancer:当数据在datanode间没有均匀分布的时候,用于重新平衡集群的工具

   升级和回滚:当hadoop软件升级,在升级遇到不可预期的问题的时候,可以回滚到hdfs升级前的状态

   二级namenode:帮助namenode维持包含了hdfs修改的日志的文件(edits日志文件,下文谈到)大小在限制范围内。

前提条件

下面的文档描述了一个hadoop集群的安装和设置:

  • ,给初次使用用户
  • 大规模、分布式集群


本文档的剩余部分假设你已经搭设并运行了一个至少拥有一个datanode的hdfs。基于本文档的目的,namenode和datanode可以运行在同一台机器上。

web接口

namenode和datanode分别跑了一个内置的web服务器,来展现集群当前状态的一些基本信息。在默认配置 下,namenode的凯发k8网页登录首页地址是http://namenode:50070(namenode就是namenode节点所在机器ip或者名称)。这个 页面列出了集群中的所有datanode以及集群的基本统计。web接口同样可以用于浏览文件系统(点击namenode凯发k8网页登录首页上的“browse the file system"链接)。

shell命令

hadoop包括了多种shell风格的命令,用于跟hdfs或者hadoop支持的其他文件系统交互。命令 bin/hadoop fs -help 可以列出hadoop shell支持的命令。更进一步,bin/hadoop fs -help command 可以展现特定命令command的帮助细节。这些命令支持一般文件系统的操作,例如拷贝文件、修改文件权限等。同时也支持了部分hdfs特有的命令,例如 修改文件的replication因子。

dfsadmin命令

'bin/hadoop dfsadmin' 命令支持一些hdfs管理功能的操作。'bin/hadoop dfsadmin -help'可以列出所有当前支持的命令。例如:

  • -report : 报告hdfs的基本统计信息。部分信息同时展现在namenode的web凯发k8网页登录首页上。 
  • -safemode : 尽管通常并不需要,管理员还是可以通过手工操作进入或者离开safemode状态
  • -finalizeupgrade : 移除上一次升级时集群所做的备份。

二级namenode

namenode将对文件系统的修改存储在一个原生文件系统文件中(名为edits的文件)。当namenode启动的时 候,它从映像文件(fsimage)读取hdfs的状态,然后将edits日志文件中的修改作用在此内存状态上,接着将得到的新的hdfs状态写回 fsimage,后续的正常操作开始于一个空的edits日志文件。由于namenode仅仅在启动的时候将fsimage和edits合并,因此在一个 大的集群上经过一定时间操作后,edits文件将会非常大。由此带来的一个副作用就是下次namenode的重新启动将花费很长时间。二级 namenode就是为了解决这个问题,它会周期性地合并fsimage和edits日志文件,并且将edits日志文件的大小保持在限制范围内。通常它 会跑在另一个机器上,因为它的内存要求跟主namenode一样。二级namenode可以通过'bin/start-dfs.sh'启动在conf /masters配置文件里配置的节点上。

rebalancer

hdfs的数据可能不会总是在datanode之间分布得很一致。一个常见的原因是往现有的集群中加入了新的datanode。当分配block的时候,namenode依据几个参数来决定哪个datanode来接受这些block。一些需要考虑的因素如下:

1)一个block的副本存放在正在写该block的节点上

2)需要将一个block的副本扩展到其他机架上,防止因为整个机架故障导致的数据丢失。

3)副本之一通常放在同一个机架的另一个节点上,减少跨机架的网络io

4)将hdfs数据均匀一致地分布在集群中的datanode上。

    基于这些相互竞争的因素,数据可能不会在datanode之间扩展得一致。hdfs给管理员提供了一个工具,用来分析block的分配情况和在datanode之间重新平衡数据。这个功能暂未实现,它的描述可以在这个 文档中看到,记录编号.

rack awareness

典型的大规模hadoop集群是部署在数个机架上的,那么显然同一个机架内的节点间的网络通讯比之不同机架间节点间的网 络通讯更可取。另外,namenode会尝试将block的副本分布在数个机架中以提高容错性。hadoop让集群管理员来决定某个节点从属于哪个机架, 通过配置变量dfs.network.script来实现。当这个脚本有配置的时候,每个节点都运行该脚本来决定它的rackid。默认安装假设所有的节 点从属于同一个机架。这个特性和配置进一步的阐述在这个文档,编号为 。

safemod(安全模式)

当namenode启动的时候,它从fsimage和edits日志两个文件中加载文件系统的状态。然后等待 datanode报告他们的block信息,以便防止namenode在确认block副本是否足够前过早地开始复制block。这段时间的 namenode就是处于所谓safemode状态。处于safemode的namenode也是hdfs集群的只读模型,此时不允许任何对文件系统或者 block的修改。正常情况下,namenode会在开始后自动退出safemode。如果有需要,hdfs可以通过'bin/hadoop dfsadmin -safemode'命令显式地进入safemode状态。namenode的web凯发k8网页登录首页显示当前的safemode是否打开。更详细的描述和配置可以参 考方法的javadoc。

译 注:详细介绍下safemode的配置参数,在safemode状态,namenode会等待所有的datanode报告他们自己的block信息,看看 所有的block的副本是否达到最低要求的数目,这个数目可以通过dfs.replication.min参数配置,默认是1,也就是至少要求有一个副 本。当报告合格的datanode的数目达到一定百分比,namenode才会离开safemode状态。这个百分比也是可配置的,通过 dfs.safemode.threshold.pct参数,默认是0.999f(也就是要求99.9%的datanode 合格)。namenode在合格的datanode数目达到要求的时候,并不是马上离开safemode状态,会有一个扩展时间,让剩余的 datanode来报告block信息,这个扩展时间默认是30秒,可以通过dfs.safemode.extension参数配置,单位是毫秒。

fsck

hdfs提供了fsck命令用来检测各种各样的不一致性。fsck被设计用来报告各种文件的问题,例如某个文件丢失的 block,block的副本数目是否低于设置等。不同于传统的一般原生文件系统的fsck命令,hdfs的fsck命令并不修正所检测到的错误。通常情 况下,namenode会自动修正大多数可以被修复的错误,hdfs的fsck不是hadoop shel的命令,可以通过'bin/hadoop fsck'执行,可以运行在整个文件系统上或者一个文件子集上。

升级和回滚

当升级某个集群的hadoop的时候,正如任何软件的升级一样,可能会引入新的bug或者不兼容的修改导致现有的应用出 现过去没有发现的问题。在所有重要的hdfs安装应用中,是不允许出现因丢失任何数据需要从零开始重启hdfs的情况。hdfs允许管理员恢复到 hadoop的早期版本,并且将集群的状态回滚到升级前。hdfs的升级细节请参考 。hdfs在任何时间只能有一个备份,因此在升级前,管理员需要通过'bin/hadoop dfsadmin -finalizeupgrade'命令移除现有的备份。下面简要描述了典型的升级过程:

1)在升级hadoop前,如果已经存在备份,需要先结束(finalize)它。可以通过'dfsadmin -upgradeprogress status'命令查询集群是否需要执行finalize

2)停止集群,分发部署新版本的hadoop

3)执行新版本的hadoop,通过添加 -upgrade 选项,例如/bin/start-dfs.sh -upgrade

4)大多数情况下,集群在升级后可以正常运行。一旦新的hdfs在运行若干天的操作后没有出现问题,那么就可以结束(finalize)这次升级。请注意,在升级前删除的文件并不释放在datanode上的实际磁盘空间,直到集群被结束(finalize)升级前。

5)如果有需要回到老版本的hadoop,那么可以:

   a)停止集群,分发部署老版本的hadoop

   b)通过rollback选项启动集群,例如bin/start-dfs.sh -rollback

文件许可和安全

文件许可的设计与其他平台(如linux) 的文件系统类似。在当前实现,安全被限制在简单的文件许可上。启动namenode的用户被作为hdfs的超级用户。hdfs的未来版本将支持网络验证, 例如kerberos方案(译注:mit开发的一个验证系统)的用户验证以及数据传输的加密。更详细的讨论参考。

伸缩性

hadoop正运行在成千上万个节点的集群上。 列 出了一些部署hadoop在大规模集群上的组织和机构。hdfs在每个集群上只有一个namenode节点,namenode节点上可用内存是当前伸缩性 的主要限制。在非常大规模的集群上,增加hdfs中存储的文件的平均大小,将可以帮助提高集群的大小而不用增加namenode的内存需求。默认的配置可 能不适合非常大规模的集群应用。页列出了对于大规模hadoop集群的配置改进建议。

关联文档

 本用户指南可作为使用hdfs很好的一个起点,在本文档持续改进的同时,有一些非常有价值的关于hadoop和hdfs的文档资料可供参考。下列资料可作为进一步探索的起点:

  • : hadoop一切的起始页。
  • :由社区维护的wiki文档。
  • from hadoop wiki.
  • hadoop .
  • hadoop user mailing list : .
  • 浏览conf/hadoop-default.xml文件,它包括了当前可用配置变量的概括介绍。



dennis 2008-08-14 20:24
]]>
hadoop分布式文件系统:架构和设计要点http://www.blogjava.net/killme2008/archive/2008/06/05/206043.htmldennisdennisthu, 05 jun 2008 06:29:00 gmthttp://www.blogjava.net/killme2008/archive/2008/06/05/206043.htmlhttp://www.blogjava.net/killme2008/comments/206043.htmlhttp://www.blogjava.net/killme2008/archive/2008/06/05/206043.html#feedback23http://www.blogjava.net/killme2008/comments/commentrss/206043.htmlhttp://www.blogjava.net/killme2008/services/trackbacks/206043.htmlhadoop分布式文件系统:架构和设计要点
原文:http://hadoop.apache.org/core/docs/current/hdfs_design.html
一、前提和设计目标
1、硬件错误是常态,而非异常情况,hdfs可能是有成百上千的server组成,任何一个组件都有可能一直失效,因此错误检测和快速、自动的恢复是hdfs的核心架构目标。
2、跑在hdfs上的应用与一般的应用不同,它们主要是以流式读为主,做批量处理;比之关注数据访问的低延迟问题,更关键的在于数据访问的高吞吐量。
3hdfs以支持大数据集合为目标,一个存储在上面的典型文件大小一般都在千兆至t字节,一个单一hdfs实例应该能支撑数以千万计的文件。
4hdfs应用对文件要求的是write-one-read-many访问模型。一个文件经过创建、写,关闭之后就不需要改变。这一假设简化了数据一致性问 题,使高吞吐量的数据访问成为可能。典型的如mapreduce框架,或者一个web crawler应用都很适合这个模型。
5、移动计算的代价比之移动数据的代价低。一个应用请求的计算,离它操作的数据越近就越高效,这在数据达到海量级别的时候更是如此。将计算移动到数据附近,比之将数据移动到应用所在显然更好,hdfs提供给应用这样的接口。
6、在异构的软硬件平台间的可移植性。

二、namenodedatanode
    hdfs采用master/slave架构。一个hdfs集群是有一个namenode和一定数目的datanode组成。namenode是一个中心服 务器,负责管理文件系统的namespace和客户端对文件的访问。datanode在集群中一般是一个节点一个,负责管理节点上它们附带的存储。在内 部,一个文件其实分成一个或多个block,这些block存储在datanode集合里。namenode执行文件系统的namespace操作,例如 打开、关闭、重命名文件和目录,同时决定block到具体datanode节点的映射。datanodenamenode的指挥下进行block的创 建、删除和复制。namenodedatanode都是设计成可以跑在普通的廉价的运行linux的机器上。hdfs采用java语言开发,因此可以部 署在很大范围的机器上。一个典型的部署场景是一台机器跑一个单独的namenode节点,集群中的其他机器各跑一个datanode实例。这个架构并不排 除一台机器上跑多个datanode,不过这比较少见。

单一节点的namenode大大简化了系统的架构。namenode负责保管和管理所有的hdfs元数据,因而用户数据就不需要通过namenode(也就是说文件数据的读写是直接在datanode上)。

三、文件系统的namespace
   hdfs支持传统的层次型文件组织,与大多数其他文件系统类似,用户可以创建目录,并在其间创建、删除、移动和重命名文件。hdfs不支持user quotas和访问权限,也不支持链接(link),不过当前的架构并不排除实现这些特性。namenode维护文件系统的namespace,任何对文 件系统namespace和文件属性的修改都将被namenode记录下来。应用可以设置hdfs保存的文件的副本数目,文件副本的数目称为文件的 replication因子,这个信息也是由namenode保存。

四、数据复制
    hdfs被设计成在一个大集群中可以跨机器地可靠地存储海量的文件。它将每个文件存储成block序列,除了最后一个block,所有的block都是同 样的大小。文件的所有block为了容错都会被复制。每个文件的block大小和replication因子都是可配置的。replication因子可 以在文件创建的时候配置,以后也可以改变。hdfs中的文件是write-one,并且严格要求在任何时候只有一个writernamenode全权管 理block的复制,它周期性地从集群中的每个datanode接收心跳包和一个blockreport。心跳包的接收表示该datanode节点正常工 作,而blockreport包括了该datanode上所有的block组成的列表。


1、副本的存放,副本的存放是hdfs可靠性和性能的关键。hdfs采用一种称为rack-aware的策略来改进数据的可靠性、有效性和网络带宽的利 用。这个策略实现的短期目标是验证在生产环境下的表现,观察它的行为,构建测试和研究的基础,以便实现更先进的策略。庞大的hdfs实例一般运行在多个机 架的计算机形成的集群上,不同机架间的两台机器的通讯需要通过交换机,显然通常情况下,同一个机架内的两个节点间的带宽会比不同机架间的两台机器的带宽 大。
    通过一个称为rack awareness的过程,namenode决定了每个datanode所属的rack id。一个简单但没有优化的策略就是将副本存放在单独的机架上。这样可以防止整个机架(非副本存放)失效的情况,并且允许读数据的时候可以从多个机架读 取。这个简单策略设置可以将副本分布在集群中,有利于组件失败情况下的负载均衡。但是,这个简单策略加大了写的代价,因为一个写操作需要传输block到 多个机架。
    在大多数情况下,replication因子是3hdfs的存放策略是将一个副本存放在本地机架上的节点,一个副本放在同一机架上的另一个节点,最后一 个副本放在不同机架上的一个节点。机架的错误远远比节点的错误少,这个策略不会影响到数据的可靠性和有效性。三分之一的副本在一个节点上,三分之二在一个 机架上,其他保存在剩下的机架中,这一策略改进了写的性能。

2、副本的选择,为了降低整体的带宽消耗和读延时,hdfs会尽量让reader读最近的副本。如果在reader的同一个机架上有一个副本,那么就读该副本。如果一个hdfs集群跨越多个数据中心,那么reader也将首先尝试读本地数据中心的副本。

3safemode
    namenode启动后会进入一个称为safemode的特殊状态,处在这个状态的namenode是不会进行数据块的复制的。namenode从所有的 datanode接收心跳包和blockreportblockreport包括了某个datanode所有的数据块列表。每个block都有指定的最 小数目的副本。当namenode检测确认某个datanode的数据块副本的最小数目,那么该datanode就会被认为是安全的;如果一定百分比(这 个参数可配置)的数据块检测确认是安全的,那么namenode将退出safemode状态,接下来它会确定还有哪些数据块的副本没有达到指定数目,并将 这些block复制到其他datanode

五、文件系统元数据的持久化
    namenode存储hdfs的元数据。对于任何对文件元数据产生修改的操作,namenode都使用一个称为editlog的事务日志记录下来。例如, 在hdfs中创建一个文件,namenode就会在editlog中插入一条记录来表示;同样,修改文件的replication因子也将往 editlog插入一条记录。namenode在本地os的文件系统中存储这个editlog。整个文件系统的namespace,包括block到文件 的映射、文件的属性,都存储在称为fsimage的文件中,这个文件也是放在namenode所在系统的文件系统上。
    namenode在内存中保存着整个文件系统namespace和文件blockmap的映像。这个关键的元数据设计得很紧凑,因而一个带有4g内存的 namenode足够支撑海量的文件和目录。当namenode启动时,它从硬盘中读取editlogfsimage,将所有editlog中的事务作 用(apply)在内存中的fsimage ,并将这个新版本的fsimage从内存中flush到硬盘上,然后再truncate这个旧的editlog,因为这个旧的editlog的事务都已经 作用在fsimage上了。这个过程称为checkpoint。在当前实现中,checkpoint只发生在namenode启动时,在不久的将来我们将 实现支持周期性的checkpoint
    datanode并不知道关于文件的任何东西,除了将文件中的数据保存在本地的文件系统上。它把每个hdfs数据块存储在本地文件系统上隔离的文件中。 datanode并不在同一个目录创建所有的文件,相反,它用启发式地方法来确定每个目录的最佳文件数目,并且在适当的时候创建子目录。在同一个目录创建 所有的文件不是最优的选择,因为本地文件系统可能无法高效地在单一目录中支持大量的文件。当一个datanode启动时,它扫描本地文件系统,对这些本地 文件产生相应的一个所有hdfs数据块的列表,然后发送报告到namenode,这个报告就是blockreport

六、通讯协议
    所有的hdfs通讯协议都是构建在tcp/ip协议上。客户端通过一个可配置的端口连接到namenode,通过clientprotocolnamenode交互。而datanode是使用datanodeprotocolnamenode交互。从clientprotocoldatanodeprotocol抽象出一个远程调用(rpc),在设计上,namenode不会主动发起rpc,而是是响应来自客户端和 datanode rpc请求。

七、健壮性
    hdfs的主要目标就是实现在失败情况下的数据存储可靠性。常见的三种失败:namenode failures, datanode failures和网络分割(network partitions)
1、硬盘数据错误、心跳检测和重新复制
    每个datanode节点都向namenode周期性地发送心跳包。网络切割可能导致一部分datanodenamenode失去联系。 namenode通过心跳包的缺失检测到这一情况,并将这些datanode标记为dead,不会将新的io请求发给它们。寄存在dead datanode上的任何数据将不再有效。datanode的死亡可能引起一些block的副本数目低于指定值,namenode不断地跟踪需要复制的 block,在任何需要的情况下启动复制。在下列情况可能需要重新复制:某个datanode节点失效,某个副本遭到损坏,datanode上的硬盘错 误,或者文件的replication因子增大。

2、集群均衡
   hdfs支持数据的均衡计划,如果某个datanode节点上的空闲空间低于特定的临界点,那么就会启动一个计划自动地将数据从一个datanode搬移 到空闲的datanode。当对某个文件的请求突然增加,那么也可能启动一个计划创建该文件新的副本,并分布到集群中以满足应用的要求。这些均衡计划目前 还没有实现。

3、数据完整性
  从某个datanode获取的数据块有可能是损坏的,这个损坏可能是由于datanode的存储设备错误、网络错误或者软件bug造成的。hdfs客户端 软件实现了hdfs文件内容的校验和。当某个客户端创建一个新的hdfs文件,会计算这个文件每个block的校验和,并作为一个单独的隐藏文件保存这些 校验和在同一个hdfs namespace下。当客户端检索文件内容,它会确认从datanode获取的数据跟相应的校验和文件中的校验和是否匹配,如果不匹配,客户端可以选择 从其他datanode获取该block的副本。

4、元数据磁盘错误
    fsimageeditloghdfs的核心数据结构。这些文件如果损坏了,整个hdfs实例都将失效。因而,namenode可以配置成支持维护多 个fsimageeditlog的拷贝。任何对fsimage或者editlog的修改,都将同步到它们的副本上。这个同步操作可能会降低 namenode每秒能支持处理的namespace事务。这个代价是可以接受的,因为hdfs是数据密集的,而非元数据密集。当namenode重启的 时候,它总是选取最近的一致的fsimageeditlog使用。
   namenodehdfs是单点存在,如果namenode所在的机器错误,手工的干预是必须的。目前,在另一台机器上重启因故障而停止服务的namenode这个功能还没实现。

5、快照
   快照支持某个时间的数据拷贝,当hdfs数据损坏的时候,可以恢复到过去一个已知正确的时间点。hdfs目前还不支持快照功能。

八、数据组织
1、数据块
    兼容hdfs的应用都是处理大数据集合的。这些应用都是写数据一次,读却是一次到多次,并且读的速度要满足流式读。hdfs支持文件的write- once-read-many语义。一个典型的block大小是64mb,因而,文件总是按照64m切分成chunk,每个chunk存储于不同的 datanode
2、步骤
    某个客户端创建文件的请求其实并没有立即发给namenode,事实上,hdfs客户端会将文件数据缓存到本地的一个临时文件。应用的写被透明地重定向到 这个临时文件。当这个临时文件累积的数据超过一个block的大小(默认64m),客户端才会联系namenodenamenode将文件名插入文件系 统的层次结构中,并且分配一个数据块给它,然后返回datanode的标识符和目标数据块给客户端。客户端将本地临时文件flush到指定的 datanode上。当文件关闭时,在临时文件中剩余的没有flush的数据也会传输到指定的datanode,然后客户端告诉namenode文件已经 关闭。此时namenode才将文件创建操作提交到持久存储。如果namenode在文件关闭前挂了,该文件将丢失。
   上述方法是对通过对hdfs上运行的目标应用认真考虑的结果。如果不采用客户端缓存,由于网络速度和网络堵塞会对吞估量造成比较大的影响。

3、流水线复制
    当某个客户端向hdfs文件写数据的时候,一开始是写入本地临时文件,假设该文件的replication因子设置为3,那么客户端会从namenode 获取一张datanode列表来存放副本。然后客户端开始向第一个datanode传输数据,第一个datanode一小部分一小部分(4kb)地接收数 据,将每个部分写入本地仓库,并且同时传输该部分到第二个datanode节点。第二个datanode也是这样,边收边传,一小部分一小部分地收,存储 在本地仓库,同时传给第三个datanode,第三个datanode就仅仅是接收并存储了。这就是流水线式的复制。

九、可访问性
    hdfs给应用提供了多种访问方式,可以通过dfsshell通过命令行与hdfs数据进行交互,可以通过java api调用,也可以通过c语言的封装api访问,并且提供了浏览器访问的方式。正在开发通过webdav协议访问的方式。具体使用参考文档。
十、空间的回收
1、文件的删除和恢复
    用户或者应用删除某个文件,这个文件并没有立刻从hdfs中删除。相反,hdfs将这个文件重命名,并转移到/trash目录。当文件还在/trash目 录时,该文件可以被迅速地恢复。文件在/trash中保存的时间是可配置的,当超过这个时间,namenode就会将该文件从namespace中删除。 文件的删除,也将释放关联该文件的数据块。注意到,在文件被用户删除和hdfs空闲空间的增加之间会有一个等待时间延迟。
    当被删除的文件还保留在/trash目录中的时候,如果用户想恢复这个文件,可以检索浏览/trash目录并检索该文件。/trash目录仅仅保存被删除 文件的最近一次拷贝。/trash目录与其他文件目录没有什么不同,除了一点:hdfs在该目录上应用了一个特殊的策略来自动删除文件,目前的默认策略是 删除保留超过6小时的文件,这个策略以后会定义成可配置的接口。

2replication因子的减小
    当某个文件的replication因子减小,namenode会选择要删除的过剩的副本。下次心跳检测就将该信息传递给datanodedatanode就会移除相应的block并释放空间,同样,在调用setreplication方法和集群中的空闲空间增加之间会有一个时间延迟。

参考资料:
hdfs java api: http://hadoop.apache.org/core/docs/current/api/
hdfs source code: http://hadoop.apache.org/core/version_control.html
   









dennis 2008-06-05 14:29
]]>
模块的设计(书摘)http://www.blogjava.net/killme2008/archive/2008/04/06/191022.htmldennisdennissun, 06 apr 2008 05:00:00 gmthttp://www.blogjava.net/killme2008/archive/2008/04/06/191022.htmlhttp://www.blogjava.net/killme2008/comments/191022.htmlhttp://www.blogjava.net/killme2008/archive/2008/04/06/191022.html#feedback0http://www.blogjava.net/killme2008/comments/commentrss/191022.htmlhttp://www.blogjava.net/killme2008/services/trackbacks/191022.html     模块化代码的首要特质就是封装。封装良好的模块不会过多向外部披露自身的细节,不会直接调用其它模块的实现码,也不会胡乱共享全局数据。模块之间通过应用程序编程接口(api)——一组严密、定义良好的程序调用和数据结构来通信。这就是模块化原则的内容。api在模块间扮演双重角色。在实现层面,作为模块之间的滞塞点(choke point),阻止各自的内部细节被相邻模块知晓;在设计层面,正是api(而不是模块间的实现代码)真正定义了整个体系。
    模块的最佳模块大小,逻辑行在200到400之间,物理行在400到800之间为最佳。模块太小,几乎所有的复杂度都集中在接口,同样不利于理解,也就是透明性的欠缺。
   紧凑性就是一个特性能否装进人脑中的特性。理解紧凑性可以从它的“反面”来理解,紧凑性不等于“薄弱”,如果一个设计构建在易于理解且利于组合的抽象概念上,则这个系统能在具有非常强大、灵活的功能的同时保持紧凑。紧凑也不等同于“容易学习”:对于某些紧凑 设计而言,在掌握其精妙的内在基础概念模型之前,要理解这个设计相当困难;但一旦理解了这个概念模型,整个视角就会改变,紧凑的奥妙也就十分简单了。紧凑也不意味着“小巧”。即使一个设计良好的系统,对有经验的用户来说没什么特异之处、“一眼”就能看懂,但仍然可能包含很多部分。
    评测一个api紧凑性的经验法则是:api的入口点通常在7个左右,或者按《代码大全2》的说法,7 2和7-2的范围内。
    如果两个或更多事物中的一个发生变化,不会影响其他事物,这些事物就是正交的。每一个动作只改变一件事,不会影响其他(没有其他副作用)。举个例子,比如读取配置文件,获得系统设置信息这个方法:
module config
   
def self.load_config
         config_str
=file.open("r","config.xml").read
         
#解析配置文件,可能转化成xml dom处理等
         
   end
end
这个方法想当然地认为配置文件存储于磁盘文件中,然而配置文件完全是有可能通过网络获取的,也就是说文件句柄未必来源于磁盘文件,这个方法承担了两个职责:获取配置数据和解析配置数据。重构一下,以提高正交性:
module config
   
def self.load_config(io)
         config_str
=io.read
         parse_config(config_str)         
   end
   private
   
def self.parse_config(config_str)
    
#解析
   end 
end
    重构技术中的很多坏味道,特别是重复代码,是违反正交性的明显例子,“重构的原则性目标就是提高正交性”。
    dry(don't repeat yourself)原则,意思是说:任何一个知识点在系统内都应当有一个唯一、明确、权威的表述。这个原则的另一种表述就是所谓spot原则(single point of truth)——真理的单点性。
    提高设计的紧凑性,有一个精妙但强大的方法,就是围绕“解决一个定义明确的问题”的强核心算法组织设计,避免臆断和捏造。



dennis 2008-04-06 13:00
]]>
singleton迷恋http://www.blogjava.net/killme2008/archive/2008/02/23/181596.htmldennisdennissat, 23 feb 2008 07:34:00 gmthttp://www.blogjava.net/killme2008/archive/2008/02/23/181596.htmlhttp://www.blogjava.net/killme2008/comments/181596.htmlhttp://www.blogjava.net/killme2008/archive/2008/02/23/181596.html#feedback5http://www.blogjava.net/killme2008/comments/commentrss/181596.htmlhttp://www.blogjava.net/killme2008/services/trackbacks/181596.html 1、仅仅在一个地方调用到了某个singleton实例,并且对这个实例的处理代码也集中在这么一两个地方,这样的情况下你为什么要singleton?这里需要一个个全局访问点吗?我看你是为了singleton而singleton。

2、我为了性能优化啊!singleton只有一个实例,减小了创建开销。oh,我终于找到一个用singleton的充分理由了——性能。慢着,跟我读高大师的名言:“不成熟的优化是万恶之源”。你怎么知道singleton对象的重复创建是明显影响了性能?现代jvm对“短命”对象的创建代价已经非常低了。不成熟的优化不仅可能是无效的,而且也给以后重构工作带来了困难。除非有明显数据证明(分析工具而来)某个对象的重复创建是对性能影响极大,否则所谓性能优化不能成为采用singleton模式的理由。

3、有时候我们需要在系统的不同层次间传递一些共享信息,如果不采用singleton对象来提供这些共享信息,就得在调用的方法中重复地传递这些参数,这是个应用singleton模式的场景。但是,如果这些共享信息是可被修改的,或者说singleton对象不是无状态的,如果还采用singleton模式,那么你就不得不在调用的方法中从single对象取出旧信息和存入新信息,这样的重复代码将遍布的到处都是,不仅仅引入了同步访问的需要,而且出错的风险大大提高。这种情况下你还将这些信息作为方法参数传递而不是采用singleton可能更为清晰和健壮。

    singleton不仅仅是“保证一个类仅有一个实例”(这仅仅是描述),更重要的是它是用来“提供全局访问点”的(这才是它的功能),不要再迷恋这把锤子,好好利用这把锤子。

题外话:脚本语言似乎更容易滥用全局变量,javascript里可以模拟命名空间,ruby也可以模拟类似的机制。最近写的一个比较大一点的ruby脚本,用了几个全局变量(都是数组)用于保存状态数据,一开始没有意识到这一点,导致对全局变量的访问散落在好几个脚本文件里,rdt下看起来红通通的一片极其不爽。那么就重构吧——封装数组重构,将对这些全局数组的访问和修改操作统一到一个模块,调用全局变量的地方都引用这个模块,通过模块去操作全局变量,代码看起来清爽多了。




dennis 2008-02-23 15:34
]]>
the future of the software developmenthttp://www.blogjava.net/killme2008/archive/2007/10/25/155788.htmldennisdennisthu, 25 oct 2007 02:11:00 gmthttp://www.blogjava.net/killme2008/archive/2007/10/25/155788.htmlhttp://www.blogjava.net/killme2008/comments/155788.htmlhttp://www.blogjava.net/killme2008/archive/2007/10/25/155788.html#feedback4http://www.blogjava.net/killme2008/comments/commentrss/155788.htmlhttp://www.blogjava.net/killme2008/services/trackbacks/155788.html阅读全文

dennis 2007-10-25 10:11
]]>
谈nullobject模式http://www.blogjava.net/killme2008/archive/2007/07/31/133628.htmldennisdennistue, 31 jul 2007 09:48:00 gmthttp://www.blogjava.net/killme2008/archive/2007/07/31/133628.htmlhttp://www.blogjava.net/killme2008/comments/133628.htmlhttp://www.blogjava.net/killme2008/archive/2007/07/31/133628.html#feedback7http://www.blogjava.net/killme2008/comments/commentrss/133628.htmlhttp://www.blogjava.net/killme2008/services/trackbacks/133628.html
if(prj.getprojectid==null)
    plan.setcost(
0.0);
else
    plan.setcost(prj.getcost());

   我们在很多地方有类似的检查对象是否为null,如果为null,需要一个默认值等等这样的场景。显然,代码重复是坏味道,怎么消除这个坏味道呢?答案就是使用nullobject替代之,null object继承原对象。
class nullproject extends project{
   
public boolean isnull(){
      
return true;
   }
}
class project{
   
private double cost;
   
private string projectid;
   .
   
public boolean isnull(){
        
return false;
   }
}

那么,原来的代码可以改写为:
if(prj.isnull())
    plan.setcost(
0.0);
else
    plan.setcost(prj.getcost());

    如果null object的引入仅仅是带来这个好处,似乎没有理由让我们多敲这么多键盘。问题的关键是类似上面这样的判断也许出现在很多处,那么有价值的技巧出现了,我们在nullobject覆写getcost,提供缺省值:
class nullproject extends project{
   
public boolean isnull(){
      
return true;
   }
   
public double getcost(){
      
return 0.0;      
   }
}
    因此,检查对象是否为null的代码可以去掉if...else了:
plan.setcost(prj.getcost());

    请注意,只有那些大多数客户端代码都要求null object做出相同响应时,这样的行为才有意义。比如我们这里当工程id为null,很多地方要求费用就默认为0.0。 特殊的行为我们仍然使用isnull进行判断。
    当然,另外在需要返回nullobject的地方,你应该创建一个null object以替代一般的对象,我们可以建立一个工厂方法:

class project{
   
private double cost;
   
private string projectid;
   .
   
public boolean isnull(){
        
return false;
   }
   
public project createnullproject(){
        
return new nullproject();
   }
}

   null object模式带来的好处:减少了检查对象是否为null的代码重复,提高了代码的可读性,通常这些null object也可以为单元测试带来简便。



dennis 2007-07-31 17:48
]]>
junit源码分析(四)——从decorator模式说起http://www.blogjava.net/killme2008/archive/2007/04/06/108894.htmldennisdennisfri, 06 apr 2007 04:39:00 gmthttp://www.blogjava.net/killme2008/archive/2007/04/06/108894.htmlhttp://www.blogjava.net/killme2008/comments/108894.htmlhttp://www.blogjava.net/killme2008/archive/2007/04/06/108894.html#feedback3http://www.blogjava.net/killme2008/comments/commentrss/108894.htmlhttp://www.blogjava.net/killme2008/services/trackbacks/108894.html    decorator模式,也就是装饰器模式,是对象结构型模式之一。

1.意图:动态地给一个对象添加一些额外的职责。给对象添加功能,我们首先想到的是继承,但是如果每增一个功能都需要继承,类的继承体系将无可避免地变的庞大和难以理解。面向对象设计的原则:优先使用组合,而非继承,继承的层次深度最好不过三。

2.适用场景:
1)在不影响其他对象的情况下,以动态、透明的方式给单个对象添加额外的责任
2)处理可以撤销的职责
3)为了避免类的数目爆炸,或者不能采用生成子类的方法进行扩展时

3.uml图和协作:



component——定义一个对象接口,可以给这些对象动态地添加职责

concretecomponent——定义一个对象,可以给这个对象添加职责

decorator——维持一个指向component的引用,并定义一个与component一致的接口,作为装饰类的父类

concretedecorator——具体装饰类

4.效果:
1)与静态继承相比,decorator可以动态添加职责,更为灵活
2)避免产生复杂的类,通过动态添加职责,而不是一次性提供一个万能的接口
3)缺点是将产生比较多的小对象,对学习上有难度,显然,java.io就是这个问题

我们以一个例子来实现decorator模式,假设这样一个场景:在某个应用中需要打印票据,我们写了一个printticket接口,然后提供一个实现类(defaultprintticket)实现打印的功能:

package com.rubyeye.design_pattern.decorator;
//抽象component接口
public interface printticket {
    
public void print();
}


//默认实现类,打印票据
package com.rubyeye.design_pattern.decorator;

public class defaultprintticket implements printticket {

    
public void print() {
        system.out.println(
"ticket body");
    }

}

ok,我们的功能已经实现,我们还体现了针对接口编程的原则,替换一个新的打印方式很灵活,但是客户开始提需求了——人生无法避免的三件事:交税、死亡和需求变更。客户要求打印页眉,你首先想到的是继承:
package com.rubyeye.design_pattern.decorator;

public class anotherprintticket implements printticket {

    
public void print() {
        system.out.println(
"ticket header");
        system.out.println(
"ticket body");
    }

}

请注意,我们这里只是简单的示例,在实际项目中也许意味着添加一大段代码,并且需要修改打印票据本体的功能。需求接踵而至,客户要求添加打印页码,要求增加打印花纹,要求可以联打......你的类越来越庞大,直到你看见这个类都想吐的地步!-_-。让我们看看另一个方案,使用decorator模式来动态地给打印增加一些功能,首先是实现一个decorator,它需要保持一个到printticket接口的引用:

package com.rubyeye.design_pattern.decorator;

public class printticketdecorator implements printticket {

    
protected printticket printticket;

    
public printticketdecorator(printticket printticket) {
        
this.printticket = printticket;
    }

    
//默认调用printticket的print
    public void print() {
        printticket.print();
    }

}

然后,我们实现两个具体的装饰类——打印页眉和页脚:
package com.rubyeye.design_pattern.decorator;

public class headerprintticket extends printticketdecorator {

    
public headerprintticket(printticket printticket){
        
super(printticket);
    }
    
    
public void print() {
        system.out.println(
"ticket header");
        
super.print();
    }
}

package com.rubyeye.design_pattern.decorator;

public class footerprintticket extends printticketdecorator {
    
public footerprintticket(printticket printticket) {
        
super(printticket);
    }

    
public void print() {
        
super.print();
        system.out.println(
"ticket footer");
    }
}

    使用起来也很容易:
   
package com.rubyeye.design_pattern.decorator;

public class decoratortest {

    
/**
     * 
@param args
     
*/
    
public static void main(string[] args) {
        printticket print
=new headerprintticket(new footerprintticket(new defaultprintticket()));
        print.print();
    }

}

输出:
ticket header
ticket body
ticket footer

    了解了decorator模式,我们联系了下junit里面的应用。作为一个测试框架,应该方便地支持二次开发,也许用户开发自己的testcase,添加自定义的功能,比如执行重复测试、多线程测试等等。动态添加职责,而又不想使用静态继承,这正是decorator使用的地方。在junit.extensions包中有一个testdecorator,正是所有装饰类的父类,也是作为二次开发的基础,它实现了test接口,而test接口就是我们定义的抽象接口:
public class testdecorator implements  test {
    
//保有一个指向test的引用
        protected test ftest;
       
    
public testdecorator(test test) {
        ftest
= test;
    }
        
        
public void basicrun(testresult result) {
          ftest.run(result);
        }
        
public void run(testresult result) {
           basicrun(result);
        }
        
}

junit已经提供了两个装饰类:junit.extensions.activetest用于处理多线程,junit.extensions.repeatedtest用于执行重复测试,看看repeatedtest是怎么实现的:
public class repeatedtest extends  testdecorator {
        
//重复次数
    private int ftimesrepeat;

    
public repeatedtest(test test, int repeat) {
        
super(test);
        ftimesrepeat
= repeat;
    }
        
public void run(testresult result) {
        
//重复执行
                for (int i= 0; i < ftimesrepeat; i) {
            
if (result.shouldstop())
                
break;
            
super.run(result);
        }
    }
        
}

    repeatedtest继承testdecorator ,覆写run(testreult result)方法,重复执行,super.run(result)将调用传入的testcase的run(testresult result)方法,这已经在testdecorator默认实现。看看使用方式,使用装饰模式的好处不言而喻。

testsuite suite = new testsuite();
suite.addtest(
new testsetup(new repeatedtest(new testmath("testadd"),12)));



dennis 2007-04-06 12:39
]]>
junit源码分析 (三)——template method模式http://www.blogjava.net/killme2008/archive/2007/04/06/108860.htmldennisdennisfri, 06 apr 2007 01:54:00 gmthttp://www.blogjava.net/killme2008/archive/2007/04/06/108860.htmlhttp://www.blogjava.net/killme2008/comments/108860.htmlhttp://www.blogjava.net/killme2008/archive/2007/04/06/108860.html#feedback0http://www.blogjava.net/killme2008/comments/commentrss/108860.htmlhttp://www.blogjava.net/killme2008/services/trackbacks/108860.html    如果你查看下testcase方法,你会发现testcase和testsuite的run()方法都是将执行测试的任务委托给了testresult,由testresult去执行测试代码并收集测试过程中的信息(这里用到了collecting parameter模式)。
   
    public testresult run() {
        testresult result
= createresult();
        run(result);
        
return result;
    }
    
/**
     * runs the test case and collects the results in testresult.
     * this is the template method that defines the control flow
     * for running a test case.
     
*/
    
public void run(testresult result) {
        result.run(
this);
    }
   
    我们直接找到testresult,看看它的run方法:
/**
     * runs a testcase.
     
*/
    
protected void run(final testcase test) {
        starttest(test);
        protectable p 
= new protectable() {
            
public void protect() throws throwable {
                test.runbare();
            }
        };
        runprotected(test, p);
        endtest(test);
    }

    这里实例化了一个内部类,内部类实现了protectable接口的 protect()方法,并执行传入的testcase的runbare()方法,显然,真正的测试代码在testcase的runbare()方法中,让我们来看下:


        //将被子类实现
    protected void setup() throws throwable {
    }
    
//同上,将被具体的testcase实现
    protected void teardown() throws throwable {
    }
     /**
     * 模板方法
     * runs the bare test sequence.
     * 
@exception throwable if any exception is thrown
     
*/
    
public void runbare() throws throwable {
        setup();
        
try {
            runtest();
        }
        
finally {
            teardown();
        }
    }

真相水落石出,对于每一个测试方法,都遵循这样的模板:setup->执行测试 runtest()->teardown。这正是模板方式模式的一个应用例子。什么是template method模式呢?

template method模式

类行为模式的一种
1.意图:定义一个操作中的算法的骨架,而将一些延迟步骤到子类中。template method使得子类可以不改变一个算法的结构即可重定义该算法的某些步骤。
2.适用场景:
1)一次性实现算法的不变部分(基本骨架),将可变的行为留给子类来完成
2)子类中的公共部分(比如junit中的初始化和清理)被抽取到一个公共父类中以避免代码重复。
3)控制了子类的扩展,这里其实也有类似回调函数的性质,具体步骤先在骨架中注册,在具体执行时被回调。

3.uml图和结构
   
  抽象父类定义了算法的基本骨架(模板方法),而不同的子类实现具体的算法步骤,客户端由此可以与算法的更改隔离。

4.效果:
1)模板方法是代码复用的基本技术,在类库中经常使用,可以减少大量的代码重复
2)通过隔离算法的不变和可变部分,增加了系统的灵活性,扩展算法的某些步骤将变的很容易。

    了解了template method模式之后,让我们回到junit的源码,看看runtest()方法,这里主要应用的是java的反射技术,对于学习反射技术的有参考价值:
protected void runtest() throws throwable {
        method runmethod
= null;
        
try {
            runmethod
= getclass().getdeclaredmethod(fname, new class[0]);
        } 
catch (nosuchmethodexception e) {
            fail(
"method \"" fname "\" not found");
        }
        
if (runmethod != null && !modifier.ispublic(runmethod.getmodifiers())) {
            fail(
"method \"" fname "\" should be public");
        }

        
try {
            runmethod.invoke(
thisnew class[0]);
        }
        
catch (invocationtargetexception e) {
            e.fillinstacktrace();
            
throw e.gettargetexception();
        }
        
catch (illegalaccessexception e) {
            e.fillinstacktrace();
            
throw e;
        }
    }

   


dennis 2007-04-06 09:54
]]>
junit源码分析(二)——观察者模式http://www.blogjava.net/killme2008/archive/2007/04/05/108743.htmldennisdennisthu, 05 apr 2007 09:19:00 gmthttp://www.blogjava.net/killme2008/archive/2007/04/05/108743.htmlhttp://www.blogjava.net/killme2008/comments/108743.htmlhttp://www.blogjava.net/killme2008/archive/2007/04/05/108743.html#feedback0http://www.blogjava.net/killme2008/comments/commentrss/108743.htmlhttp://www.blogjava.net/killme2008/services/trackbacks/108743.html    我们知道junit支持不同的使用方式:swt、swing的ui方式,甚至控制台方式,那么对于这些不同的ui我们如何提供统一的接口供它们获取测试过程的信息(比如出现的异常信息,测试成功,测试失败的代码行数等等)?我们试想一下这个场景,当一个error或者exception产生的时候,测试能够马上通知这些ui客户端:发生错误了,发生了什么错误,错误是什么等等。显而易见,这是一个订阅-发布机制应用的场景,应当使用观察者模式。那么什么是观察者模式呢?

观察者模式(observer)

observer是对象行为型模式之一

1.意图:定义对象间的一种一对多的依赖关系,当一个对象的状态发现改变时,所有依赖于它的对象都得到通知并被自动更新

2.适用场景:
1)当一个抽象模型有两个方面,其中一个方面依赖于另一个方面,通过观察者模式将这两者封装在不同的独立对象当中,以使它们可以独立的变化和复用
2)当一个对象改变时,需要同时改变其他对象,并且不知道其他对象的具体数目
3)当一个对象需要引用其他对象,但是你又不想让这个对象与其他对象产生紧耦合的时候

3.uml图:
                 
   subject及其子类维护一个观察者列表,当需要通知所有的observer对象时调用nitify方法遍历observer集合,并调用它们的update方法更新。而具体的观察者实现observer接口(或者抽象类),提供具体的更新行为。其实看这张图,与bridge有几分相似,当然两者的意图和适用场景不同。

4.效果:
1)目标和观察者的抽象耦合,目标仅仅与抽象层次的简单接口observer松耦合,而没有与具体的观察者紧耦合
2)支持广播通信
3)缺点是可能导致意外的更新,因为一个观察者并不知道其他观察者,它的更新行为也许将导致一连串不可预测的更新的行为

5.对于观察者实现需要注意的几个问题:
1)谁来触发更新?最好是由subject通知观察者更新,而不是客户,因为客户可能忘记调用notify
2)可以通过显式传参来指定感兴趣的更新
3)在发出通知前,确保subject对象状态的一致性,也就是notify操作应该在最后被调用
4)当subject和observer的依赖关系比较复杂的时候,可以通过一个更新管理器来管理它们之间的关系,这是与中介者模式的结合应用。


    讨论完观察者模式,那我们来看junit是怎么实现这个模式的。在junit.framework包中我们看到了一个observer接口——testlistener,看看它的代码:
package junit.framework;

/**
 * a listener for test progress
 
*/
public interface testlistener {
   
/**
     * an error occurred.
     
*/
    
public void adderror(test test, throwable t);
   
/**
     * a failure occurred.
     
*/
     
public void addfailure(test test, throwable t);  
   
/**
     * a test ended.
     
*/
     
public void endtest(test test); 
   
/**
     * a test started.
     
*/
    
public void starttest(test test);
}

    接口清晰易懂,就是一系列将测试过程的信息传递给观察者的操作。具体的子类将接受这些信息,并按照它们的方式显示给用户。
     比如,我们看看swing的ui中的testrunner,它将这些信息显示在一个swing写的ui界面上:
    public void starttest(test test) {
        showinfo(
"running: "test);
    }

    public void adderror(test test, throwable t) {
        fnumberoferrors.settext(integer.tostring(ftestresult.errorcount()));
        appendfailure(
"error", test, t);
    }
    
public void addfailure(test test, throwable t) {
        fnumberoffailures.settext(integer.tostring(ftestresult.failurecount()));
        appendfailure(
"failure", test, t);
    }
    public void endtest(test test) {
        setlabelvalue(fnumberofruns, ftestresult.runcount());
        fprogressindicator.step(ftestresult.wassuccessful());
    }

可以看到,它将错误信息,异常信息保存在list或者vector集合内,然后显示在界面上:
private void showerrortrace() {
        
int index= ffailurelist.getselectedindex();
        
if (index == -1)
            
return;
    
        throwable t
= (throwable) fexceptions.elementat(index);
        
if (ftraceframe == null) {
            ftraceframe
= new traceframe();
            ftraceframe.setlocation(
100100);
           }
        ftraceframe.showtrace(t);
        ftraceframe.setvisible(
true);
    }
    
private void showinfo(string message) {
        fstatusline.setfont(plain_font);
        fstatusline.setforeground(color.black);
        fstatusline.settext(message);
    }
    
private void showstatus(string status) {
        fstatusline.setfont(bold_font);
        fstatusline.setforeground(color.red);
        fstatusline.settext(status);
    }

而junit中的目标对象(subject)就是testresult对象,它有添加观察者的方法:

/**
     * registers a testlistener
     
*/
    
public synchronized void addlistener(testlistener listener) {
        flisteners.addelement(listener);
    }

而通知观察者又是怎么做的呢?请看这几个方法,都是循环遍历观察者列表,并调用相应的更新方法:

/**
     * adds an error to the list of errors. the passed in exception caused the
     * error.
     
*/
    
public synchronized void adderror(test test, throwable t) {
        ferrors.addelement(
new testfailure(test, t));
        
for (enumeration e = flisteners.elements(); e.hasmoreelements();) {
            ((testlistener) e.nextelement()).adderror(test, t);
        }
    }

    
/**
     * adds a failure to the list of failures. the passed in exception caused
     * the failure.
     
*/
    
public synchronized void addfailure(test test, assertionfailederror t) {
        ffailures.addelement(
new testfailure(test, t));
        
for (enumeration e = flisteners.elements(); e.hasmoreelements();) {
            ((testlistener) e.nextelement()).addfailure(test, t);
        }
    }

    
/**
     * registers a testlistener
     
*/
    
public synchronized void addlistener(testlistener listener) {
        flisteners.addelement(listener);
    }

    
/**
     * informs the result that a test was completed.
     
*/
    
public synchronized void endtest(test test) {
        
for (enumeration e = flisteners.elements(); e.hasmoreelements();) {
            ((testlistener) e.nextelement()).endtest(test);
        }
    }


使用这个模式后带来的好处:
1)上面提到的subject与observer的抽象耦合,使junit可以支持不同的使用方式
2)支持了广播通信,目标对象不关心有多少对象对自己注册,它只是通知注册的观察者

最后,我实现了一个简单的consolerunner,在控制台执行junit,比如我们写了一个简单测试:
package junit.samples;

import junit.framework.*;

/**
 * some simple tests.
 * 
 
*/
public class simpletest extends testcase {
    
protected int fvalue1;

    
protected int fvalue2;

    
public simpletest(string name) {
        
super(name);
    }

    
public void setup() {
        fvalue1 
= 2;
        fvalue2 
= 3;
    }

    
public void testadd() {
        
double result = fvalue1  fvalue2;
        
assert(result == 5);
    }


    
public void testequals() {
        assertequals(
1212);
        assertequals(
12l12l);
        assertequals(
new long(12), new long(12));

        assertequals(
"size"1212);
        assertequals(
"capacity"12.011.990.01);
    }
}

使用consolerunner调用这个测试,代码很简单,不多做解释了:
package net.rubyeye.junit.framework;

import java.lang.reflect.method;
import java.util.arraylist;
import java.util.list;
import java.util.vector;

import junit.framework.test;
import junit.framework.testlistener;
import junit.framework.testresult;
import junit.samples.simpletest;
//实现观察者接口
public class consolerunner implements testlistener {

    
private testresult ftestresult;

    
private vector fexceptions;

    
private vector ffailedtests;

    
private list ffailurelist;

    
public consolerunner() {
        fexceptions 
= new vector();
        ffailedtests 
= new vector();
        ffailurelist 
= new arraylist();
    }

    
public void endtest(test test) {
        system.out.println(
"测试结束:");
        string message 
= test.tostring();
        
if (ftestresult.wassuccessful())
            system.out.println(message 
 " 测试成功!");
        
else if (ftestresult.errorcount() == 1)
            system.out.println(message 
 " had an error");
        
else
            system.out.println(message 
 " had a failure");

        
for (int i = 0; i < ffailurelist.size(); i) {
            system.out.println(ffailurelist.get(i));
        }
        
for (int i = 0; i < ffailedtests.size(); i) {
            system.out.println(ffailurelist.get(i));
        }
        
for (int i = 0; i < fexceptions.size(); i) {
            system.out.println(ffailurelist.get(i));
        }

        system.out.println(
"------------------------");
    }

    
public void starttest(test test) {
        system.out.println(
"开始测试:"  test);
    }

    
public static testresult createtestresult() {
        
return new testresult();
    }

    
private string truncatestring(string s, int length) {
        
if (s.length() > length)
            s 
= s.substring(0, length)  "";
        
return s;
    }

    
public void adderror(test test, throwable t) {
        system.out.println(ftestresult.errorcount());
        appendfailure(
"error", test, t);
    }

    
public void addfailure(test test, throwable t) {
        system.out.println(ftestresult.failurecount());
        appendfailure(
"failure", test, t);
    }

    
private void appendfailure(string kind, test test, throwable t) {
        kind 
= ""  test;
        string msg 
= t.getmessage();
        
if (msg != null) {
            kind 
= ":"  truncatestring(msg, 100);
        }
        ffailurelist.add(kind);
        fexceptions.addelement(t);
        ffailedtests.addelement(test);
    }

    
public void go(string args[]) {
        method[] methods 
= simpletest.class.getdeclaredmethods();

        
for (int i = 0; i < methods.length; i) {
            
//取所有以test开头的方法
            if (methods[i].getname().startswith("test")) {
                test test 
= new simpletest(methods[i].getname());
                ftestresult 
= createtestresult();
                ftestresult.addlistener(consolerunner.
this);
                
//执行测试
                test.run(ftestresult);
            }
        }

    }

    
public static void main(string args[]) {
        
new consolerunner().go(args);
    }

}



dennis 2007-04-05 17:19
]]>
junit源码分析(一)——command模式和composite模式http://www.blogjava.net/killme2008/archive/2007/04/05/108702.htmldennisdennisthu, 05 apr 2007 07:10:00 gmthttp://www.blogjava.net/killme2008/archive/2007/04/05/108702.htmlhttp://www.blogjava.net/killme2008/comments/108702.htmlhttp://www.blogjava.net/killme2008/archive/2007/04/05/108702.html#feedback3http://www.blogjava.net/killme2008/comments/commentrss/108702.htmlhttp://www.blogjava.net/killme2008/services/trackbacks/108702.html    我读的是junit3.0的源码,目前junit已经发布到4.0版本了,尽管有比较大的改进,但基本的骨架不变,读3.0是为了抓住重点,省去对旁支末节的关注。我们来看看junit的核心代码,也就是junit.framework包,除了4个辅助类(assert,assertfailederror,protectable,testfailure),剩下的就是我们需要重点关注的了。我先展示一张uml类图:

    我们先不去关注testdecorator类(此处是decorator模式,下篇文章再讲),看看test接口,以及它的两个实现类testcase和testsuite。很明显,此处用到了command模式,为什么要使用这个模式呢?让我们先来看看什么是command模式。

command模式

command模式是行为型模式之一

1.意图:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。
2.适用场景:
1)抽象出待执行的动作以参数化对象,command模式是回调函数的面向对象版本。回调函数,我想大家都明白,函数在某处注册,然后在稍后的某个时候被调用。
2)可以在不同的时刻指定、排列和执行请求。
3)支持修改日志,当系统崩溃时,这些修改可以被重做一遍。
4)通过command模式,你可以通过一个公共接口调用所有的事务,并且也易于添加新的事务。


3。uml图:
   

4.效果:
1)命令模式将调用操作的对象与如何实现该操作的对象解耦。
2)将命令当成一个头等对象,它们可以像一般对象那样进行操纵和扩展
3)可以将多个命令复合成一个命令,与composite模式结合使用
4)增加新的命令很容易,隔离对现有类的影响
5)可以与备忘录模式配合,实现撤销功能。

    在了解了command模式之后,那我们来看junit的源码,test接口就是命令的抽象接口,而testcase和testsuite是具体的命令
//抽象命令接口
package junit.framework;

/**
 * a test can be run and collect its results.
 *
 * 
@see testresult
 
*/
public interface test {

    
/**
     * counts the number of test cases that will be run by this test.
     
*/
    
public abstract int counttestcases();
    
/**
     * runs a test and collects its result in a testresult instance.
     
*/
    
public abstract void run(testresult result);
}

//具体命令一

public abstract class testcase extends assert implements test {
    
/**
     * the name of the test case
     
*/
    
private final string fname;
    
/**
   

//具体命令二

public class testsuite implements test {
     

由此带来的好处:
1.客户无需使用任何条件语句去判断测试的类型,可以用统一的方式调用测试和测试套件,解除了客户与具体测试子类的耦合
2.如果要增加新的testcase也很容易,实现test接口即可,不会影响到其他类。
3.很明显,testsuite是通过组合多个testcase的复合命令,这里使用到了composite模式(组合)
4.尽管未实现redo和undo操作,但将来也很容易加入并实现。

    我们上面说到testsuite组合了多个testcase,应用到了composite模式,那什么是composite模式呢?具体来了解下。

composite模式

composite模式是对象结构型模式之一。
1.意图:将对象组合成树形结构以表示“部分——整体”的层次结构。使得用户对单个对象和组合结构的使用具有一致性。

2.适用场景:
1)想表示对象的部分-整体层次
2)希望用户能够统一地使用组合结构和单个对象。具体到junit源码,我们是希望用户能够统一地方式使用testcase和testsuite

3.uml图:

      

图中单个对象就是树叶(leaf),而组合结构就是compoiste,它维护了一个leaf的集合。而component是一个抽象角色,给出了共有接口和默认行为,也就是junit源码中的test接口。

4.效果:
1)定义了基本对象和组合对象的类层次结构,通过递归可以产生更复杂的组合对象
2)简化了客户代码,客户可以使用一致的方式对待单个对象和组合结构
3)添加新的组件变的很容易。但这个会带来一个问题,你无法限制组件中的组件,只能靠运行时的检查来施加必要的约束条件

    具体到junit源码,单个对象就是testcase,而复合结构就是testsuite,test是抽象角色只有一个run方法。testsuite维护了一个testcase对象的集合ftests:

     private vector ftests= new vector(10); 
      
/**
     * adds a test to the suite.
     
*/
    
public void addtest(test test) {
        ftests.addelement(test);
    }
    /**
     * runs the tests and collects their result in a testresult.
     
*/
    
public void run(testresult result) {
        
for (enumeration e= tests(); e.hasmoreelements(); ) {
              
if (result.shouldstop() )
                  
break;
            test test
= (test)e.nextelement();
            test.run(result);
        }
    }

当执行run方法时遍历这个集合,调用里面每个testcase对象的run()方法,从而执行测试。我们使用的时候仅仅需要把testcase添加到集合内,然后用一致的方式(run方法)调用他们进行测试。

考虑使用composite模式之后带来的好处:
1)junit可以统一地处理组合结构testsuite和单个对象testcase,避免了条件判断,并且可以递归产生更复杂的测试对象
2)很容易增加新的testcase。


参考资料:《设计模式——可复用面向对象软件的基础》
          《junit设计模式分析》 刘兵
          junit源码和文档









    


dennis 2007-04-05 15:10
]]>
创建型模式摘记http://www.blogjava.net/killme2008/archive/2007/03/17/104485.htmldennisdennissat, 17 mar 2007 09:01:00 gmthttp://www.blogjava.net/killme2008/archive/2007/03/17/104485.htmlhttp://www.blogjava.net/killme2008/comments/104485.htmlhttp://www.blogjava.net/killme2008/archive/2007/03/17/104485.html#feedback1http://www.blogjava.net/killme2008/comments/commentrss/104485.htmlhttp://www.blogjava.net/killme2008/services/trackbacks/104485.html
创建型模式属于对象创建模型。所谓对象创建模型就是说将实例化的工作委托给另一个对象来做。与之相对应的是类创建模型,这是一种通过继承改变被实例化的类。
       创建型模式有两个重要的特点:
1) 客户不知道创建的具体类是什么(除非看源代码)
2) 隐藏了类的实例是如何被创建和放在一起的

一。抽象工厂模式
1.意图:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们的具体的类。
2.适用场景:
1)一个系统要独立于它的产品的创建、组合和表示时
2)一个系统要由多个产品系列中的一个来配置时
3)当你要强调一系列相关的产品对象的设计以便进行联合使用时
4)当你提供一个产品类库,而只想显示它们的接口而不是实现时

3.uml图——结构

4.效果:
1)分离了具体的类,通过抽象接口将客户与具体的类分离
2)易于交换产品系列
3)有利于产品的一致性
4)难以支持新种类的产品,比如我们现在有一个productc产品,我们需要增加类abstractproductc,增加abstractfactory:: creanteproductc方法,并且两个产品系列的实际创建者concreatefactory1、concreatefactor2都要实现该方 法。
可以通过给方法加参数的方式来指明创建的是什么产品,这样客户代码就无需改变,只要传递不同的参数。abstractfactory类只需要提供一个createproduct(const string& name)方法即可。

5.代码实现,以《深入浅出设计模式(java c#)》的动物工厂为例:
using system;
namespace animalworld
{
    
// 抽象大陆工厂
    abstract class continentfactory
    {
        
abstract public herbivore createherbivore();
        
abstract public carnivore createcarnivore();
    }
    
//非洲大陆,有角马,狮子
    class africafactory : continentfactory
    {
        
override public herbivore createherbivore()
        {
            
return new wildebeest();
        }
        
override public carnivore createcarnivore()
        {
            
return new lion();
        }
    }
    
// 美洲大陆,有狼,野牛
    class americafactory : continentfactory
    {
        
override public herbivore createherbivore()
        {
            
return new bison();
        }
        
override public carnivore createcarnivore()
        {
            
return new wolf();
        }
    }
    
//食草动物"
    abstract class herbivore
    {
    }
    
//肉食动物"
    abstract class carnivore
    {
        
//猎食食草动物的方法
        abstract public void eat( herbivore h );
    }
    
//角马
    class wildebeest : herbivore
    {
    }
    
//狮子"
    class lion : carnivore
    {
        
//重载猎食食草动物的方法
        override public void eat( herbivore h )
        {
            console.writeline( 
this  " eats "  h );
        }
    }
    
//野牛
    class bison : herbivore
    {
    }
    
//
    class wolf : carnivore
    {
        
//重载猎食食草动物的方法
        override public void eat( herbivore h )
        {
            console.writeline( 
this  " eats "  h );
        }
    }
    
//动物世界类
    class animalworld
    {
        
private herbivore herbivore;
        
private carnivore carnivore;
        
// 创建两种动物分类
        public animalworld( continentfactory factory )
        {
            carnivore 
= factory.createcarnivore();
            herbivore 
= factory.createherbivore();
        }
        
//运行食物链
        public void runfoodchain()
        {
            
//肉食动物猎食食草动物
            carnivore.eat( herbivore );
        }
    }
    
/// 
    
/// 抽象工厂模式客户应用测试
    
/// 

    class gameapp
    {
        [stathread]
        
static void main(string[] args)
        {
            
//创造并运行非洲动物世界
            continentfactory africa = new africafactory();
            animalworld world 
= new animalworld( africa );
            world.runfoodchain();
            
//创造并运行美洲动物世界
            continentfactory america = new americafactory();
            world 
= new animalworld( america );
            world.runfoodchain();
            console.read();
        }

    }
}

二。builder模式
1.意图:将一个复杂对象的构建与它的表示相分离,使得同样的构建过程可以创建不同的表示(或者说产品)

2.适用场景:
1)当创建复杂对象的算法应该独立于改对象的组成部分以及它们的装配方式时
2)当构造过程必须允许被构造的对象有不同的表示时

3.uml图——结构


director接受一个builder子类对象,完成创建过程,并通知builder对象返回以及构造好的产品。

4.效果:
1)可以使你改变一个对象的内部表示
2)构造代码和表示代码分开
3)可以对构造过程进行更精细的控制

5。实现:以一个车辆建造过程为例(c#)
using system;

namespace carshop
{
    
using system;
    
using system.collections;
    
//指挥者,director
    class shop{
        
public void construct( vehiclebuilder vehiclebuilder ){
            vehiclebuilder.buildframe();
            vehiclebuilder.buildengine();
            vehiclebuilder.buildwheels();
            vehiclebuilder.builddoors();
        }
    }
    
/* "builder 建造者",builder
    抽象建造者具有四种方法
    装配框架
    装配发动机
    装配轮子
    装配车门
    
*/
    
abstract class vehiclebuilder
    {
        
protected vehicle vehicle;
        
//返回建造完成的车辆
        public vehicle vehicle{
            
getreturn vehicle; }
        }
        
abstract public void buildframe();
        
abstract public void buildengine();
        
abstract public void buildwheels();
        
abstract public void builddoors();
    }
    
//具体建造者-摩托车车间
    class motorcyclebuilder : vehiclebuilder
    {
        
override public void buildframe(){
            vehicle 
= new vehicle( "摩托车" );
            vehicle[ 
"frame" ] = "motorcycle frame";
        }

        
override public void buildengine(){
            vehicle[ 
"engine" ] = "500 cc";
        }

        
override public void buildwheels(){
            vehicle[ 
"wheels" ] = "2";
        }

        
override public void builddoors(){
            vehicle[ 
"doors" ] = "0";
        }
    }
    
//具体建造者-轿车车间
    class carbuilder : vehiclebuilder
    {
        
override public void buildframe(){
            vehicle 
= new vehicle( "轿车" );
            vehicle[ 
"frame" ] = "car frame";
        }
        
override public void buildengine(){
            vehicle[ 
"engine" ] = "2500 cc";
        }
        
override public void buildwheels(){
            vehicle[ 
"wheels" ] = "4";
        }
        
override public void builddoors(){
            vehicle[ 
"doors" ] = "4";
        }
    }
    
// 具体建造者-单脚滑行车车间
    class scooterbuilder : vehiclebuilder
    {
        
override public void buildframe(){
            vehicle 
= new vehicle( "单脚滑行车" );
            vehicle[ 
"frame" ] = "scooter frame";
        }

        
override public void buildengine(){
            vehicle[ 
"engine" ] = "none";
        }

        
override public void buildwheels(){
            vehicle[ 
"wheels" ] = "2";
        }

        
override public void builddoors(){
            vehicle[ 
"doors" ] = "0";
        }
    }
    
//车辆产品类
    class vehicle
    {
        
private string type;
        
private hashtable parts = new hashtable();
        
//筑构函数,决定类型
        public vehicle( string type ){
            
this.type = type;
        }
        
//索引
        public object thisstring key ]{
            
getreturn parts[ key ]; }
            
set{ parts[ key ] = value; }
        }
        
//显示方法
        public void show()
        {
            console.writeline( 
"\n---------------------------");
            console.writeline( 
"车辆类类型: " type );
            console.writeline( 
" 框架 : "  parts[ "frame" ] );
            console.writeline( 
" 发动机 : " parts[ "engine"] );
            console.writeline( 
" #轮子数: " parts[ "wheels"] );
            console.writeline( 
" #车门数 : " parts[ "doors" ] );
        }
    }
    
/// 
    
/// 建造者模式应用测试
    
/// 

     class carshop
    {
        [stathread]
        
static void main(string[] args)
        {
            
// 创造车间及车辆建造者
            shop shop = new shop();
            vehiclebuilder b1 
= new scooterbuilder();
            vehiclebuilder b2 
= new carbuilder();
            vehiclebuilder b3 
= new motorcyclebuilder();
            
// 筑构并显示车辆
            shop.construct( b1 );
            b1.vehicle.show();
            shop.construct( b2 );
            b2.vehicle.show();
            shop.construct( b3 );
            b3.vehicle.show();
            console.read();

        }
    }
}


三。factory method模式
1.意图:定义一个用于创建对象的接口,让子类决定实例化具体的哪一个类。
2.适用场景:
1)当一个类不知道它所必须创建的对象的类的时候,让子类来决定
2)当一个类希望由它的子类来决定它所创建的对象的时候

3。uml图——结构:

4.效果:
1)为子类提供回调函数
2)连接平行的类层次
3) 创建函数可以接收参数来决定创建什么产品
4)factory method容易导致创建过多的creator的子类以对应不同的产品,这个方法可以通过模板技术来解决

6.实现,手机工厂,产品可能是nokia,也可能是motorola
using system;
using system.collections.generic;
using system.text;

namespace handphone
{
    
//手机接口
    interface mobile
    {
         
void call();
    }

    
//手机工厂接口
    interface mobilefactory
    {
          mobile createmobile();
    }

    
//nokia
    class nokia:mobile
    {
        
public void call()
        {
            console.writeline(
"this is a {0} phone"this.gettype().name);
        }
    }

    
//motorola
    class motorola : mobile
    {
        
public void call()
        {
            console.writeline(
"this is a {0} phone"this.gettype().name);
        }
    }

    
//motorola工厂
    class motorolafactory : mobilefactory
    {
         
public mobile createmobile()
        {
            
return new motorola();
        }
    }

    
//nokia工厂
    class nokiafactroy : mobilefactory
    {
        
public mobile createmobile()
        {
            
return new nokia();
        }
    }

    
public class client
    {
        
static void main(string []args)
        {
            mobilefactory factory
=null;
            mobile mobile
=null;

            factory
=new nokiafactroy();
            mobile
=factory.createmobile();
            mobile.call();

            factory
=new motorolafactory();
            mobile
=factory.createmobile();
            mobile.call();
            
        }
    }

}

四。prototype模式

1.意图:通过原型实例指定创建对象的种类,并通过拷贝这些原型来创建新的对象

2.适用场景:
1)要实例化的类是在运行时刻指定的,比如动态装载
2)为了避免创建一个与产品层次平行的工厂类层次
3)当一个类的实例只能有几个不同的状态组合中的一种时,建立相应数目的原型并克隆它们可能比每次用合适的状态手工化该类更方便一些。

3.uml图——结构:



4.效果:
1)运行时动态增加或者删除产品
2)减少子类的构造数目
3)用类动态配置应用
4)动态指定新的对象,通过改变结构或者值
5)缺陷在于每一个prototype的子类都需要实现clone操作

5。实现,无论java还是c#都从语言层次内置了对prototype模式的支持。具体不再详述。

五。singleton模式
1。意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点

2.适用场景:
1)当需要类只有一个实例,并且客户只能通过一个全局点去访问它
2)当这个唯一实例应该是通过子类化可扩展的,并且客户应该无需更改代码就能使用扩展的实例

3.uml图:略

4.效果:
1)对唯一实例的受控访问
2)缩小命名空间
3)允许对操作和表示的细化
4)允许可变数目的实例

5实现,关于singleton在java多线程环境下的实现,请见讨论《》,c#对singleton多线程环境下的能够正确实现double-checked模式:
public sealed class singleton
{
    
private static volatile singleton instance;
    
private static object syncroot = new object();

    
private singleton() {}

    
public static singleton instance
    {
        
get 
        {
            
if (instance == null
            {
                
lock (syncroot) 
                {
                    
if (instance == null
                        instance 
= new singleton();
                }
            }

            
return instance;
        }
    }
}


本文仅作速查记忆用,摘记于《设计模式——可复用面向对象软件基础》和《深入浅出设计模式(java c#)》两书



dennis 2007-03-17 17:01
]]>
《设计模式精解》读后感http://www.blogjava.net/killme2008/archive/2007/03/03/101592.htmldennisdennissat, 03 mar 2007 02:41:00 gmthttp://www.blogjava.net/killme2008/archive/2007/03/03/101592.htmlhttp://www.blogjava.net/killme2008/comments/101592.htmlhttp://www.blogjava.net/killme2008/archive/2007/03/03/101592.html#feedback10http://www.blogjava.net/killme2008/comments/commentrss/101592.htmlhttp://www.blogjava.net/killme2008/services/trackbacks/101592.html    在春节期间读完了这本书,本书详细介绍了13种常见的设计模式,以一个实际问题引出了对面向对象新观点和设计模式的讨论,在介绍完所有的模式后,更难能可贵的是作者详细介绍了自己对模式的使用经验(使用共同点/变化点分析,使用分析矩阵等),整本书读下来令人心旷神怡。
    我们为什么要学习设计模式呢?总之是为了获得可以复用和容易扩展的凯发天生赢家一触即发官网的解决方案,建立通用术语以方便团队内的沟通交流,另外,模式能让你以更高的层次或者说视角去观察问题,这样的视角将你从过早处理细节的泥潭中解放出来。模式本身就是对如何创建优良面向对象设计策略的实现:
    1.针对接口编程
    2.优先使用对象组合,而不是类继承
    3.发现并封装变化点
    你可以在每一个模式的背后或者每一个优秀设计的背后看到这些原则的影子。比如abstract factory、adapter、strategy体现了针对接口编程,composite、bridge体现了优先使用组合而不是继承等。
   
    作者解释了面向对象的新观点:
   
                原来的观点                新的观点

对象           伴随有方法的数据            拥有责任的实体,或者说拥有特定行为的实体  


封装          数据隐藏                    各种形式的封装,1.数据的封装,2.方法的封装
                                           3.父类对子类的隐藏   4.其他对象的封装

继承          特化和复用                   对象分类的一种方法

    这些观点其实并不新,martin fowler提出了软件开发过程中的三种视觉:概念、规格和实现,过去我们对面向对象的观点来自于实现的角度(代码的角度)去观察,而新的观点只是从概念的角度重新观察面向对象设计。
    深入到具体模式的讨论,记录一些需要注意的问题:
1.adapter与facade模式的区别
它们都是包装器,但是两者也有细微的区别:
.两个模式中,我们都有已经存在的类(或者说系统)
.facade模式中,我们无需针对接口编程;而adapter模式我们必须针对接口编程
.adapter模式通常是为了保持多态,而facade模式对此不感兴趣
.动机不同,facade模式是为了简化接口,而adapter模式是针对一个现存的接口编程
结论:facade模式简化接口,而adapter模式将接口转换成另一个现有的接口

2.bridge模式的理解
bridge模式的意图是将抽象部分与它的实现部分分离,使它们可以独立的变化。这里的关键点是需要理解“实现部分”,如果把“实现部分”看成“对象外部、被对象使用的某种东西”,此模式就很好理解了。我们将变化转移到一个使用或者拥有变化的对象(此对象是指抽象类的对象和用来实现抽象类的派生类的对象)。当出现继承的类爆炸情况时,也许你该考虑此模式的应用场景了。此模式的uml图
bridge1.jpg


3.observer模式,实现自己的观察者模式也是很简单,不过java已经内置了对此模式的支持。java.util.observer和java.util.observable是此模式的一个实现版本,实际应用中你所需要做的只是实现observer接口,和继承observable类

4.decorator模式是为现有的功能动态添加附加功能的一种方法,uml图如下
showimg.jpg
java的io库是典型的应用实现,java.io.inputstream和java.io.outputstream就是图中的component接口,filterinputstream继承inputstream(也就是图中的decorator,装饰器),其他的如bytearrayinputstream、fileinputstream等直接继承自inputstream的类就是被装饰对象,而继承filterinputstream的就是各式各样的装饰者。

5.strategy模式是一种定义算法家族的方法,所有的算法都做相同的工作,它们只是拥有不同的实现。当你的代码中出现了很多switch或者if else的语句,你应该考虑此模式。strategy模式带来的缺点是类的数量的增加,在java中可以通过将实现类作为嵌套类放在strategy抽象类中来解决。

6.singleton模式的实现
单线程应用:
第一种:静态初始化
public class singleton {
    
private singleton() {
    }

    
private static singleton instance = new singleton();

    
public static singleton getinstance() {
        
return instance;
    }
}

第二种:lazy loading
public   class   singleton   {    
    
private   static   singleton   instance   =   null;  
   
    
public   static   singleton   getinstance()   {  

    if   (instance==null)  
      instance=
new   singleton();  
    
return   instance;     }    
   
  }  

多线程环境下:在c 中安全的double-checked locking模式,在java中是不安全的,详细原因与java的内存管理模型有关,请见dreamstone的文章《java中的模式——单态》
安全的实现方法是使用同步:
   public class singleton {  
  
  
  
  static singleton instance;  
  
  
  
   public static synchronized singleton getinstance() {  
  
     if (instance == null)  
  
       instance == new singleton();  
  
     return instance;  
  
   }  
 
  
 
}  

《effective java》中提到的另一种写法
public class singleton {

  
static class singletonholder {
    
static singleton instance = new singleton();
  }

  
public static singleton getinstance() {
    
return singletonholder.instance;
  }

}

    在介绍完13个模式之后,作者提出了 一种称为分析矩阵的方法,详细讨论不是这篇短文能说的完,有兴趣请找来此书的电子版看看。简单来讲,先从问题领域中分析出所有的变化点和共同点,观察每一种必须实现的功能并作为矩阵的行,而矩阵中的列表示特定情况中的特定实现;然后观察行,并根据场景探讨使用合适的模式;最后观察列,从整体上考虑整个问题的模式的使用。在出现的概念的场景中添加新的概念来进行设计。

    最后,作者总结了面向对象的原则:
.“对象”是负有定义良好的责任的东西
.对象对自己负责
.封装意味着
    ——数据隐藏
    ——类隐藏(藏在抽象类或者接口后面)
    ——实现隐藏(变化封装为对象进行引用)
.使用共同点/变化点分析抽象出行为和数据中的变化点
.针对接口编程
.把继承考虑为一种封装变化的方法,而不是为现有的对象制造特殊情况
.将变化点封装在一个类中,并使之与其他变化点相分离
.力求松耦合
.力求高内聚
.绝对细心地应用“一次并且只有一次”规则(只在一个地方实现一条规则)
     书本只是提供了模式的介绍和参照,真正的应用还是要靠自己日常工作中的观察和体验,我将继续在工作中理解并贯彻这些原则。     


dennis 2007-03-03 10:41
]]>
使用annotation设计持久层http://www.blogjava.net/killme2008/archive/2007/02/06/98239.htmldennisdennistue, 06 feb 2007 04:24:00 gmthttp://www.blogjava.net/killme2008/archive/2007/02/06/98239.htmlhttp://www.blogjava.net/killme2008/comments/98239.htmlhttp://www.blogjava.net/killme2008/archive/2007/02/06/98239.html#feedback0http://www.blogjava.net/killme2008/comments/commentrss/98239.htmlhttp://www.blogjava.net/killme2008/services/trackbacks/98239.html链接:
http://www.javaresearch.org/article/59935.htm
http://www.javaresearch.org/article/59043.htm


代码下载同样在www.126.com的邮箱里,用户名 sharesources 密码 javafans

    本文只是学习性质的文章,我一开始的想法就是修改《设计模式之事务处理》,提供annotation来提供事务支持,支持到方法级别。通过引入一个 @transaction标注,如果被此标注的方法将自动享受事务处理。目的是学习下annotation和加深下对声明式事务处理的理解。

    annotation是jdk5引入的新特性,现在越来越多的框架采用此特性来代替烦琐的xml配置文件,比如hibernate,ejb3, spring等。对annotation不了解,请阅读ibm网站上的文章,还有推荐javaeye的annotation专栏:http: //www.javaeye.com/subject/annotation

    代码的示例是一个简单的用户管理例子。

    首先,环境是mysql jdk5 myeclipse5 tomcat5,在mysql中建立一张表adminusers:
    create table adminusers(id int(10) auto_increment not null primary key,
     name varchar(10) not null,
     password varchar(10) not null,
     user_type varchar(10));
    然后在tomcat下建立一个数据源,把代码中的strutslet.xml拷贝到tomcat安装目录下的 /conf/catalina/localhost目录里,请自行修改文件中的数据库用户名和密码,以及数据库名称。另外,把mysql的 jdbc驱动拷贝到tomcat安装目录下的common/lib目录。这样数据源就建好了。在web.xml中引用:

  
        db connection
        jdbc/test
        javax.sql.datasource
        container
   

   
    我的例子只是在《设计模式之事务处理》的基础上改造的,在那篇文章里,我讲解了自己对声明式事务处理的理解,并利用动态代理实现了一个 transactionwrapper(事务包装器),通过业务代理工厂提供两种版本的业务对象:经过事务包装的和未经过事务包装的。我们在默认情况下包装业务对象中的所有方法,但实际情况是,业务对象中的很多方法不用跟数据库打交道,它们根本不需要包装在一个事务上下文中,这就引出了,我们为什么不提供一种方式来配置哪些方法需要事务控制而哪些并不需要?甚至提供事务隔离级别的声明?很自然的想法就是提供一个配置文件,类似spring式的事务声明。既然jdk5已经引入annotation,相比于配置文件的烦琐和容易出错,我们定义一个@transaction的annotation来提供此功能。

    看下transaction.java的代码:
    package com.strutslet.db;

    import java.lang.annotation.documented;
    import java.lang.annotation.elementtype;
    import java.lang.annotation.retention;
    import java.lang.annotation.retentionpolicy;
    import java.lang.annotation.target;
    import java.sql.connection;

    @target(elementtype.method)
    @retention(retentionpolicy.runtime)
    @documented
    public @interface transaction {
       //事务隔离级别,默认为read_committed
       public int level() default connection.transaction_read_committed    ;
    }

@transaction 标注只有一个属性level,level表示事务的隔离级别,默认为read_committed(也是一般jdbc驱动的默认级别,jdbc驱动默认级别一般于数据库的隔离级别一致)。 @target(elementtype.method)表示此标注作用于方法级别, @retention(retentionpolicy.runtime)表示在运行时,此标注的信息将被加载进jvm并可以通过annotation的 api读取。我们在运行时读取annotation的信息,根据隔离级别和被标注的方法名决定是否将业务对象的方法加进事务控制。我们只要稍微修改下 transactionwrapper:

//transactionwrapper.java
package com.strutslet.db;

import java.lang.annotation.annotation;
import java.lang.reflect.invocationhandler;
import java.lang.reflect.method;
import java.lang.reflect.proxy;
import java.sql.connection;
import java.sql.sqlexception;

import com.strutslet.exception.systemexception;

public class transactionwrapper {

   
    public static object decorate(object delegate) {
        return proxy.newproxyinstance(delegate.getclass().getclassloader(),
                delegate.getclass().getinterfaces(), new xawrapperhandler(
                        delegate));
    }

    static final class xawrapperhandler implements invocationhandler {
        private final object delegate;

        xawrapperhandler(object delegate) {
            // cache the wrapped delegate, so we can pass method invocations
            // to it.
            this.delegate = delegate;
        }

        public object invoke(object proxy, method method, object[] args)
                throws throwable {
            object result = null;
            connection con = connectionmanager.getconnection();
            //得到transaction标注
            transaction transaction = method.getannotation(transaction.class);

            //如果不为空,说明代理对象调用的方法需要事务控制。
            if (transaction != null) {
                // system.out.println("transaction.." con.tostring());
                // 得到事务隔离级别信息
                int level = transaction.level();
                try {
                    if (con.getautocommit())
                        con.setautocommit(false);
                    //设置事务隔离级别
                    con.settransactionisolation(level);
                    //调用原始对象的业务方法
                    result = method.invoke(delegate, args);
                    con.commit();
                    con.setautocommit(true);
                } catch (sqlexception se) {
                    // rollback exception will be thrown by the invoke method
                    con.rollback();
                    con.setautocommit(true);
                    throw new systemexception(se);
                } catch (exception e) {
                    con.rollback();
                    con.setautocommit(true);
                    throw new systemexception(e);
                }
            } else {
                result = method.invoke(delegate, args);
            }

            return result;
        }
    }
}

现在,看下我们的usermanager业务接口,请注意,我们是使用动态代理,只能代理接口,所以要把@transaction标注是接口中的业务方法(与ejb3中的remote,local接口类似的道理):
package com.strutslet.demo.service;

import java.sql.sqlexception;

import com.strutslet.db.transaction;
import com.strutslet.demo.domain.adminuser;

public interface usermanager {
    //查询,不需要事务控制
    public boolean checkuser(string name, string password) throws sqlexception;

    //新增一个用户,需要事务控制,默认级别
    @transaction
    public boolean adduser(adminuser user) throws sqlexception;

}

要把adduser改成其他事务隔离级别(比如oracle的serializable级别),稍微修改下:@transaction(level=connection.transaction_serializable)
public boolean adduser(adminuser user) throws sqlexception;

不准备详细解释例子的业务流程,不过是登录和增加用户两个业务方法,看下就明白。阅读本文前最好已经读过开头提过的两篇文章。我相信代码是最好的解释:)

dennis 2007-02-06 12:24
]]>
设计自己的mvc框架http://www.blogjava.net/killme2008/archive/2007/02/06/98227.htmldennisdennistue, 06 feb 2007 03:54:00 gmthttp://www.blogjava.net/killme2008/archive/2007/02/06/98227.htmlhttp://www.blogjava.net/killme2008/comments/98227.htmlhttp://www.blogjava.net/killme2008/archive/2007/02/06/98227.html#feedback2http://www.blogjava.net/killme2008/comments/commentrss/98227.htmlhttp://www.blogjava.net/killme2008/services/trackbacks/98227.html阅读全文

dennis 2007-02-06 11:54
]]>
一个servicelocator模式的实现http://www.blogjava.net/killme2008/archive/2007/02/06/98224.htmldennisdennistue, 06 feb 2007 03:49:00 gmthttp://www.blogjava.net/killme2008/archive/2007/02/06/98224.htmlhttp://www.blogjava.net/killme2008/comments/98224.htmlhttp://www.blogjava.net/killme2008/archive/2007/02/06/98224.html#feedback0http://www.blogjava.net/killme2008/comments/commentrss/98224.htmlhttp://www.blogjava.net/killme2008/services/trackbacks/98224.html阅读全文

dennis 2007-02-06 11:49
]]>
缓存filter及资源池模式http://www.blogjava.net/killme2008/archive/2007/02/06/98222.htmldennisdennistue, 06 feb 2007 03:47:00 gmthttp://www.blogjava.net/killme2008/archive/2007/02/06/98222.htmlhttp://www.blogjava.net/killme2008/comments/98222.htmlhttp://www.blogjava.net/killme2008/archive/2007/02/06/98222.html#feedback0http://www.blogjava.net/killme2008/comments/commentrss/98222.htmlhttp://www.blogjava.net/killme2008/services/trackbacks/98222.html阅读全文

dennis 2007-02-06 11:47
]]>
设计模式之事务处理http://www.blogjava.net/killme2008/archive/2007/02/06/98217.htmldennisdennistue, 06 feb 2007 03:32:00 gmthttp://www.blogjava.net/killme2008/archive/2007/02/06/98217.htmlhttp://www.blogjava.net/killme2008/comments/98217.htmlhttp://www.blogjava.net/killme2008/archive/2007/02/06/98217.html#feedback4http://www.blogjava.net/killme2008/comments/commentrss/98217.htmlhttp://www.blogjava.net/killme2008/services/trackbacks/98217.html阅读全文

dennis 2007-02-06 11:32
]]>
网站地图