庄周梦蝶

生活、程序、未来
   :: 首页 ::  ::  :: 聚合  :: 管理

     摘要: 简单的web server性能测试,一般多线程方式与采用jdk5线程池的比较。  阅读全文

posted @ 2007-08-29 18:10 dennis 阅读(4714) | 评论 (6)编辑 收藏

    linux/unix系统的I/O也就是一般所说的低级I/O——操作系统提供的基本IO服务,与os绑定,特定于*nix平台。而标准I/O是ANSI C建立的一个标准I/O模型,是一个标准函数包和stdio.h头文件中的定义,具有一定的可移植性。两者一个显著的不同点在于,标准I/O默认采用了缓冲机制,比如调用fopen函数,不仅打开一个文件,而且建立了一个缓冲区(读写模式下将建立两个缓冲区),还创建了一个包含文件和缓冲区相关数据的数据结构。低级I/O一般没有采用缓冲,需要自己创建缓冲区,不过其实在*nix系统中,都是有使用称为内核缓冲的技术用于提高效率,读写调用是在内核缓冲区和进程缓冲区之间进行的数据复制。

1.fopen与open
标准I/O使用fopen函数打开一个文件
FILE* fp=fopen(const char* path,const char *mod)

其中path是文件名,mod用于指定文件打开的模式的字符串,比如"r","w","w+","a"等等,可以加上字母b用以指定以二进制模式打开(对于*nix系统,只有一种文件类型,因此没有区别),如果成功打开,返回一个FILE文件指针,如果失败返回NULL,这里的文件指针并不是指向实际的文件,而是一个关于文件信息的数据包,其中包括文件使用的缓冲区信息。

*nix系统使用open函数用于打开一个文件
int fd=open(char *name,int how);
与fopen类似,name表示文件名字符串,而how指定打开的模式:O_RDONLY(只读),O_WRONLY(只写),O_RDWR (可读可写),还有其他模式请man 2 open。成功返回一个正整数称为文件描述符,这与标准I/O显著不同,失败的话返回-1,与标准I/O返回NULL也是不同的。

2.fclose与close
与打开文件相对的,标准I/O使用fclose关闭文件,将文件指针传入即可,如果成功关闭,返回0,否则返回EOF
比如:
if(fclose(fp)!=0)  
  printf(
"Error in closing file");

而*nix使用close用于关闭open打开的文件,与fclose类似,只不过当错误发生时返回的是-1,而不是EOF,成功关闭同样是返回0。C语言用error code来进行错误处理的传统做法。

3.读文件,getc,fscanf,fgets和read
标准I/O中进行文件读取可以使用getc,一个字符一个字符的读取,也可以使用gets(读取标准io读入的)、fgets以字符串单位进行读取(读到遇到的第一个换行字符的后面),gets(接受一个参数,文件指针)不判断目标数组是否能够容纳读入的字符,可能导致存储溢出(不建议使用),而fgets使用三个参数:
 char * fgets(char *s, int size, FILE *stream);
第一个参数和gets一样,用于存储输入的地址,第二个参数为整数,表示输入字符串的最大长度,最后一个参数就是文件指针,指向要读取的文件。最后是fscanf,与scanf类似,只不过增加了一个参数用于指定操作的文件,比如fscanf(fp,"%s",words)

*nix系统中使用read函数用于读取open函数打开的文件,函数原型如下:
ssize_t numread=read(int fd,void *buf,size_t qty);

其中fd就是open返回的文件描述符,buf用于存储数据的目的缓冲区,而qty指定要读取的字节数。如果成功读取,就返回读取的字节数目(小于等于qty)。

4.判断文件结尾,如果尝试读取达到文件结尾,标准IO的getc会返回特殊值EOF,而fgets碰到EOF会返回NULL,而对于*nix的read函数,情况有所不同。read读取qty指定的字节数,最终读取的数据可能没有你所要求的那么多(qty),而当读到结尾再要读的话,read函数将返回0.

5.写文件:putc,fputs,fprintf和write

与读文件相对应的,标准C语言I/O使用putc写入字符,比如:
putc(ch,fp);
第一个参数是字符,第二个是文件指针。而fputs与此类似:
fputs(buf,fp);
仅仅是第一个参数换成了字符串地址。而fprintf与printf类似,增加了一个参数用于指定写入的文件,比如:
fprintf(stdout,"Hello %s.\n","dennis");
切记fscanf和fprintf将FILE指针作为第一个参数,而putc,fputs则是作为第二个参数。

在*nix系统中提供write函数用于写入文件,原型与read类似:
ssize_t result=write(int fd,void *buf ,size_t amt);

fd是文件描述符,buf是将要写入的内存数据,amt是要写的字节数。如果写入成功返回写入的字节数,通过result与amt的比较可以判断是否写入正常,如果写入失败返回-1。write函数仅仅是将数据写入了缓冲区,何时写入磁盘由内核决定,如果要强制写入硬盘,那么在open的时候选择O_SYNC选项,或者调用fsync函数

6.随机存取:fseek()、ftell()和lseek()
标准I/O使用fseek和ftell用于文件的随机存取,先看看fseek函数原型
int fseek(FILE *stream, long offset, int whence);
第一个参数是文件指针,第二个参数是一个long类型的偏移量(offset),表示从起始点开始移动的距离。第三个参数就是用于指定起始点的模式,stdio.h指定了下列模式常量:
SEEK_SET            文件开始处
SEEK_CUR            当前位置
SEEK_END            文件结尾处
看几个调用例子:
fseek(fp,0L,SEEK_SET);  //找到文件的开始处
fseek(fp,0L,SEEK_END);  //定位到文件结尾处
fseek(fp,2L,SEEK_CUR);  //文件当前位置向前移动2个字节数

而ftell函数用于返回文件的当前位置,返回类型是一个long类型,比如下面的调用:
fseek(fp,0L,SEEK_END);//定位到结尾
long last=ftell(fp);  //返回当前位置
那么此时的last就是文件指针fp指向的文件的字节数。

与标准I/O类似,*nix系统提供了lseek来完成fseek的功能,原型如下:
off_t lseek(int fildes, off_t offset, int whence);

fildes是文件描述符,而offset也是偏移量,whence同样是指定起始点模式,唯一的不同是lseek有返回值,如果成功就返回指针变化前的位置,否则返回-1。因此可以通过下列方法模拟ftell函数来返回当前偏移量:
off_t    currpos;
currpos = lseek(fd, 0, SEEK_CUR);
whence的取值与fseek相同:SEEK_SET,SEEK_CUR,SEEK_END,但也可以用整数0,1,2相应代替。

最后,以一个例子结尾,通过c语言编写linux系统的cp指令,先看看使用标准I/O版本的:
#include<stdio.h>
#include
<stdlib.h>
void oops(char *,char *);
int main(int ac,char *av[])
{
  FILE 
*in,*out;
  
int ch;

  
if(ac!=3){
   fprintf(stderr,
"Useage:%s source-file target-file.\n",av[0]);
   exit(
1);
  }
  
if((in=fopen(av[1],"r"))==NULL)
   oops(
"can not open ",av[1]);
  
if((out=fopen(av[2],"w"))==NULL)
   oops(
"can not open ",av[2]);
  
while((ch=getc(in))!=EOF)
    putc(ch,out);
  
if(fclose(in)!=0||fclose(out)!=0)
    oops(
"can not close files.\n"," ");
  
return 0;
}
void oops(char *s1,char* s2)
{
  fprintf(stderr,
"Error:%s %s\n",s1,s2);
  exit(
1);
}

再看一个使用unix io的版本:
#include<unistd.h>
#include
<stdio.h>
#include
<fcntl.h>

#define BUFFERSIZE 
4096
#define COPYMODE 
0644
void oops(char *,char *);

int main(int ac,char *av[])
{
  
int in_fd,out_fd,n_chars;
  
char buf[BUFFERSIZE];
  
if(ac!=3){
    fprintf(stderr,
"useage:%s source-file target-file.\n",av[0]);
    exit(
1);
  }

  
if((in_fd=open(av[1],O_RDONLY))==-1)
     oops(
"Can't open ",av[1]);
  
if((out_fd=creat(av[2],COPYMODE))==-1)
    oops(
"Can't open ",av[2]);
  
while((n_chars=read(in_fd,buf,BUFFERSIZE))>0)
      
if(write(out_fd,buf,n_chars)!=n_chars)
           oops(
"Write error to ",av[2]);
  
if(n_chars==-1)
      oops(
"Read error from ",av[1]);
  
if(close(in_fd)==-1||close(out_fd)==-1)
    oops(
"Error closing files","");
  
return 0;
}
void oops(char *s1,char *s2)
{
  fprintf(stderr,
"Error:%s",s1);
  perror(s2);
  exit(
1);
}

显然,在使用unix i/o的时候,你要更多地关注缓冲问题以提高效率,而stdio则不需要考虑。






posted @ 2007-08-22 16:47 dennis 阅读(1321) | 评论 (0)编辑 收藏

    这一周一口气开发了4个新功能,修正了十几个客户提出的新要求,设计了新的绩效评价算法,将《Programming Erlang》读到第11章,《C primer plus》读到14章,开始读《unix/linux编程实践》,写了数十道C习题,累,而又充实。今天下午,前几天联系我的一家美国公司聘请ROR兼职的在MSN面试了一下,一开始没有意识到是面试,对自己的表现很不满意,先问下了我对自己优势和弱点的看法,然后就针对这些开始提问,问了sql语句,一般的db设计应该注意的问题,数据结构中queue和stack区别,最后发了封email,一个比较长的需求分析,考察我的E文理解能力和需求分析、设计能力。对自己的回答并不满意吧,我觉的我没有完全展现出自己的能力,也是太累了,回答完刚好下班,躺在沙发上竟然睡着了。能不能成也要看运气了,做ROR兼职真是项有趣的挑战,我很希望试试。

posted @ 2007-08-10 18:40 dennis 阅读(379) | 评论 (1)编辑 收藏

     摘要:     这是最近在项目中的一个需求,已知a=3,求字符串"a<=2"的值,也就是应该返回false。这个问题可大可小,就我们的应用场景也就是用来让用户自定义变量区间,比如类似下面这样的规则:a<=2    返回积分系数1.02<a<=5  返回积分系数1.1a>5   ...  阅读全文

posted @ 2007-08-06 12:37 dennis 阅读(7961) | 评论 (7)编辑 收藏

    这是Programming Erlang第8章节的一个练习,创建N个process,连接成一个圈,然后在这个圈子里发送消息M次,看看时间是多少,然后用另一门语言写同样的程序,看看时间是多少。我自己写的版本在处理3000个进程,1000次消息循环(也就是300万次消息传递)时花了5秒多,后来去google别人写的版本,竟然让我找到一个98年做的benchmark:Erlang vs. java,也是同样的的问题。测试的结果是Erlang性能远远大于java,这也是显然的结果,Erlang的process是轻量级、无共享的,而java的线程是os级别的,两者创建的cost不可同日而语。详细的比较请看这里
    不过我分析了这个测试里的Erlang代码,存在问题,并没有完成所有的循环,进程就结束了,这对比较结果有较大的影响。原始代码如下:

-module(zog).

%% This is a test program that first creates N processes (that are
%% "connected" in a ring) and then sends M messages in that ring.
%%
%% - September 1998
%% - roland


-export([start/0, start/1, start/2]).

-export([run/2, process/1]). % Local exports - ouch

start() -> start(16000).

start(N) -> start(N, 1000000).

start(N, M) -> spawn(?MODULE, run, [N, M]).


run(N, M) when N < 1 ->
io:format("Must be at least 1 process~n", []),
0.0;
run(N, M) ->
statistics(wall_clock),

Pid = setup(N-1, self()),

{_,T1} = statistics(wall_clock),
io:format("Setup : ~w s", [T1/1000]),
case N of
1 -> io:format(" (0 spawns)~n", []);
_ -> io:format(" (~w us per spawn) (~w spawns)~n",
[1000*T1/(N-1), N-1])
end,
statistics(wall_clock),

Pid ! M,
K = process(Pid),

{_,T2} = statistics(wall_clock),
Time = 1000*T2/(M+K),
io:format("Run : ~w s (~w us per msg) (~w msgs)~n",
[T2/1000, Time, (M+K)]),

Time.

setup(0, OldPid) ->
OldPid;
setup(N, OldPid) ->
NewPid = spawn(?MODULE, process, [OldPid]),
setup(N-1, NewPid).


process(Pid) ->
receive
M ->
Pid ! M-1,
if
M < 0 -> -M;
true -> process(Pid)
end
end.
  我将process修改一下
process(Pid) ->
receive
M ->
Pid ! M-1,
io:format("form ~w to ~w~n",[self(),Pid]),
if
M < 0 -> -M;
true -> process(Pid)
end
end.
然后执行下zog:run(3,3),你将发现消息绕了两圈就结束了,第三圈根本没有进行,不知道测试者是什么用意。依照现在的执行300万次消息传送竟然只需要3毫秒!我修改了下了下代码如下:
-module(zog).

%% This is a test program that first creates N processes (that are
%% "connected" in a ring) and then sends M messages in that ring.
%%
%% - September 1998
%% - roland
-export([start/0, start/1, start/2]).

-export([run/2, process/2]).                    % Local exports - ouch

start() -> start(16000).

start(N) -> start(N, 1000000).

start(N, M) -> spawn(?MODULE, run, [N, M]).


run(N, M) when N < 1 ->
    io:format("Must be at least 1 process~n", []),
    0.0;
run(N, M) ->
    statistics(wall_clock),
    Limit=N-N*M+1+M,
    Pid = setup(N-1,Limit,self()),

    {_,T1} = statistics(wall_clock),
    io:format("Setup : ~w s", [T1/1000]),
    case N of
        1 -> io:format(" (0 spawns)~n", []);
        _ -> io:format(" (~w us per spawn) (~w spawns)~n",
                       [1000*T1/(N-1), N-1])
    end,
    statistics(wall_clock),
  %  io:format("run's Pid=~w~n",[Pid]),
    Pid ! M,
    K = process(Pid,Limit),
  %  io:format("run's K=~w~n",[K]),

    {_,T2} = statistics(wall_clock),
    Time = 1000*T2/(M+K),
    io:format("Run   : ~w s (~w us per msg) (~w msgs)~n",
              [T2/1000, Time, (M+K)]),
 T2/1000.

setup(0,Limit, OldPid) ->
    OldPid;
setup(N,Limit, OldPid) ->
    NewPid = spawn(?MODULE, process, [OldPid,Limit]),
    setup(N-1, Limit,NewPid).


process(Pid,Limit) ->
    receive
        M ->
            Pid ! M-1,
         %   io:format("from ~w to ~w and M=~w~n",[self(),Pid,M]),
            if
                M <Limit  -> -M;
                true   -> process(Pid,Limit)
            end
    end.
修改之后,执行zog:run(3000,1000),也就是3000个进程,1000次消息循环,总共300万次消息传递,结果在2.5秒左右,这也是相当惊人的结果。有人用haskell和scheme各实现了一个版本,有兴趣的看看这里这里


posted @ 2007-08-04 17:46 dennis 阅读(1513) | 评论 (0)编辑 收藏

    《C Primer Plus》读到12章,我的C语言复习进展的挺不错。这一章介绍存储类、连接和内存管理,可以说是重中之重。
C的5种存储类:
自动——在一个代码块内(或在一个函数头部作为参量)声明的变量,无论有没有存储类修饰符auton,都属于自动存储类。该类具有自动存储时期、代码块的作用域和空链接(no linkage),如未初始化,它的值是不确定的(java要求局部变量必须初始化)

寄存器——在一个代码块内(或在一个函数头部作为参量)使用修饰符register声明的变量属于寄存器存储类。该类与自动存储类相似,具有自动存储时期、代码块作用域和空连接,声明为register仅仅是一个请求,而非命令,因此变量仍然可能是普通的自动变量,但是仍然无法获取地址。。如果没有被初始化,它的值也是未定的。

静态、空链接——在一个代码块内使用存储类修饰符static声明的局部变量属于静态空连接存储类。该类具有静态存储时期、代码块作用域和空链接,仅在编译时初始化一次。如未明确初始化,它的字节将被设定为0.

静态、外部链接——在所有函数外部定义、未使用static修饰的变量属于静态、外部链接存储类。改类具有静态存储时期、文件作用域和外部链接,仅在编译时初始化一次。如未明确初始化,它的字节也被设定为0.

静态、内部链接——与静态、外部链接存储类不同的是,它使用static声明,也定义在所有函数外部,但是具有内部链接(仅能被与它在同一个文件的函数使用),仅在编译时初始化一次。如未明确初始化,它的字节也被设定为0.

两个关键字:volatile和restrict,两者都是为了方便编译器的优化。

volatile告诉编译器该被变量除了可被程序修改意外还可能被其他代理修改,因此,当要求使用volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,而不是使用寄存器中的缓存。比如
val1=x;
val2=x;
如果没有声明volatile,系统在给val2赋值的时候可能直接从寄存器读取x(假定聪明的编译器优化了),而不是从内存的初始位置,那么在两次赋值之间,x完全有可能被被某些编译器未知的因素更改(比如:操作系统、硬件或者其它线程等)。如果声明为volatile,编译器将不使用缓存,而是每次都从内存重新读取x。

而restrict是c99引入的,它只可以用于限定指针,并表明指针是访问一个数据对象的唯一且初始的方式,考虑下面的例子:
int ar[10];
int * restrict restar=(int *)malloc(10*sizeof(int));
int *par=ar;

这里说明restar是访问由malloc()分配的内存的唯一且初始的方式。par就不是了。
那么:
for(n=0;n<10;n++)
{
   par[n]+=5;
   restar[n]+=5;
   ar[n]*=2;
   par[n]+=3;
   restar[n]+=3;
}
因为restar是访问分配的内存的唯一且初始的方式,那么编译器可以将上述对restar的操作进行优化:
   restar[n]+=8;

而par并不是访问数组ar的唯一方式,因此并不能进行下面的优化:
   par[n]+=8;
因为在par[n]+=3前,ar[n]*=2进行了改变。使用了关键字restric,编译器就可以放心地进行优化了。这个关键字据说来源于古老的FORTRAN。有兴趣的看看这个

posted @ 2007-08-04 15:34 dennis 阅读(9458) | 评论 (1)编辑 收藏

    昨天在读到《Programming Erlang》第8章,开篇点出Erlang是一门纯粹的消息传递风格语言(message passing),我才算是领悟了消息传递。为了这个问题,我还冒昧地去问javaeye上的T1,对这个问题的兴趣是因为SICP第二章以及《失踪的链环》上的介绍。T1给我解答如下:智能能对象只是消息传递的一种具体应用.消息传递说的更为清晰一些就是一种映射关系或者说映射规则.f:a->b;这个规则可以是任意的。我一直将procedural representations of data,也就是sicp中声称intelligent data objects (智能对象)等价于消息传递, 而其实智能对象仅仅是消息传递的一种具体应用罢了。
消息传递机制通俗地来讲就是类似于马路上到处投递小广告的投递者,它采取的是Send and Pray策略,既不关心消息是否能精确的传送到真正需要消息的接收者,而是以广播的方式把消息发送给所有人,然后通过回馈来确定消息接收者的类型(引自《失踪的链环》)。因此,动态语言的duct typing是消息传递风格,智能对象是消息传递风格,显然,Erlang的process间的通信机制同样是消息传递风格(Process之间完全通过send message来进行控制和指示,不确定接收方是否具有处理消息的能力 ,异步的,接收的确认也要等待reply)。
    再来说说lambda算子理论,推荐下g9老大的lambda算子系列文章,这是开篇《lambda算子简介1.a》,以及另外一篇《康托尔、哥德尔、图灵——永恒的金色对角线(rev#2)》。lambda算子理论是函数式编程的理论基础,通过9条公理就可以推到出一个图灵完备的形式系统,其中的Y combinator的推导简直是魔法(为了表示递归),再次领略了计算理论的魅力。另外,最近读sicp第三章《模块化、对象和状态》,也理解了最初的面向对象思想来自何处,在引入了内部状态模拟时间变化之后,对象的最初思想也产生了,同时也带来了赋值导致的Side-Effect,而其实这正是动态OO语言中的对象的理念,通过消息来决定对象的type(ducktyping)。可现代的静态OO语言,在type和clas
之间画上了等号,java里面说一切都是object,其实他想表达的却是一切都是class,通过type以及函数签名等来决定消息的分派(message dispatch),导致更多的代码集中在消息分派,而不是真正的计算任务上,可以说静态OO已经偏离原始的对象模型很远。
    一点胡思乱想吧,我没有科班经历,所有的东西都是自己在学,在摸索,如有理论和常识上的谬误,请不吝赐教。

posted @ 2007-08-03 09:23 dennis 阅读(1254) | 评论 (0)编辑 收藏

习题3.6,我的实现如下:
(define rand
  (let ((x 3))
     (lambda(arg)
      (cond((eq? arg 'generate)
            ((lambda()(set! x (rand-update x)) x)))
           ((eq? arg 'reset)
            (lambda(init) (set! x init) (set! x (rand-update x)) x))
           (else
             error "Unkonown OP")))))
简单解释下,当参数是generate时,直接调用匿名lambda函数(lambda()(set! x (rand-update x)) x) ,最后返回随机值。而当第一个参数是reset时,返回匿名函数(lambda(init) (set! x init) (set! x (rand-update x)) x),这个匿名函数接受一个新的初始值,并赋值给x,然后调用rand-update

习题3.7,引入赋值的代价就是引入了副作用以及相应的复杂性,3.3小节提出了命令式语言与函数式语言的基本差别。这一题,首先修改习题3.3(参见《sicp 3.1小结习题尝试解答》),增加一个检查密码是否正确的功能,用以检查输入的原始帐户密码是否正确,make-account修改一下
;习题3.3
(define (make-account2 balance passwd)
  (define (checkpwd pwd)
    (eq? pwd passwd))
  (define (withdraw amount)
    (if (>= balance amount)
        (begin (set! balance (- balance amount)) balance)
        "余额不足"))
  (define (deposit amount)
    (set! balance (+ balance amount))
    balance)
  (define (dispatch pwd m)
    (if (eq? pwd passwd)
        (cond ((eq? m 'withdraw) withdraw)
              ((eq? m 'deposit) deposit)
              ((eq? m 'checkpwd) checkpwd)
            (else
               (error "Unknow request--MAKE-ACCOUNT" m)))
        (lambda(x) "Incorrect password")))
        
  dispatch)

那么,make-joint可以写为:
(define (make-joint account-name account-pass new-pass)
  (if (account-name 'checkpwd account-pass)
      (lambda (passwd m)
      (if (eq? new-pass passwd)
          (account-name account-pass m)
           (error "Incorrect password")))
      (error
         "Incorrect password to the original account")))

首先是检查原始帐户的密码是否正确,正确返回匿名函数(两个参数passwd m),此匿名函数检查密码以及调用原始帐户操作;如果不正确就提示消息。
测试一下:
> (define dennis-acc (make-account2 100 '123))
> (define zane-acc (make-joint dennis-acc '123 'abc))
> ((dennis-acc '123 'withdraw) 10)
90
>  ((zane-acc 'abc 'withdraw) 10)
80


习题3.8,这一题比较简单了,在内部维持一个状态变量即可
(define f
  (let ((y 1))
    (lambda(x) (set! y (* x y)) y)))
测试可知,在(f 1) (f 0)执行的顺序不同时,返回的值不同。


posted @ 2007-08-01 15:45 dennis 阅读(396) | 评论 (0)编辑 收藏

    知道这个模式还是通过《重构》,这个模式的出现还是了为了解决代码重复的坏味道。在项目中很经常见到类似下面这样的代码:
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也可以为单元测试带来简便。

posted @ 2007-07-31 17:48 dennis 阅读(5132) | 评论 (7)编辑 收藏

    亚洲杯我没怎么关注,昨天本来是想找找看能不能看到酋长杯的转播,可这地方收不到广东体育台,于是我就看亚洲杯决赛了,没想到还真值了,非常精彩,好久没有这样忘情地看一场比赛了。谁也没有想到国内没有职业联赛,自己的国家还在战火中煎熬,队员的亲人近几日在恐怖袭击中丧生,教练是刚接手球队两个月,一开始就没有人看好他们的伊拉克队闯进了亚洲杯的决赛,他们已经创造历史,他们已经亲手缔造了奇迹,不管决赛的结果如何。相比于他们,中国男子足球队应该找块豆腐撞死,不,应该解散他们。
    还是回到比赛,比赛中我一个细节让我印象深刻,伊拉克的一个后防队员奋不顾身地用头去解围一个低空球,沙特的队员的脚堪堪从他的耳朵旁大力射门划过,解说员说伊拉克人真是拼了,连受伤都不怕了,那时我想,对于伊拉克队员们来说,死亡都不可怕,更何况是区区的受伤。他们是在用灵魂在战斗,他们希望用胜利来告慰苦难中的祖国和逝去的生命,这已经不是一场普通的球赛,这也不是一场战争,让我想到的词语竟然是悲剧,一幕让人激昂而又悲哀的悲剧。

posted @ 2007-07-30 13:10 dennis 阅读(218) | 评论 (0)编辑 收藏

仅列出标题
共56页: First 上一页 35 36 37 38 39 40 41 42 43 下一页 Last