将共享数据封装在软件管道中的良好实施策略


13

我正在努力重构现有Web服务的某些方面。实现服务API的方式是通过具有一种“处理管道”,其中有按顺序执行的任务。毫不奇怪,以后的任务可能需要由以前的任务计算的信息,当前完成此操作的方法是将字段添加到“管道状态”类中。

我一直在思考(并希望?),比在管道步骤之间共享信息的更好方法要好于拥有一个具有成千上万个字段的数据对象,其中某些字段对某些处理步骤有意义,而对另一些处理步骤却没有意义。使该类成为线程安全类将是一个巨大的痛苦(我不知道它是否可能实现),无法推断其不变性(很可能没有任何原因)。

我翻阅了《四人帮》设计模式书以找到一些灵感,但我觉得那儿没有解决的办法(Memento的想法有些相同,但不完全相同)。我也在网上看过,但是第二次搜索“管道”或“工作流”时,您就会被Unix管道信息或专有工作流引擎和框架所淹没。

我的问题是-您将如何处理记录软件处理管道执行状态的问题,以便以后的任务可以使用以前的任务计算的信息?我猜想与Unix管道的主要区别在于,您不仅在乎前一项任务的输出。


根据要求,使用一些伪代码来说明我的用例:

“管道上下文”对象具有许多字段,不同的管道步骤可以填充/读取这些字段:

public class PipelineCtx {
    ... // fields
    public Foo getFoo() { return this.foo; }
    public void setFoo(Foo aFoo) { this.foo = aFoo; }
    public Bar getBar() { return this.bar; }
    public void setBar(Bar aBar) { this.bar = aBar; }
    ... // more methods
}

每个管道步骤也是一个对象:

public abstract class PipelineStep {
    public abstract PipelineCtx doWork(PipelineCtx ctx);
}

public class BarStep extends PipelineStep {
    @Override
    public PipelineCtx doWork(PipelieCtx ctx) {
        // do work based on the stuff in ctx
        Bar theBar = ...; // compute it
        ctx.setBar(theBar);

        return ctx;
    }
}

对于假设类似FooStep,它可能需要由BarStep计算的Bar以及其他数据。然后,我们进行了真正的API调用:

public class BlahOperation extends ProprietaryWebServiceApiBase {
    public BlahResponse handle(BlahRequest request) {
        PipelineCtx ctx = PipelineCtx.from(request);

        // some steps happen here
        // ...

        BarStep barStep = new BarStep();
        barStep.doWork(crx);

        // some more steps maybe
        // ...

        FooStep fooStep = new FooStep();
        fooStep.doWork(ctx);

        // final steps ...

        return BlahResponse.from(ctx);
    }
}

6
不要越过哨所,但要标记国防部移动
棘手怪胎

1
我会继续前进,我想我应该花更多的时间熟悉规则。谢谢!
RuslanD

1
您是否在为实现避免任何持久性数据存储,或者此时有什么需要抢夺的?
CokoBWare

1
嗨,RuslanD,欢迎您!实际上,这比堆栈溢出更适合程序员,因此我们删除了SO版本。请记住@ratchetfreak提到的内容,您可以举报适当的关注,并要求将问题迁移到更合适的站点,而无需交叉发布。在这两个站点之间进行选择的经验法则是,程序员用于解决您在白板上设计项目时遇到的问题,而堆栈溢出用于解决更多技术问题(例如,实现问题)。有关更多详细信息,请参见我们的常见问题解答
yannis

1
如果将体系结构更改为处理DAG(有向无环图)而不是管道,则可以显式传递先前步骤的结果。
Patrick

Answers:


4

使用流水线设计的主要原因是您要分离各阶段。因为一个阶段可能用于多个管道(例如Unix Shell工具),或者因为您获得了一定的扩展优势(即,您可以轻松地从单节点体系结构转换为多节点体系结构)。

无论哪种情况,都需要为流水线的每个阶段提供完成其工作所需的一切。没有理由不能使用外部存储(例如数据库),但是在大多数情况下,最好将数据从一个阶段传递到另一个阶段。

但是,这并不意味着您必须或应该在每个可能的字段中传递一个大消息对象(尽管请参见下文)。相反,管道中的每个阶段都应为其输入和输出消息定义接口,这些接口标识该阶段所需的数据。

这样,您就可以在实现实际消息对象方面具有很大的灵活性。一种方法是使用实​​现所有必要接口的巨大数据对象。另一个方法是围绕一个简单的包装盒创建包装器类Map。还有一个方法是围绕数据库创建包装器类。


1

有些想法浮现在脑海,首先是我没有足够的信息。

  • 是否每个步骤都会产生流水线之外使用的数据,还是我们只关心最后阶段的结果?
  • 有很多大数据问题吗?即。内存问题,速度问题等

答案可能会让我更仔细地考虑设计,但是根据您所说的,我可能会首先考虑两种方法。

将每个阶段构造为自己的对象。第n个阶段将有1到n-1个阶段作为代表列表。每个阶段都封装数据和数据处理;减少总体复杂性和每个对象中的字段。您还可以在较早的阶段通过遍历委托,使较后的阶段根据需要访问数据。您仍然在所有对象之间保持非常紧密的耦合,因为重要的是阶段(即所有attrs)的结果,但是它大大减少了,并且每个阶段/对象都可能更具可读性和可理解性。通过使委托列表变得懒惰,并根据需要使用线程安全队列在每个对象中填充委托列表,可以使其成为线程安全的。

另外,我可能会做一些与您的工作相似的事情。通过代表每个阶段的功能的海量数据对象。这通常更快,更轻便,但由于它只是一大堆数据属性,因此更加复杂且容易出错。显然不是线程安全的。

老实说,我经常为ETL和其他一些类似的问题做后一遍。由于数据量而不是可维护性,我专注于性能。而且,它们是一次性的,将不再使用。


1

这看起来像GoF中的链模式。

一个很好的起点是看一下Commons链的作用。

组织复杂处理流程执行的一种流行技术是“责任链”模式,这在经典的“四人帮”设计模式书中(在许多地方中都有描述)。尽管实现此设计模式所需的基本API合同非常简单,但拥有有助于使用模式并(更重要的是)鼓励从多个不同来源组成命令实现的基础API很有用。

为此,Chain API将计算建模为一系列可以组合为“链”的“命令”。命令的API由单个方法(execute())组成,该方法传递了一个包含计算动态状态的“上下文”参数,其返回值是一个布尔值,该布尔值确定当前链的处理是否已完成( true),或者是否应将处理委托给链中的下一个命令(false)。

“上下文”抽象旨在将命令实现与运行它们的环境隔离开(例如,可以在Servlet或Portlet中使用的命令,而不必直接绑定到这两个环境中的任何一个的API契约)。对于需要在委派之前分配资源,然后在返回时释放资源的命令(即使委派给的命令引发异常),“ command”的“ filter”扩展提供了postprocess()这种清除方法。最后,可以存储命令并在“目录”中查找命令,以允许推迟实际执行哪个命令(或链)的决策。

为了最大程度地发挥责任链模式API的效用,除了适当的JDK之外,还以零依赖关系的方式定义了基本接口协定。提供了这些API的便捷基类实现,以及针对Web环境的更专业(但可选)的实现(即Servlet和Portlet)。

假定命令实现旨在符合这些建议,那么在Web应用程序框架(例如Struts)的“前端控制器”中使用责任链API应该是可行的,但也可以在业务中使用它。逻辑层和持久层,以通过组合对复杂的计算需求进行建模。此外,将计算分离为在通用上下文中运行的离散命令可以更轻松地创建可进行单元测试的命令,因为可以通过观察所提供上下文中的相应状态变化来直接测量执行命令的影响...


0

我可以想象的第一个解决方案是使步骤明确。它们每个都成为能够处理一条数据并将其传输到下一个处理对象的对象。每个流程都会产生一个新的(理想情况下是不可变的)产品,因此流程之间没有交互作用,也就不会有数据共享带来的风险。如果某些进程比其他进程更耗时,则可以在两个进程之间放置一些缓冲区。如果您正确地将调度程序用于多线程,它将分配更多资源来刷新缓冲区。

第二种解决方案可能是使用专用框架来考虑“消息”而不是管道。然后,您有一些“角色”从其他角色接收消息,并将其他消息发送给其他角色。您可以在管道中组织演员,并将主要数据提供给发起链的第一个演员。没有共享数据,因为共享已被消息发送代替。我知道Scala的actor模型可以在Java中使用,因为这里没有Scala特定的东西,但是我从未在Java程序中使用过。

解决方案是相似的,您可以使用第一个实施第二个。基本上,主要概念是处理不可变数据,以避免由于数据共享而造成的传统问题,并创建代表管道中流程的显式且独立的实体。如果满足这些条件,则可以轻松创建清晰,简单的管道,并在并行程序中使用它们。


嘿,我用一些伪代码更新了我的问题-实际上,我们确实有明确的步骤。
RuslanD 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.