C++ 的通用管道框架






4.70/5 (10投票s)
通用、可扩展的 C++ 管道框架演示

引言
在软件开发中,“管道”处理是少数几个真实物理概念在软件中有实用对应物的例子之一。另一个流行的例子是将软件想象成“对象”。这些概念类型的优势之一是,因为我们理解它们的物理对应物,所以我们知道如何使用它们,并且很容易理解它们。虽然面向对象编程不仅仅是理解物理对象的概念,但管道确实就是这么简单。
这里定义的管道,就是由管道段连接在一起的不同方式组成的一个集合,根据预先指定的规则,以完成一项任务。在其最简单的形式中,管道只需要一个头部和一个尾部,它们可以是同一个(尽管这通常会破坏目的)。
软件管道可以以多种方式使用。我见过的一些例子是:执行漫长的科学算法,其中每一步都得到清晰的界定;执行抽象的设备读/写,就像缓冲 I/O(网络、硬盘等)一样;以及在图形处理中。
背景
作为“接口即契约”思想的忠实拥护者,当我开始设计管道时,我希望建立一个连接两个对象的简单方法,同时在整个过程中保持接口级别的自由。在这个上下文中,将一个对象连接到另一个对象——通过 object1.connectTo(&object2)
方法,或使用重载的 object1 += object2
运算符——等同于告诉一个对象(object1
)将其输出发送到另一个对象(object2
)。
如果您不熟悉“接口即契约”范例,我将给您我的简要概述。它表明,当您开始一个软件开发项目时,您应该考虑的一个方面是接口定义。在您认为会发生变化/扩展的区域定义接口——换句话说(不是我的话),“封装变化”。如果您在软件设计中包含良好的接口,您将多次受益。这在管道设计中尤其有用,通常最好在每个连接点定义一个接口。然后,用户可以实现他们有兴趣参与的特定接口(同意一个契约),并无缝地将其连接到管道中。
Using the Code
该框架提供了一个单独的类来定义管道中的一个段,并且可以在编译时确定它是否能够成功连接到另一个管道段。用户可以指定一个类、基类或接口(抽象
类),它定义了他的管道段可以连接到的类的类型。管道段可以同时连接到其他管道段,也可以同时有其他管道段连接到它。
由于提供的演示应用程序是有史以来最无聊的应用程序,而且我不希望我的文章因为能够治愈失眠而获得赞誉,我将展示如何使用该框架构建一个稍微更有趣的、纯粹假设的 HTTP 服务器请求处理管道。请注意,这里不会有任何服务器的实现(这段代码将无法编译),只有下面这个极其简化的四步管道:
- 接收请求并启动管道
- 身份验证请求
- 授权请求
- 响应请求
我们的 HTTP 服务器处理管道将包含每个步骤的一个管道段
HTTPRequestHandler
- 通过简单地将其发送到管道来响应传入的HTTPRequest
对象。HTTPRequestAuthenticator
- 接受HTTPRequest
对象,进行身份验证,并在适当的时候将其传递到管道。HTTPRequestAuthorizer
- 接受HTTPRequest
,进行授权,并在适当的时候将其传递到管道。HTTPRequestResponder
- 接受HTTPRequest
,并以适当的方式对其进行响应。
这个示例的绝妙之处在于,为了完成这项巨大的编码壮举,我们只需要定义一个接口,上面的每个类都将实现该接口。
- 一个
IHTTPRequestHandler
接口,定义了一个名为HandleHTTPRequest(HTTPRequest *theRequest)
的单一方法。
我不会讨论 HTTPRequest
类的内部细节,我只会假设它已经存在,我也不会对进行 HTTP 请求身份验证或类似事项的正确方法进行长篇大论。请不要写信告诉我这不是编写 HTTP 服务器的正确方法。我确定这还有很多其他的方面。
我们在这里的目标是,在 HTTP 服务器处理第一个请求之前执行以下代码。这就是我们构建管道的地方。我意识到,如果从这里复制代码并粘贴实现,可能会导致一些作用域问题——再说一遍,这只是一个示例,让您感受一下。我会让您自己找出如何解决作用域/变量生命周期问题。
...
//Build the individual pipeline segments
HTTPRequestHandler theRequestHandler;
HTTPRequestAuthenticator theAuthenticator;
HTTPRequestAuthorizer theAuthorizer;
HTTPRequestResponder theResponder;
//Attach the individual pipeline segments
theRequestHandler += theAuthenticator;
theAuthenticator += theAuthorizer;
theAuthorizer += theResponder;
//Or, you can attach them all in one line if you like
theRequestHandler.connectTo(theAuthenticator.connectTo
(theAuthorizer.connectTo(theResponder)));
...
希望这相当自明。
那么,我们如何才能实现这一点呢?嗯,首先我们将编写 IHTTPRequestHandler
接口,并将其定义如下:
#include "HTTPRequest.h"
#include "PipeSegmentBaseAdapter.h"
class IHTTPRequestHandler : public PipeLineProcessing::PipeSegmentBaseAdapter
{
public:
virtual void HandleHTTPRequest(HTTPRequest *request)=0;
};
有了这个接口定义,我们就可以开始编写管道段对象了。它们可能看起来像这样:
#include "HTTPRequest.h"
#include "PipeSegment.h"
class HTTPRequestHandler :
public IHTTPRequestHandler, //the object itself "is a" IHTTPRequestHandler
//and it only outputs to IHTTPRequestHandler objects
public PipeLineProcessing::PipeSegment<IHTTPRequestHandler>
{
public:
virtual void HandleHTTPRequest(HTTPRequest *request) {
//Since the HTTPRequestHandler object does nothing besides
//send the request off to any connected request handlers,
//this method simply iterates over the collection of output
//handlers currently "connected to" this object.
for (int i=0; i < (int)this->theOutput.size(); i++) {
IHTTPRequestHandler *anOutputHandler =
(IHTTPRequestHandler *)this->theOutput.at(i);
anOutputHandler->HandleHTTPRequest(request);
}
};
};
class HTTPRequestAuthenticator :
public IHTTPRequestHandler, //the object itself "is a" IHTTPRequestHandler
//and it only outputs to IHTTPRequestHandler objects
public PipeLineProcessing::PipeSegment<IHTTPRequestHandler>
{
private:
bool requestIsAuthentic(HTTPRequest *request)
{ return true; }; //everyone's authenticated!
public:
virtual void HandleHTTPRequest(HTTPRequest *request) {
//This object only passes the HTTPRequest down the
//pipeline if the request is successfully authenticated
if (requestIsAuthentic(request)) {
for (int i=0; i < (int)this->theOutput.size(); i++) {
IHTTPRequestHandler *anOutputHandler =
(IHTTPRequestHandler *)this->theOutput.at(i);
anOutputHandler->HandleHTTPRequest(request);
}
}
}
};
上面我们定义了前两个管道段。希望它们足够易读,您可以看到它们都具有相同的继承树。在管道术语中,它们都输出到 IHTTPRequestHandler
类型对象,并作为其输入。这里的约定是,继承定义中列出的第一个类型指定了您正在实现的接口(如果有)。其次,您使用 PipeSegment
来指定您将输出到哪种类型的对象。如果您最近几个晚上没睡好,并且/或者您此时有更深入的问题,请查看提供的演示应用程序中的代码。它应该能弄清楚——或者让您入睡,取决于您的病情。
请也注意每个类中的 for
循环。这将在稍后详细讨论。
所以,另外两个类也会看起来类似——可能像这样:
class HTTPRequestAuthorizer :
public IHTTPRequestHandler, //the object itself "is a" IHTTPRequestHandler
//and it only outputs to IHTTPRequestHandler objects
public PipeLineProcessing::PipeSegment<IHTTPRequestHandler>
{
private:
bool requestIsAuthorized(HTTPRequest *request)
{ return true; }; //everyone's authorized!
public:
virtual void HandleHTTPRequest(HTTPRequest *request) {
//This object only passes the HTTPRequest down the
//pipeline if the request is successfully authorized
if (requestIsAuthorized(request)) {
for (int i=0; i < (int)this->theOutput.size(); i++) {
IHTTPRequestHandler *anOutputHandler =
(IHTTPRequestHandler *)this->theOutput.at(i);
anOutputHandler->HandleHTTPRequest(request);
}
}
}
};
class HTTPRequestResponder :
public IHTTPRequestHandler, //the object itself "is a" IHTTPRequestHandler
//and it is the end of the tail -- it has no output objects
public PipeLineProcessing::IPipeTail
{
private:
void respondToRequest(HTTPRequest *request)
{ /* umm, nothing here. */ };
public:
virtual void HandleHTTPRequest(HTTPRequest *request) {
respondToRequest(request);
}
};
就这样!现在您可以执行我们最初开始执行的代码,并将 HTTPRequest
发送到 HTTPRequestHandler
。HTTPRequest
对象将通过我们这个小巧的管道进行身份验证、授权和响应——所有这些都是自动完成的!
唯一剩下的问题是,“for
循环是怎么回事?”嗯,这是整个过程中唯一糟糕的部分。我无法找到一种方法来自动确定和调用输出对象的方法,而不使事情变得极其复杂,添加额外的库依赖,以及/或者可能使用函数对象/委托,而我对这些还不太熟悉。我不想这样做,所以我想保持简单,并要求用户明确说明何时希望将输出发送到管道的其余部分。
for
循环遍历从 PipeLineProcessing::PipeSegmentBase
基类继承的 theOutput
STL 向量。该向量存储指向此管道段连接到的所有管道段对象的指针(即,将其输出发送到)。当它遍历这些输出处理程序时,它会将它们下转换为指定类定义的模板参数类型的指针。通过在该类型上调用所需的方法,管道将继续。
聪明的程序员在看到“下转换”这个词时应该会警醒,因为这被认为是不安全的、危险的转换。与上转换(将派生对象转换为基类类型,因此始终安全)不同,下转换将基类转换为派生类。这是危险的,因为一般来说,一个人永远不知道运行时执行的对象是否会是派生类型,而一个人总是知道派生类型可以被视为基类型。用一个物理例子来说,所有的足球都是球(上转换),但并非所有的球都是足球(下转换)。
那么,我们如何才能安全地将一个球下转换为一个足球呢?只有当我们确定它就是足球的时候。在这个框架中,这种确定性是通过向 theOutput
向量添加对象的类来实现的。虽然它们在内部是通用的,但对客户端公开的促进连接的唯一方法——+=
运算符和 connectTo
方法——只接受适当类型的对象。换句话说,我们让编译器进行检查;除非添加到管道末尾的对象是,或者可以安全地被视为(即,上转换为)该类定义的输出部分中指定的类型,否则它将在编译时被标记。这是这个小程序项目中比较酷的技巧之一,而且这很大程度上要归功于我发现的 Kevlin Henney 技巧。我在我的无聊演示中展示了这些示例。
这是我第一次提交给 CodeProject,所以请尝试一下并留下评论。
相关链接
- VTK - Kitware 的 Visual Toolkit。一个开源的 3D 图形/可视化框架,建立在图形管道概念之上。我不确定他们是否使用了与我这里提出的类似的设计,因为我没有查看代码。但我使用过这个工具包,我希望他们使用了像这样的机制来做到这一点!
- 通用数据处理管道库。 C++ 中处理管道的完全不同的方法。主要基于宏,如果您在应用程序中多次使用完全相同的管道,则很有用。
- 酷炫模板技巧。这是 Kevlin Henney 提供的一个很棒的小技巧。当您希望在编译时要求模板参数是特定基类型时,可以使用它。
关于演示的一些说明
- 我希望这次提交能获得声望卓著的 Code Project“最无聊演示应用程序”奖。请不要下载演示应用程序,认为它会做些疯狂的事情,否则您会失望的。它真的只是为那些希望使用此框架构建管道的人提供一个起点。
- 除了演示之外,我还提供了 VS 2003 .NET 的解决方案和项目文件,以便在该环境中轻松构建。
- 此外,我还包含了用于在 Eclipse CDT(我选择的 IDE)中构建的 .cdtbuild、.cdtproject 和 .project 文件。
- 由于该框架完全是基于头文件的(.cpp 文件仅包含文档),您所要做的就是将它们放在某个地方,并使其对您的工具链可访问。在这种情况下,我包含的项目文件设置为在包含演示项目目录的同一个根目录下查找一个名为“Pipeline”的目录。例如,如果“PipelineDemo”目录位于“projects”下,那么当它在“projects”下找到“Pipeline”目录时,演示项目将正确构建。否则,您将需要将其指向系统上“Pipeline”文件夹的位置。
许可证
G & A Technical Software (GATS) 已将此代码发布到公共领域。这意味着您可以采用此代码,并且几乎没有任何法律义务。没有任何“Copyleft”之类的东西,也没有要求您保留头文件完整,什么都没有。事实上,唯一的规定是,通过使用该软件,您使 GATS 免于承担任何和所有责任——我们都不会对您或贵公司使用该代码可能引起的任何困难负责。有关更多详细信息,请阅读源文件中任何文件的标题。
未来
与所有开源软件一样,该框架可以(希望很容易地)得到扩展,为使用它的开发人员提供更多功能。我的一些想法是:
- 多方向/多通道管道 - 在管道中,通常直观地需要一个双向连接,一个概念上“向下游”流,另一个“向上游”流。目前,为了实现这一点,必须维护两个不相关的管道。如果它们都可以定义在单个管道中,那就太好了。
- 函数对象/委托 - 避免上述
for
循环的一种方法将是很好的。 - 内置控制信号 - 一个通用的机制,用于对管道进行基本控制和分析,将对许多潜在用户有益且有用。
- 多线程支持 - 在装配线处理管道中,过程的每个步骤都在并发处理其部分。如果每个管道段都在自己的线程中启动,这将是一个重大的成就/改进。
- 更好的错误处理和异常支持 - 目前,处理管道中的错误没有任何机制。拥有其中一个肯定会带来优势。
- 一个主要的
Pipeline
类型 - 让用户能够用一个语句定义整个管道,类似于他们在上面提到的 borland 文章中那样做,将是一个很棒的补充——而且它可能会使其中一些其他改进/添加/功能更容易实现。
显然,如果您进行了任何您想分享的改进,请将它们发送给我,如果我觉得它们不错,我一定会把它们添加到这里。
历史
- 原始发布:2006 年 9 月 23 日