I have a small framework that allows me to create a pipe&filter system. I had the idea of using fluent interfaces to build the pipe&filter system:开发者_StackOverflow中文版
PipeFilter pipeFilter = PipeFilter.StartBuild()
.AddFilter(new SomeFilter1())
.AddFilter(new SomeFilter2())
.AddFilter(new SomeFilter3())
.AddFilter(new SomeFilter4())
.Build();
The shown code works as expected. Here is a "picture" of the system:
SomeFilter1 -> SomeFilter2 -> SomeFilter3 -> SomeFilter4
Now, there is a kind of Filter that instead of one output has two, instead. I call it bifurcation
.
Here is an example of a system with a bifurcation
:
|-> SomeFilter2 -> SomeFilter3
SomeFilter1 --|
|-> SomeFilter4
I'd like to implement something like this:
PipeFilter pipeFilter = PipeFilter.StartBuild()
.AddFilter(new SomeFilter1())
.AddBifurcation()
.Output1()
.AddFilter(new SomeFilter2())
.AddFilter(new SomeFilter3())
.Output2()
.AddFilter(new SomeFilter4())
.Build();
But it seems I just can't get it right. Is this even possible, to do?
In the first example, I just needed a PipeFilterBuilder
(that is being returned by PipeFilter.StartBuild()
). In this second example, I've tried creating other kinds of builders to bring into the mix but that seems to be of no avail.
Forgot to mention, the idea would be that I could nest bifurcations anywhere I want, that is, I could get "trees" full of branches!
Can anybody be of any help with this?
I would go the following way
PipeFilter pipeFilter = PipeFilter.StartBuild()
.AddFilter(new SomeFilter1())
.AddBifurcation(
withOutput(1)
.AddFilter(new SomeFilter2())
.AddFilter(new SomeFilter3()), /* this separates first and second output */
withOutput(2)
.AddFilter(new SomeFilter4())
)
.Build();
In more format terms, I define the Bifurcation class to be an implementor of the Filter
interface.
A bifurcation can have any number of Output filters, chained using Output
object. To distinguish these output objects, they all have an index.
As a consequence, addBifurcation create a new Bifurcation object and adds it, while withOutput(int)
is a static method creating an Output
object, which has all the required method. Notice this point implies you get rid of the classical distinction between Builder and builded object, in favor of a code where the Builder methods are defined in the base interface for Filter.
I think you are being constrained by your notation. At the lowest level, your filtering system can be composed of primitive filters, and sequential and parallel compositions. Your first example could be written (in pseudocode):
pipeFilter = Seq(new SomeFilter1(),
Seq(new SomeFilter2(),
Seq(new SomeFilter3(), new SomeFilter4())));
With such an interface it is completely obvious how to add parallel -- or really any other kind of combinator -- into the interface:
pipeFilter = Seq(new SomeFilter1(),
Parallel(Seq(new SomeFilter2(), new SomeFilter3()),
Seq(new SomeFitler4())));
Cumbersome though it may seem, I would suggest building your interface this way (called a "functional" as oppposed to "imperative" interface), and then writing convenience methods to reduce some of the structural burden, for example variants of Seq
and Parallel
that take an arbitrary numbers of arguments -- but it would probably be best to have those simply delegate down to folds of the binary variants.
To elaborate on the subtler design issue here, the class or interface with which you are working is a filter builder, not a filter itself. Parallel
and Seq
are methods on that class. This gives you the option to implement the combinators in multiple ways for multiple interpretations. I would write the interface like this:
interface FilterBuilder<Filter> {
Filter Seq(Filter a, Filter b);
Filter Parallel(Filter a, Filter b);
}
It may not be perfect for your needs, but it is a nice, flexible design pattern that doesn't seem to be widely known.
It is possible to implement the system as you've designed it.
You won't need any builder other than PipeFilterBuilder
, but you will need a data structure capable of representing your tree of Filters. Your PipeFilterBuilder
then holds a reference to this structure and keeps track of the current insertion point.
Any operation you perform on PipeFilterBuilder
needs to update the insertion point and return the builder itself (this
). Calling AddBifurcation
would add the current insertion point to, say, a stack. Conversely, Output2
would set the insertion point to the value popped off the stack. Other functions should be fairly trivial.
精彩评论