Java中的管道设计模式

2023/05/26

一、概述

在本教程中,我们将回顾一个有趣的模式,它不是经典GoF模式的一部分——管道模式。

它功能强大,可以帮助解决棘手的问题并改进应用程序的设计。此外,Java 有一些内置的解决方案来帮助实现这种模式;我们将在最后讨论它们。

2.相关模式

通常,管道模式被比作责任链Pipeline 与Decorator也有很多共同点。在某些方面,它更接近装饰者而不是责任链。让我们回顾一下这些模式之间的异同。

2.1. 责任链

管道和责任链经常被比较,因为这两种模式都明确声明了一个循序渐进的过程。Pipeline 和 Chain of Responsibility 之间的第一个区别是后者通常没有handleRequest()方法的返回值:

img

然而,没有什么能阻止我们从handleRequest()方法返回值。在这种情况下,它将被定义为Handler接口的一部分。

2.2. 装潢师

Decorator 并没有立即提出与管道模式的相似之处,因为它没有明确说明其链状结构。但是,通过委托和递归嵌套,其行为与责任链或管道非常相似:

img

在经典 (GoF) 实现中,此模式添加了行为并且没有操作的返回值。但是,这是更改对象状态或使用不同组件处理数据的合理选择。通常,状态改变解决方案可能过于复杂,因为我们可以使用更直接的结构来实现结果。同时,Decorator 提供了时间依赖性的管理并维护了执行的顺序。

3.流水线

管道模式背后的主要思想是创建一组操作(管道)并通过它传递数据。虽然责任链和装饰者可以部分处理这个任务。Pipeline 的主要功能在于它对其结果的类型具有灵活性。

责任链和装饰器仅返回分别在处理程序和组件接口中定义的类型。另一方面,管道可以处理任何类型的输入和输出。这种模式的灵活性是它的主要特点。

3.1. 不可变管道

让我们为不可变管道创建一个简单示例。我们将从Pipe接口开始:

public interface Pipe<IN, OUT> {
    OUT process(IN input);
}

这是一个非常简单的接口,只有一种方法,它接受输入并产生输出。接口是参数化的,我们可以在其中提供任何实现。另外,请注意本文中的示例将与类型参数的官方命名约定不同。这是为了更好地区分方法级别和类级别的参数。现在让我们创建一个将管道保存在管道中的类:

public class Pipeline<IN, OUT> {

    private Collection<Pipe<?, ?>> pipes;

    private Pipeline(Pipe<IN, OUT> pipe) {
        pipes = Collections.singletonList(pipe);
    }

    private Pipeline(Collection<Pipe<?, ?>> pipes) {
        this.pipes = new ArrayList<>(pipes);
    }

    public static <IN, OUT> Pipeline<IN, OUT> of(Pipe<IN, OUT> pipe) {
        return new Pipeline<>(pipe);
    }

    public <NEW_OUT> Pipeline<IN, NEW_OUT> withNextPipe(Pipe<OUT, NEW_OUT> pipe) {
        final ArrayList<Pipe<?, ?>> newPipes = new ArrayList<>(pipes);
        newPipes.add(pipe);
        return new Pipeline<>(newPipes);
    }

    public OUT process(IN input) {
        Object output = input;
        for (final Pipe pipe : pipes) {
            output = pipe.process(input);
        }
        return (OUT) output;
    }
}

构造函数和静态工厂非常简单,所以让我们专注于withNextPipe方法:

public <NEW_OUT> Pipeline<IN, NEW_OUT> withNextPipe(Pipe<OUT, NEW_OUT> pipe) {
    final ArrayList<Pipe<?, ?>> newPipes = new ArrayList<>(pipes);
    newPipes.add(pipe);
    return new Pipeline<>(newPipes);
}

因为我们需要一定程度的类型安全并且不允许管道失败,所以我们需要存储有关当前输入和输出类型的信息。此信息存储在管道 对象中。但是,在添加新Pipe 时,我们需要更新此信息,而我们不能对同一个对象执行此操作。这就是为什么决定让Pipeline不可变并且添加一个新的Pipe将产生一个新的单独的 Pipeline。

Pipeline的流程部分非常简单:

public OUT process(IN input) {
    Object output = input;
    for (final Pipe pipe : pipes) {
        output = pipe.process(output);
    }
    return (OUT) output;
}

但是,在这种情况下我们需要使用原始类型。我们确保 Pipes 正确通过,所以应该没有问题。最终,我们必须将结果转换为预期的类型。

3.2. 简单管道

我们可以稍微简化上面的示例并完全摆脱 Pipeline类:

public interface Pipe<IN, OUT> {
    OUT process(IN input);

    default <NEW_OUT> Pipe<IN, NEW_OUT> add(Pipe <OUT, NEW_OUT> pipe) {
        return input -> pipe.execute(execute(input));
    }
}

此实现更接近于之前讨论的模式(装饰器和责任链),因为它具有从一个管道委托到另一个管道的递归结构。但是,在此实现中,所有管道都隐藏在方法调用中,因此很难获取整个管道。同时,与之前使用Pipeline 的实现相比,该方案非常简单和灵活。

3.3. 功能解决方案

我们可以迭代以前的解决方案并使用 vanilla Java 改进它。让我们再次看一下Pipe 接口:

public interface Pipe<IN, OUT> {
    OUT process(IN input);

    default <NEW_OUT> Pipe<IN, NEW_OUT> add(Pipe <OUT, NEW_OUT> pipe) {
        return input -> pipe.execute(execute(input));
    }
}

这是一个具有一种默认方法的功能接口。我们可以用一个已经存在的Function接口来代替它:

public interface Function<T, R> {
    //...
    R apply(T t);
    //...
}

此外, Function接口包含几个有用的方法,其中之一是andThen:

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
    Objects.requireNonNull(after);
    return (T t) -> after.apply(apply(t));
}

我们可以使用它来代替我们之前的add方法。此外,Function接口提供了一种在管道开头添加函数的方法:

default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
    Objects.requireNonNull(before);
    return (V v) -> apply(before.apply(v));
}

通过使用Function,我们可以创建非常灵活且易于使用的管道:

@Test
void whenCombiningThreeFunctions_andInitializingPipeline_thenResultIsCorrect() {
    Function<Integer, Integer> square = s -> s * s;
    Function<Integer, Integer> half = s -> s / 2;
    Function<Integer, String> toString = Object::toString;
    Function<Integer, String> pipeline = square.andThen(half)
        .andThen(toString);
    String result = pipeline.apply(5);
    String expected = "12";
    assertEquals(expected, result);
}

管道直接获取参数,使这种方法非常干净。作为奖励,我们可以使用BiFunctions扩展管道:

@Test
void whenCombiningFunctionAndBiFunctions_andInitializingPipeline_thenResultIsCorrect() {
    BiFunction<Integer, Integer, Integer> add = Integer::sum;
    BiFunction<Integer, Integer, Integer> mul = (a, b) -> a * b;
    Function<Integer, String> toString = Object::toString;
    BiFunction<Integer, Integer, String> pipeline = add.andThen(a -> mul.apply(a, 2))
        .andThen(toString);
    String result = pipeline.apply(1, 2);
    String expected = "6";
    assertEquals(expected, result);
}

因为andThen方法采用 Function,所以我们必须使用柯里化将mul BiFunction转换 为函数。尽管我们在函数内部而不是在调用管道时提供参数,但该解决方案仍然简单明了。Stream API 中使用了相同的方法,流中的操作序列称为管道。

4。结论

在本文中,我们将流水线模式作为一种强大的工具进行了讨论,但它并不流行,也未包含在已知模式的经典 (GoF) 列表中。

我们可以通过多种方式实现这种模式,但 Java 也提供了一个很好的选择,可以通过 Stream API 来利用它。在大多数情况下,Java 提供的解决方案就足够了。在特定管道的情况下,可以从头开始实施它们。

这种模式的主要好处是它允许简化逻辑并使代码更易于维护,同时简洁明了。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章