Thursday, February 28, 2008

浅谈静态OO设计的陷阱

静态OO设计 是指 面向使用C++/C#/Java这些静态类型OO语言 的项目 进行的设计.这种设计很容易陷进一些陷阱.

这些陷阱是:

1. is-a 的类型系统和 like-a 的真实世界不匹配的陷阱

静态类型系统推崇的编译时类型检查机制,实际上是强调了消息的接受者必须是某一特定类型.

举个例子,比如某人需要购买一部手机.在真实世界里,去商场购买和从淘宝购买,都是实现需求的合理手段.

如果,用静态OO语言来描述,就是:

商店->买(手机型号,现金对象&,);

淘宝网->买(手机型号,信用卡对象&);

对于"商店"和"淘宝网"这样两个对象,很难抽象出一个统一基类.

对于"买/2"这个方法,也很难抽象出一个接口.(void*不算哈).

但是这是一个真实的世界的实际存在的场景.

那么, "人" 这个类的购买手机方法怎么定义呢?

对于静态类型系统确实是个难题.

如果,把"买"看成是对商店和淘宝网的一个消息,静态类型系统要求的就是"淘宝网"和"商店"必须继承/实现同一个接口,如:

class 商店 : 可以接受"买"消息的接口 IBuy

{};

class 淘宝网 : 可以接受"买"消息的接口 IBuy

{};

class 人

{

bool 购买手机(购买方式* pIBuy);

};

这样的方法在消息的数量少的时候,是可行的.但是,真实世界里,对象的功能无法穷尽,消息的种类也是无穷无尽.

最后,在静态类型OO设计里,不得不为了每一个消息定义一个接口.也就是常见的,一个命令字一个类.这在一些过度设计的服务器程序的消息分派模块中很常见.

通常认为,一个消息一个类,实现了单一职责原则,殊不知,真正,彻底的方式就是不用接口.

为什么说,可以不用接口呢?

因为,所有的函数的作用都是通过由参数,返回值和副作用三者的组合来实现的.接口实际只是一个调用约定.

如果,我们能够保证,对不符合 调用约定的对象 发送 其不能识别的消息 的结果 是可控的话,这种事先约定就变成了一个负担.

改成函数指针的形式就是:

class 人

{

bool 购买手机(FUNC_POINTER* pBuy)// FUNC_POINTER 是预定义的一个函数指针类型

{

return pBuy(...);// 但是这么做隐含着一个风险,就是穿错了参数,会导致程序崩溃,要解决这个问题只能通过引入动态类型系统,来保证消息传递的安全性.

}

};

例如: 有的单根静态类型语言在事件机制中,这样定义 事件参数

class 事件参数类

{

对象指针 sender;

对象指针 arg_obj;

};

这里其实是为了规避类型系统的限制.如果用C语言描述就是这样:

struct EventArg

{

void* pSender;

void* pArg;

}

这里静态系统的困难时原发性的. "void*"或者"object引用"的使用,就是静态类型系统对无穷类型的真实世界的投降.

归根到底,是静态类型检查的有效性只在简单的场景有效率.

在复杂系统中,为了满足静态类型系统的血盆大口,只有两个选择,

1. 要不像 Boost 那样用模板让 模板系统去生产代码喂饱静态类型系统;

2. 要不像 C 程序员的风格 和静态类型系统耍无赖,用 void * 瞒天过海.

显然,

第一种方法是用一种更加复杂的复杂性取代原先的复杂性,问题只会越来越复杂,而且在设计和调试效率上都是很低效的.

第二种方法,和不用静态类型系统没什么差别.

说到这里,算是讲清楚了第一个矛盾.动态类型系统好处在这里也就不表了,大家可以参考.

2. 串行的命令式语义(同步函数调用)和无穷并发的真实世界不匹配的陷阱

真实世界的每个对象都是并发的运作着.大部分情况下,对象和对象间没有一种串行的关系.但是,在命令式OOP语言中,函数调用是串行的(阻塞的),状态(内存)是共享的,而不是像每个人的大脑那样是独立的.

这必然要求,在做设计的时候进行一个映射 从 并发真实世界 到 串行命令世界 的映射.

然而,所有的抽象都有损失.

这种并行到串行的映射引入了两个问题:

1. 命令式OOP语言为了实现并发引入了高阶的并发模型,比如线程.在这种模型中,状态在各个执行绪之间共享,好比让一群人公用一个草稿本进行代数演算.问题是显而易见的.为了抢草稿本的使用权,浪费大量时间,如果有人人不守规矩在本子上乱涂乱画,所有的人的工作都进行不下去了.

2. 并发世界的消息大部分是无严格先后顺序的,在状态共享的串行命令式系统中,所有消息必须排队(在一个线程里,你不可能同时执行两个函数),即使不是必须串行的消息.这就使得,当一个命令需要很长时间完成的时候,其他命令不得不等待,这不但低效而且有的时候是不可以接受的.

为了利用多个CPU Core的计算能力,有的设计是通过一个树状锁的数据结构来实现多线程互斥访问临界数据.结果,不但容易遇到死锁问题,而且随着CPU Core数量的增加,锁碰撞几率节节升高,其效率并不理想.

解决这个问题的方法,有两个:

1. 在硬件上通过事务内存来提高效率,但是还是解决不了设计上必须经行并串映射的复杂度.

2. 函数式编程模型,使用像Erlang/OTP 这种FP的平台.

Friday, February 22, 2008

[Erlang] tcp_server.erl 详细注释

首先请注意下 receive 关键字出现的次数

Copyright (C) 2002, Joe Armstrong

File : tcp_server.erl

Author : Joe Armstrong (joe@sics.se)

Purpose : Keeps track of a number of TCP sessions

Last modified: 2002-11-17

-module(tcp_server).

-export([start_raw_server/4, start_client/3,
stop/1, children/1]).

-define(KILL_DELAY, 1000).

%% -export([start_child/3]).

%% start_raw_server(Port, Fun, Max)
%% This server accepts up to Max connections on Port
%% The *first* time a connection is made to Port
%% Then Fun(Socket) is called.
%% Thereafter messages to the socket result in messsages to the handler.

%% a typical server is usually written like this:

%% To setup a lister

%% start_server(Port) ->
%% S = self(),
%% process_flag(trap_exit, true),
%% tcp_server:start_raw_server(Port,
%% fun(Socket) -> input_handler(Socket, S) end,
%% 15,
%% 0)
%% loop().

%% The loop() process is a central controller that all
%% processes can use to synchronize amongst themselfves if necessary
%% It ends up as the variable "Controller" in the input_handler

%% A typical server is written like this:

%% input_handler(Socket, Controller) ->
%% receive
%% {tcp, Socket, Bin} ->
%% ...
%% gen_tcp:send(Socket, ...)
%%
%% {tcp_closed, Socket} ->
%%
%%
%% Any ->
%% ...
%%
%% end.

start_client(Host, Port, Length) -> % 做客户端连别人
gen_tcp:connect(Host, Port,
[binary,
{active, true},
{packet, 2},
{packet_size, Length}], 30000).

%% Note when start_raw_server returns, it should be ready to
%% Immediately accept connections
%% 模块的入口函数
start_raw_server(Port, Fun, Max, Length) -> % FUN=>Handler Max=>最大连接数 Lengt=>最大包长
Name = port_name(Port), % 用端口号来生成进程名
case whereis(Name) of % 判断是不是 已经定义过了 ,注意:这里Name是已经绑定的
undefined -> % 如果端口还未定义的话,则启动
Self = self(), % 这是一个惯用法
Pid = spawn_link(fun() -> % 产生一个新进程
cold_start(Self, Port, Fun, Max, Length) % 冷启动,注意下FUN的调用位置,这个FUN是用户传入的一个函数,用来处理新的Socket,Self 在cold_start里面将被称为Master
end),
receive
{Pid, ok} -> % 这个消息是期待 cold_start 发来的
register(Name, Pid), % 注册进程成为知名进程,方便通过名字来发消息
{ok, Pid};
{Pid, Error} -> % 出错了,其实可以省略,嘿嘿
Error
end;
_Pid -> % 如果端口已定义,则报错
{error, already_started} % 把错误返回给调用者, 开端口失败
end.

stop(Port) when integer(Port) ->
Name = port_name(Port),
case whereis(Name) of
undefined ->
not_started;
Pid ->
exit(Pid, kill), % 这里是强制进程退出,注意: 服务器是跑在另外的一个进程里面的
(catch unregister(Name)), % 去注册进程名
stopped
end.

children(Port) when integer(Port) ->
port_name(Port) ! {children, self()}, % 这个是通过进程名来发消息

receive
{session_server, Reply} -> Reply % Reply应该去的活动链接列表 Active
end.


port_name(Port) when integer(Port) ->
list_to_atom("portServer" ++ integer_to_list(Port)). % 一个简单的助手函数,根据端口号生成进程名

% 监听进程的入口函数
cold_start(Master, Port, Fun, Max, Length) -> % 这个函数是端口监听进程的入口函数
process_flag(trap_exit, true), % 设置退出陷阱,不设置进程会被EXIT信号终止
io:format("Starting a port server on ~p...~n",[Port]),
% 绑定到端口,进行监听
case gen_tcp:listen(Port, [binary, % 这里才开始监听端口
%% {dontroute, true},
{nodelay,true}, % 非阻塞
{packet_size, Length}, % 由使用者指定的包最大大小,可以为0
{packet, 2},
{backlog, 1024}, % 未经过处理的连接请求队列可以容纳的最大数目
{reuseaddr, true}, % 可以快速重用地址
{active, false}]) of
{ok, Listen} ->
%% io:format("Listening on:~p~n",[Listen]),
Master ! {self(), ok}, % 发送成功消息
New = start_accept(Listen, Fun), % 开始接受连接 ,注意: Listen就是侦听Socket,Fun用户传入的Handler函数 , start_accept/2 开了一个进程!!! New是新进程的Pid
%% Now we're ready to run
socket_loop(Listen, New, [], Fun, Max); % 接受连接后,开始网络通信了
Error ->
Master ! {self(), Error} % 不认识的消息都转发给主进程处理
end.

%% Don't mess with the following code uless you really know what you're
%% doing (and Thanks to Magnus for heping me get it right)
%% 作用
%% 接受连接后,开始运作
socket_loop(Listen, New, Active, Fun, Max) -> % 服务器循环函数,这个函数凸显的FP风格的精髓
receive % 从进程的消息队列收消息
{istarted, New} -> % 接受了一个新连接
Active1 = [New|Active], % 参见start_child,把新连接加入到活动列表中
possibly_start_another(false, Listen, Active1, Fun, Max);
{'EXIT', New, _Why} -> % 注意: New是已经在参数中绑定成 start_accpet/2 返回的accpet进程Pid了
%%io:format("Child exit=~p~n",[Why]),
possibly_start_another(false, Listen, Active, Fun, Max);
{'EXIT', Pid, _Why} -> % 由用户断开了连接
%%io:format("Child exit=~p~n",[Why]),
Active1 = lists:delete(Pid, Active),
possibly_start_another(New, Listen, Active1, Fun, Max);
{children, From} ->
From ! {session_server, Active}, % 这里对 children 消息做个简单的回应Active是活动链接列表
socket_loop(Listen, New, Active, Fun, Max);
Other ->
io:format("Here in loop:~p~n",[Other])
end.

possibly_start_another(New, Listen, Active, Fun, Max) when pid(New) ->
socket_loop(Listen, New, Active, Fun, Max); % 继续提供服务
possibly_start_another(false, Listen, Active, Fun, Max) ->
case length(Active) of
N when N < Max ->
New = start_accept(Listen, Fun), % 用户已近断开了,等待新连入请求
socket_loop(Listen, New, Active, Fun, Max);
_ -> % 应该是N>=Max了,length的返回值很简单
error_logger:warning_report(
[{module, ?MODULE},
{line, ?LINE},
{message, "Connections maxed out"},
{maximum, Max},
{connected, length(Active)},
{now, now()}]),
socket_loop(Listen, false, Active, Fun, Max) % 继续循环
end.


start_accept(Listen, Fun) ->
S = self(),
spawn_link(fun() -> start_child(S, Listen, Fun) end). % 一个进程处理一个连接 返回进程Pid

start_child(Parent, Listen, Fun) -> % Listen 是侦探Socket, Parent 是调用start_accept/2的进程,也就是侦听进程
case gen_tcp:accept(Listen) of % 接受一个连接
{ok, Socket} ->
Parent ! {istarted,self()}, % 参见socket_loop, tell the controller 新用户连入
inet:setopts(Socket, [{nodelay,true}, % 非阻塞
{packet, 2},
{active, true}]), % before we activate socket
Fun(Socket); % 这里应用了Handler函数Fun
_Other ->
exit(oops) % 其实,不用写这句 :)
end.

本文基于 http://coderplay.javaeye.com/blog/92804 的成果.在此表示感谢.