管道式编程:我原来是个水管工 (一)
昨天工作不忙,脑袋又开始抽抽了,从linux shell的管道想到java的piped stream,从.net的工作流想到了自来水管,从自来水管想到了一个城市复杂的下水道系统( 这都想的啥:( )。反正不管怎么说,最后还是相处了点东西,跟大家扯扯淡。
作为一个程序员,我们写代码的时候经常会发现越写越乱,这一般是没有做好设计的缘故。怎么设计,简单的来说就是画一些流程图啊、类图什么的。不过有时候画了也不顶事,搞到最后还是很乱。其实我觉得画不画图无所谓,关键是脑子里要想清楚,想的太乱的时候可以画画图理一理思维,但最终还是要在脑子里搭个模型出来。软件系统说白了就是一个数据处理器,一坨数据进去,一坨数据出来。计算机最擅长的就是这个,话说回来它也只能干这个,别的也不会。基于此,我们也可以把某些软件系统看成一个下水道系统,数据就像污水,从各个地方流进来,最后汇总啊,处理啊,再最后就折腾到河里去了。当然也可以把某些软件系统看成一个供水系统,水从水库经过折腾,通过各种管子最后接到了你家的水龙头里面。下面的图演示了这两种结构。
在污水处理系统里面,下水道可以看做初始输入,河是最终输出,中间的污水管和接头,以及污水处理厂都可以看做一个整体,抽象一下就是一个软件系统。在自来水系统里面,水库可以看做初始输入,水龙头是最终输出,中间的水管、自来水厂和接头也可以看做一个整体,同样可以抽象成一个软件系统。总而言之,我们这里的计算机就是排除掉初始输入和最终输出的中间的数据处理系统。基于此抽象,我们发现软件系统其实是很多管子连起来的。从另外一种角度讲,软件系统里面只有两种玩意,一个是管子,一个是接头,污水管和水管都可以看成管子(pipe),污水处理厂、自来水厂和普通的接头我们都可以称之为连接头(joint)。进一步讲,程序员就像水管工,一部分负责造管子和连接头,其他的负责把这些管子和连接头连起来构成软件系统。或者说编程变成了两种活,一是造管子和连接头,二是连管子和连接头。
好了,淡也扯的差不多了,开始干活,也就是用代码实现上面的东西,我们先从造管子和连接头开始。从上面的图我们可以看出来,管子可以分三种:
1. 输入管道。污水处理系统里面那个连接下水道和接头的管子就是输入管道,因为下水道属于软件系统之外,所以管子从外部有输入,但是污水流入的接头在软件系统里面,所以没输出。
2. 输出管道。 自来水系统里面连接水龙头的那个管子就是输出管道,水龙头属于软件系统之外,所以管子向外部有输出,但是连接的接头在软件系统里,所以没输入。
3. 中间管道。在软件系统里面负责连接两个连接头,对外部无任何输入和输出。
当然,上面的分类是基于管子对外部的可见性而言的,实际上,每个管子都有头和尾,也就是输入和输出,只不过内部的输入输出被实现封装起来了。我们将上面的三种管子变成类,下面是类图:
剩下的就是连接头了,连接头比较简单,只有两种基本功能,一个是塞东西进去,一个是取东西出来,下面是简单的类图:
上面只说了一下基本结构和接口,所以比较简单,下面分析如何实现三种管道和连接头。在我的设计里,管道属于主动行为者,需要通过操作连接头推动或拉动数据的流动,而连接头一般只通过暴露一些操作给管道,比较被动,所以我们先实现连接头。(后面会提到一种主动的连接头,待会再说)。
1. 连接头:
/// <summary>
/// 连接头
/// </summary>
/// <typeparam name="T">管道系统里流动的数据对象</typeparam>
public abstract class Joint<T>
{
/// <summary>
/// 一个数据从输入管道流进连接头
/// </summary>
/// <param name="t">数据对象</param>
internal abstract void Add(T t);
/// <summary>
/// 一个数据从连接头流出到管道
/// </summary>
/// <returns>数据对象</returns>
internal abstract T Get();
}
/**
* 连接头
* @param <T> 管道系统里流动的数据对象
*/
public abstract class Joint<T> {
/** 一个数据从输入管道流进连接头
* @param t 数据对象
*/
abstract void Add(T t);
/** 一个数据从连接头流出到管道
* @return 数据对象
*/
abstract T Get();
}
2. 管道:
a) 输入管道: 构造时必须传入一个连接头作为参数,这样有数据到来时,管道会将数据推送进连接头。
/// <summary>
/// 输入管道
/// </summary>
/// <typeparam name="T"> 数据对象类型</typeparam>
public class InPipe<T>
{
/// <summary>
/// 构造方法
/// </summary>
/// <param name="joint">数据流向的连接头</param>
public InPipe(Joint<T> joint)
{
_outJoint = joint;
}
/// <summary>
/// 向管道塞入一个数据
/// </summary>
/// <param name="t"></param>
public virtual void Push(T t)
{
//将数据直接加入连接头
_outJoint.Add(t);
}
protected Joint<T> _outJoint = null;
}
/**
* 输入管道
* @param <T> 数据对象类型
*/
public class InPipe<T>
{
/** 构造方法
* @param joint 数据流向的连接头
*/
public InPipe(Joint<T> joint)
{
_outJoint = joint;
}
/** 向管道塞入一个数据
* @param t
*/
public void Push(T t)
{
//将数据直接加入连接头
_outJoint.Add(t);
}
protected Joint<T> _outJoint = null;
}
b) 输出管道: 构造时也必须传入一个连接头作为参数,管道需要读数据时会从连接头获取。
/// <summary>
/// 输出管道
/// </summary>
/// <typeparam name="T"></typeparam>
public class OutPipe<T>
{
/// <summary>
/// 构造方法
/// </summary>
/// <param name="joint">数据流入的连接头</param>
public OutPipe(Joint<T> joint)
{
_inJoint = joint;
}
/// <summary>
/// 从管道获取一个数据
/// </summary>
/// <returns></returns>
public virtual T Pop()
{
//从连接头获取一个数据返回
return _inJoint.Get();
}
protected Joint<T> _inJoint = null;
}
/** 输出管道
* @param <T> 数据对象类型
*/
public class OutPipe<T> {
/** 构造方法
* @param joint 数据流入的连接头
*/
public OutPipe(Joint<T> joint) {
_inJoint = joint;
}
/** 从管道获取一个数据
* @return
*/
public T Pop() {
//从连接头获取一个数据返回
return _inJoint.Get();
}
protected Joint<T> _inJoint = null;
}
c) 中间管道: 构造时必须传入两个连接头,一个用来获取数据,一个用来塞入数据。
/// <summary>
/// 中间管道
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
public abstract class MidPipe<T>
{
/// <summary>
/// 构造方法
/// </summary>
/// <param name="inJoint">流入连接头</param>
/// <param name="outJoint">流出连接头</param>
public MidPipe(Joint<T> inJoint, Joint<T> outJoint)
{
_inJoint = inJoint;
_outJoint = outJoint;
}
protected Joint<T> _inJoint = null;
protected Joint<T> _outJoint = null;
}
/** 中间管道
* @author shun.li
*
* @param <T> 数据类型
*/
public abstract class MidPipe<T> {
/** 构造方法
* @param inJoint 流入连接头
* @param outJoint 流出连接头
*/
public MidPipe(Joint<T> inJoint, Joint<T> outJoint) {
_inJoint = inJoint;
_outJoint = outJoint;
}
protected Joint<T> _inJoint = null;
protected Joint<T> _outJoint = null;
}
线程安全连接头:上面的Joint只是抽象类,你可以用一个简单的队列实现之,也可以封装msmq或0mq之类的实现比较复杂的功能。下面提供一个简单的实现,此实现保证了线程安全性,所以可以同时连接多个输入管道或输出管道。
/// <summary>
/// 线程安全连接头
/// </summary>
/// <typeparam name="T"></typeparam>
public class ConcurrentJoint<T> : Joint<T>
{
internal override void Add(T t)
{
lock (_queue)
{
_queue.Enqueue(t);
}
}
internal override T Get()
{
lock (_queue)
{
return _queue.Dequeue();
}
}
private Queue<T> _queue = new Queue<T>();
}
/** 线程安全连接头
* @param <T>
*/
public class ConcurrentJoint<T> extends Joint<T> {
@Override
void Add(T t) {
synchronized (_queue) {
_queue.add(t);
}
}
@Override
T Get() {
synchronized (_queue) {
return _queue.remove();
}
}
private Queue<T> _queue = new LinkedList<T>();
}
推拉模式:除了连接头外,MidPipe也是个抽象类。如果不是,内部管道其实什么都做不了,因为无论是别人想要数据,还是别人给它数据,它都无法知道。管道内的数据流动模式有两种:
1. 推。污水系统就是一个推的管道系统,人们把污水倒入,污水会主动流动,知道最终流入河里。
2. 拉。供水系统则相反,是一个拉的管道系统,人们打开水龙头,水才开始从源头流向水龙头。
下面提供两种实现,注意每种模式都对应一套内部节点和连接头。
1. 推实现:
/// <summary>
/// 推模式连接头
/// </summary>
/// <typeparam name="T"></typeparam>
public class PushJoint<T> : Joint<T>
{
internal override void Add(T t)
{
lock(_queue)
{
_queue.Enqueue(t);
}
//:通知别人有数据进入
if (_addCallback != null)
{
_addCallback();
}
}
internal override T Get()
{
lock (_queue)
{
return _queue.Dequeue();
}
}
/// <summary>
/// 设置数据塞入回调方法
/// </summary>
public Action AddCallback
{
set { _addCallback = value; }
}
private Queue<T> _queue = new Queue<T>();
private Action _addCallback = null;
}
/// <summary>
/// 推模式中间管道
/// </summary>
/// <typeparam name="T"></typeparam>
public class PushMidPipe<T> : MidPipe<T>
{
public PushMidPipe(PushJoint<T> inJoint, Joint<T> outJoint)
: base(inJoint, outJoint)
{
inJoint.AddCallback = Flush;
}
/// <summary>
/// 刷新方法
/// </summary>
private void Flush()
{
//:从流入连接头获取数据塞入流出连接头
T t = _inJoint.Get();
_outJoint.Add(t);
}
}
/**
* 推模式中间管道
* @param <T>
*/
public class PushJoint<T> extends Joint<T> {
@Override
void Add(T t) {
synchronized(_queue) {
_queue.add(t);
}
//:通知别人有数据进入
if(_addRunnable != null) {
_addRunnable.run();
}
}
@Override
T Get() {
synchronized(_queue) {
return _queue.remove();
}
}
/** 设置塞入新数据时的回调对象
* @param r
*/
void setAddRunnable(Runnable r) {
_addRunnable = r;
}
private Queue<T> _queue = new LinkedList<T>();
private Runnable _addRunnable = null;
}
public class PushMidPipe<T> extends MidPipe<T> implements Runnable {
public PushMidPipe(PushJoint<T> inJoint, Joint<T> outJoint) {
super(inJoint, outJoint);
inJoint.setAddRunnable(this);
}
@Override
public void run() {
//:从流入连接头获取数据塞入流出连接头
T t = _inJoint.Get();
_outJoint.Add(t);
}
}
注意:推模式在应用时有两种情况:
1)完全主动。这种情况下基本的输出管道已经满足不了要求,因为根本没人调用它的Pop方法。以污水系统为例,在完全主动模式下,排放管无人操作,污水留到它的输入连接头时将自动流到河里,而不是等人开一下排污口放水。所以将输出管道改为如下的实现:
/// <summary>
/// 完全主动推模式的输出管道
/// </summary>
/// <typeparam name="T"></typeparam>
public class PushOutPipe<T> : OutPipe<T>
{
public PushOutPipe(PushJoint<T> inJoint)
: base(inJoint)
{
inJoint.AddCallback = Flush;
}
/// <summary>
/// 刷新方法
/// </summary>
private void Flush()
{
//:从流入连接头获取数据塞入流出连接头
T t = _inJoint.Get();
//TODO:下面是处理数据t的代码
}
public override T Pop()
{
throw new InvalidOperationException("主动推模式下无法获取数据");
}
}
/**
* 完全主动推模式的输出管道
*
*/
public class PushOutPipe<T> extends OutPipe<T> implements Runnable {
public PushOutPipe(PushJoint<T> inJoint) {
super(inJoint);
inJoint.setAddRunnable(this);
}
@Override
public void run() {
T t = _inJoint.Get();
//TODO:下面是处理数据t的代码
}
}
2)半主动。这种情况下,数据将会堆积到输出管道的输入连接头。以污水系统为例,污水将堆积在污水处理厂,知道有人主动打开排放管的口子(主动调用OutPipe的Pop),污水才会留到河里。
2. 拉实现:
/// <summary>
/// 拉模式连接头
/// </summary>
/// <typeparam name="T"></typeparam>
public class PullJoint<T> : Joint<T> where T : class
{
internal override void Add(T t)
{
lock (_queue)
{
_queue.Enqueue(t);
}
}
internal override T Get()
{
//:没货了,通知输入管道开始拉货
if (_queue.Count == 0 && _getCallback != null)
{
_getCallback();
}
lock (_queue)
{
if (_queue.Count == 0)
return null;
else
return _queue.Dequeue();
}
}
/// <summary>
/// 设置数据读取回调方法
/// </summary>
public Action GetCallback
{
set { _getCallback = value; }
}
private Queue<T> _queue = new Queue<T>();
private Action _getCallback = null;
}
/// <summary>
/// 拉模式中间管道
/// </summary>
/// <typeparam name="T"></typeparam>
public class PullMidPipe<T> : MidPipe<T> where T : class
{
public PullMidPipe(Joint<T> inJoint, PullJoint<T> outJoint)
: base(inJoint, outJoint)
{
outJoint.GetCallback = PullData;
}
/// <summary>
/// 从输入队列拉数据
/// </summary>
private void PullData()
{
//:从流入连接头获取数据塞入流出连接头
T t = _inJoint.Get();
_outJoint.Add(t);
}
}
/** 拉模式连接头
* @param <T>
*/
public class PullJoint<T> extends Joint<T> {
@Override
void Add(T t) {
synchronized(_queue) {
_queue.add(t);
}
}
@Override
T Get() {
//:没货了,通知输入管道开始拉货
if(_queue.size() == 0 && _getRunnable != null) {
_getRunnable.run();
}
synchronized(_queue) {
return _queue.remove();
}
}
/** 设置需要数据时的回调对象
* @param r
*/
void setGetRunnable(Runnable r) {
_getRunnable = r;
}
private Queue<T> _queue = new LinkedList<T>();
private Runnable _getRunnable = null;
}
/**
* 完全主动推模式的输出管道
*
*/
public class PushOutPipe<T> extends OutPipe<T> implements Runnable {
public PushOutPipe(PushJoint<T> inJoint) {
super(inJoint);
inJoint.setGetRunnable(this);
}
@Override
public void run() {
T t = _inJoint.Get();
//TODO:下面是处理数据t的代码
}
@Override
public T Pop() {
throw new RuntimeException("主动推模式下无法获取数据");
}
}
注意:拉模式在应用时也有两种情况:
1)完全主动。这种情况下基本的输入管道已经满足不了要求,因为根本没人调用它的Push方法。以供水系统为例,在完全主动模式下,自来水厂的输入管完全自动化,当自来水厂需要水时水将自动从水库流入自来水厂。所以将输入管道改为如下的实现:
/// <summary>
/// 完全主动拉模式的输入管道
/// </summary>
/// <typeparam name="T"></typeparam>
public class PullOutPipe<T> : InPipe<T>
{
public PullOutPipe(PullJoint<T> outJoint)
: base(outJoint)
{
outJoint.GetCallback = PullData;
}
private void PullData()
{
//TODO:下面是获取数据的代码
T t = default(T);
//将数据传递下去
_outJoint.Add(t);
}
public override void Push(T t)
{
throw new InvalidOperationException("主动拉模式下无法塞入数据");
}
}
/**
* 完全主动拉模式的输入管道
*
*/
public class PullInPipe<T> extends InPipe<T> implements Runnable {
public PullInPipe(PullJoint<T> outJoint) {
super(outJoint);
outJoint.setGetRunnable(this);
}
@Override
public void run() {
//TODO:下面是获取数据的代码
T t = null;
//将数据传递下去
_outJoint.Add(t);
}
@Override
public void Push(T t) {
throw new RuntimeException("主动拉模式下无法塞入数据");
}
}
2)半主动。 这种情况和推模式类似,用一般的输入管道就可以了。
本篇只扯了一些理论,下一篇将给出更多实用的例子。