最近读了读图灵的《.NET2.0模式开发实战》,里面介绍了一种不常见的设计模式Pipes-and-Filters模式,整体讲解还是很不错,但是配书的代码你的程序组织很乱,无法正常调试。到网上寻找,在CodeProject上发现一篇介绍此模式的文章,看了后感觉不错,而且代码可以正常运行。空闲时间将其翻译了一下,E文不好朋友可以参考一下,E文好的朋友移步原文。同时文中也提到了《Foundations of Object-Oriented Programming Using .NET 2.0 Patterns》有此书的朋友看书即可,两者大致相同。本文还介绍了Decorator模式及这两种模式之间的关系,李大哥的文章已经把Decorator模式讲的很明白了,这部分的翻译省略,只翻译两种模式关系部分!
Bashiiui著
本文介绍Pipes and Filters与Decorator两种设计模式的实现。
介绍
本文介绍了使用Pipes-and-Filters模式与Decorator模式来实现一个简单问题的方法,并探索他们可能存在的联系。
Pipes-and-Filters模式的意图
Pipes-and-Filters模式给系统现有组件提供了一种处理数据流(过滤)及将传送数据的相邻组件(管道)连接起来的结构。这种架构提供了可重用性,可维护性,并将系统进程中拥有不同的,易识别的,独立但有些复杂的任务分离。
Pipes-and-Filters模式的使用被限制在过滤器的处理顺序是以自然顺序强制定义的系统中。该模式应用到的问题应该可以自然的分解为一系列一定程度上独立的任务。在管道模式中,这些一定程度上独立的任务正代表了管道的处理阶段。管道的结构是静态的,且连续的阶段之间的迭代是有规律的且有松散的同步性。管道是完成一个业务功能的步骤/任务的定义。每一步可能包括读写确认"管道状态"的数据,也有可能要访问一个外部服务。每一个步骤中需要异步调用一个服务,管道可以等待直到响应数据返回(如果响应数据是被期待的),如果响应数据对继续下面的处理不是必需的,则会继续处理管道中下一个任务。
在以下场合会使用管道模式:
管道模式的优势在于:
管道模式存在以下缺点:
背景
过滤器
过滤器是管道的处理单元。一个过滤器可能会丰富,改进,处理或改变输入数据的格式。
管
管是数据源到第一个过滤器,过滤器之间及最后一个过滤器和数据接收器这些链接之间的连接器。作为必须的,一个管同步这些动态的元素从而它们连接在一起。
数据
数据源是一个给系统提供输入数据的实体(如文件或输入设备)它即可以主动的将数据送入管道中,也可以当收到请求时被动的提供数据。
数据接收器
数据接收器是一个在管道末端收集数据的实体,它可以动态地由最后一个过滤器获取数据,也可以被动的相应最后一个过滤器的请求。
具体实现
我们可以考虑实现泛型过滤器作为一个处理数据的组件。IComponent基础接口定义如下:
We may consider the generic filter as a component that processes data. The basic interface could be defined as follows:
public interface IComponent<T>
{
bool Process(T t);
}
整个例子中,我使用'T'作为泛型类型的参数。输入输出流所使用的接口为InputStream和OutputStream。
public interface InputStream<T> : IEnumerable<T>
{
bool Available();
T Read();
ulong Read(out T[] Data, ulong offset, ulong length);
void Reset();
void Skip(ulong offset);
}
public interface OutputStream<T>
{
void Flush();
void Write(T data);
void Write(T[] Data);
void Write(T[] Data, ulong offset, ulong length);
}
public interface IStreamingControl<T>
{
InputStream<T> Input
{
get; set;
}
OutputStream<T> Output
{
get; set;
}
InputStream<T> FactoryInputStream();
OutputStream<T> FactoryOutputStream();
}
现在,定义具体实现类StreamingControlImpl来实现IstreamingControl如下:
public class StreamingControlImpl<T> :
IStreamingControl<T> where T : new()
public interface IComponentStreaming<T> :
IComponent<StreamingControlImpl<T>> where T : new()
{
bool Process(InputStream<T> input, OutputStream<T> output);
}
public abstract class InputSource<T> :
StreamingComponentBase<T> where T : new()
{
public const bool isOutputSink = false;
public abstract bool Process(OutputStream<T> output);
public override bool Process(InputStream<T> input,
OutputStream<T> output)
{
foreach (T t in input)
output.Write(t);
return Process(output);
}
}
public abstract class OutputSink<T> :
StreamingComponentBase<T> where T : new()
{
public const bool isOutputSink = true;
public abstract bool Process(InputStream<T> input);
public override bool Process(InputStream<T> input,
OutputStream<T> output)
{
return Process(input);
}
}
我们有两种处理类型可选择:顺序处理和管道处理。用异步方式将组件连接在一起允许此链条中每一个单元在自己的线程或进程中来执行。当一个单元完成一个过滤器的处理,它可以将数据送到输出流并直接开始处理另一个数据,它不需要等待随后的组件读取并处理数据。这允许多种的数据消息在进入它们各自独立的阶段时被同时处理。例如,下一个消息可以被加密。我们称这种配置为处理管道,因为消息经过过滤器正如液体流过一个管道。与严格的顺序处理过程相比较,管道处理模型可以极大的提升系统的吞吐量。这里我们仅讨论顺序处理模型。
管道接口定义如下:
public interface IPipeline<T>
{
void AddLink(IComponent<T> link);
bool Process(T t);
}
LinearPipeline实现了IPipeline接口,以顺序方式处理过滤器。在处理过滤器的规则上有两种方式,通过以下枚举定义:
public enum OPTYPE { AND, OR };
如果管道中任一个过滤器的处理发生错误并且/或者返回false,处理会停止而不再执行上述的过滤器,并且管道会返回false到调用程序,在AND操作符方式下进行的管道处理如下:
if (_optype == OPTYPE.AND)
{
result = true;
foreach (IComponent<T> link in _links)
{
if (! link.Process(t))
{
result = false;
break;
}
}
}
如果管道中任何过滤器的处理都返回true,则管道返回true给调用程序。管道继续处理每一个过滤器而不管当前处理的过滤器返回的结果,在OR操作符方式下进行的管道处理如下:
if (_optype == OPTYPE.OR)
{
result = false;
foreach (IComponent<T> link in _links)
{
if (link.Process(t))
{
result = true;
//break;
}
}
}
不同的过滤器完成不同的工作,因此被实现为不同的类。下面是管道中5个过滤器的列表:
下面的类描述了LoadFileFilter任务的实现:
internal class LoadFileFliter : InputSource<Item>
{
private string _path;
public LoadFileFliter(string path)
{
_path = path;
}
public override bool Process(OutputStream<Item> output)
{
try
{
if (File.Exists(_path))
{
string buffer;
// 创建一个StreamReader的实例来由文件读取数据。
// using语法同样可以关闭StreamReader。
using (StreamReader sr = new StreamReader(_path))
{
buffer = sr.ReadToEnd();
}
buffer = buffer.Replace("\r\n", " ");
Tokens Tokenizer = new Tokens(buffer, new char[] { ' ', ';', ',' });
foreach (string Token in Tokenizer)
{
if (Token == string.Empty) continue;
System.Diagnostics.Debug.Write(Token);
output.Write(new Item(Token));
}
return true;
}
else
throw new Exception("File could not be read");
}
catch (Exception ex)
{
System.Diagnostics.Debug.Write(ex.Message);
}
return false;
}
}
由于每一个过滤器都暴露了一个非常简单的接口,它由进入其的管道获取消息,处理消息,并将结果发布到输出管道。管道将一个过滤器连接到下一个,由一个过滤器向下一个发送输出消息。因为,所有的组件使用相同的外部接口。
通过将组件连接到不同的管道,它们可以构成不同的解决方案,我们可以添加新的过滤器,移除现有的,或者以一个新的顺序重新安排它们 – 这些都不需要改变过滤器本身。下面是一个主要的Invoke类,它定义了过滤器的执行顺序并将其放入管道中处理。
public static void Invoke()
{
LinearPipeline<StreamingControlImpl<Item>> pipeline =
new LinearPipeline<StreamingControlImpl<Item>>(/*OPTYPE.OR*/);
pipeline.AddLink(Factory<Item>.CreateLoadFile(@"c:\a.log"));
pipeline.AddLink(Factory<Item>.CreateStartWithSearch("wa"));
pipeline.AddLink(Factory<Item>.CreateWordLengthSearch(9));
pipeline.AddLink(Factory<Item>.CreatePalindromeSearch());
pipeline.AddLink(Factory<Item>.CreateOutput());
StreamingControlImpl<Item> controldata =
new StreamingControlImpl<Item>();
bool done = pipeline.Process(controldata);
if (done)
System.Diagnostics.Debug.Write("All filters " +
"processed successfully");
}
当读入文件并标记它来准备数据流时,我想使用内建的string类型作为类型参数,但是使用StreamingControlImpl<string>会产生一个编译错误,这是由于继承层次中的每个上面等级的泛型约束where T : new()要求"string"类型有一个无参的构造函数。由于string类型不具有无参构造函数且它是一个密封类,我们不能创建一个继承自string的类。要解决这个问题而不引入is-a关系,我们可以实现一个拥有has-a关系的新类Item,并使用Item.Next属性获取值!没什么大不了的:
internal class Item
{
private string _text = null;
public string Text
{
get { return _text; }
set { _text = value; }
}
public Item()
{
_text = string.Empty;
}
public Item(string t)
{
_text = t;
}
}
下面的类图解释了Pipes and Filters模式的整个概念:
本类图visio源文件下载
Pipes and Filters模式的缺点
Pipes-and-filters模式存在以下缺点[Buschmann]:
比较
Pipes-and-Filters的好处
Pipes-and-filters模式有以下好处[Buschmann]:
pipeline.AddLink(Factory<Item>.CreateLoadFile(@"c:\a.log"));
pipeline.AddLink(Factory<Item>.CreateStartWithSearch("wa"));
pipeline.AddLink(Factory<Item>.CreateWordLengthSearch(9));
pipeline.AddLink(Factory<Item>.CreatePalindromeSearch());
pipeline.AddLink(Factory<Item>.CreateOutput());
pipeline.Process();
装饰模式的优势
装饰模式有如下好处:
IComponent<Item> S = new FileLoader(".log");
S = new StartWithSearch(S, "wa");
S = new WordLengthSearch(S, 5);
S = new PalindromeSearch(S);
S = new Output(S);
S.Process();
结论
促使我的基本思想是一方面动态安排过滤器,另一方面职责(包装器)的动态可插拔能力。如果在Pipes and Filters中我不讨论并行管道处理,你会看到顺序处理的Pipes and Filters会很好的映射到Decorator模式。所以在下面场景中它们是有关联的模式,在Pipes and Filters中,管是无状态的,引导数据流穿过多个过滤器,同时过滤器作为流修改器处理输入流并将结果输出流送到下一个过滤器的管中。在Decorator模式中,我们可以将装饰者看作过滤器。
本文中涉及的代码可以在原文章页面下载。